From cbf8deabf36fdab53cd0cb06bcf3dd69818881f0 Mon Sep 17 00:00:00 2001 From: Vasyka Date: Tue, 8 Sep 2026 05:07:32 +0000 Subject: [PATCH] Fix labor table grouping colspan + auto-import catalog on deploy - LaborResource: hide category column from data rows by default (toggleable) so group headers span all columns cleanly; the group header already shows the localized category name - ImportLaborCatalogCommand: add --all flag to iterate every company, refactor into importForCompany() helper, keep idempotent (skips existing codes, updates only with --force) - entrypoint.sh: run catalog:import-labors --all after each deploy so all 386 operations + 45 categories are seeded automatically Co-Authored-By: Claude Sonnet 4.6 --- .../Commands/ImportLaborCatalogCommand.php | 197 ++++++++++-------- .../Tenant/Resources/LaborResource.php | 4 +- docker/entrypoint.sh | 3 + 3 files changed, 112 insertions(+), 92 deletions(-) diff --git a/app/Console/Commands/ImportLaborCatalogCommand.php b/app/Console/Commands/ImportLaborCatalogCommand.php index 8bfb3ae..69366f1 100644 --- a/app/Console/Commands/ImportLaborCatalogCommand.php +++ b/app/Console/Commands/ImportLaborCatalogCommand.php @@ -8,67 +8,96 @@ use App\Models\Tenant\Labor; use App\Models\Tenant\LaborCategory; use App\Tenancy\TenantManager; use Illuminate\Console\Command; -use Illuminate\Support\Facades\DB; class ImportLaborCatalogCommand extends Command { - protected $signature = 'catalog:import-labors {company : Company slug or numeric ID} {--file= : Path to JSON file (default: psauto-katalog-rabot-full.json)} {--force : Update existing records}'; + protected $signature = 'catalog:import-labors + {company? : Company slug or numeric ID (omit with --all)} + {--all : Import for every company in the database} + {--file= : Path to JSON file (defaults to database/data/labor-catalog.json)} + {--force : Update existing records by code}'; - protected $description = 'Import labor catalog from psauto-katalog-rabot-full.json into labor_categories and labors tables'; + protected $description = 'Import labor catalog (categories + operations) from JSON into labor_categories and labors tables'; public function handle(): int { - $companyArg = $this->argument('company'); - $company = is_numeric($companyArg) - ? Company::find((int) $companyArg) - : Company::where('slug', $companyArg)->first(); - - if (! $company) { - $this->error("Company '{$companyArg}' not found."); - return 1; - } - - app(TenantManager::class)->setCurrent($company); - - $file = $this->option('file') ?: database_path('data/labor-catalog.json'); - if (! file_exists($file)) { - foreach ([ - '/tmp/service/todo/psauto-katalog-rabot-full.json', - storage_path('app/labor-catalog.json'), - ] as $path) { - if (file_exists($path)) { $file = $path; break; } - } - } - - if (! file_exists($file)) { - $this->error("JSON file not found. Pass --file=/path/to/psauto-katalog-rabot-full.json"); + $file = $this->resolveFile(); + if (! $file) { + $this->error('JSON file not found. Use --file=/path/to/labor-catalog.json'); return 1; } $data = json_decode(file_get_contents($file), true); - $allCats = $data['categories'] ?? []; - $operations = $data['operations'] ?? []; + if (! $data) { + $this->error('Failed to parse JSON file.'); + return 1; + } - $catMap = collect($allCats)->keyBy('id'); + if ($this->option('all')) { + $companies = Company::all(); + foreach ($companies as $company) { + $this->importForCompany($company, $data); + } + return 0; + } - // Identify leaf categories (those that have operations assigned to them) - $usedCatIds = collect($operations)->pluck('service_category_id')->unique(); - $leafCats = $catMap->only($usedCatIds->all()); + $arg = $this->argument('company'); + if (! $arg) { + $this->error('Provide a company slug/id, or use --all.'); + return 1; + } - $this->info("Company: {$company->name} (id={$company->id})"); - $this->info("Categories to import: {$leafCats->count()}"); - $this->info("Operations to import: " . count($operations)); + $company = is_numeric($arg) + ? Company::find((int) $arg) + : Company::where('slug', $arg)->first(); + + if (! $company) { + $this->error("Company '{$arg}' not found."); + return 1; + } + + $this->importForCompany($company, $data); + return 0; + } + + private function resolveFile(): ?string + { + $file = $this->option('file'); + if ($file && file_exists($file)) return $file; + + foreach ([ + database_path('data/labor-catalog.json'), + '/tmp/service/todo/psauto-katalog-rabot-full.json', + storage_path('app/labor-catalog.json'), + ] as $path) { + if (file_exists($path)) return $path; + } + + return null; + } + + private function importForCompany(Company $company, array $data): void + { + app(TenantManager::class)->setCurrent($company); $force = $this->option('force'); - // ── 1. Upsert labor categories ──────────────────────────────────── - $this->info('Importing categories...'); - $catCodeToName = []; // leaf code → canonical name (= code itself) - $sortBase = 100; + $allCats = $data['categories'] ?? []; + $operations = $data['operations'] ?? []; + $catMap = collect($allCats)->keyBy('id'); + + $usedCatIds = collect($operations)->pluck('service_category_id')->unique(); + $leafCats = $catMap->only($usedCatIds->all()); + + $this->line("── {$company->name} (id={$company->id}) ── cats:{$leafCats->count()} ops:" . count($operations)); + + // ── 1. Labor categories ─────────────────────────────────────────── + $catCodeToId = []; + $sortBase = 100; foreach ($leafCats as $leaf) { - $parent = $catMap->get($leaf['parent_id']); - $nameRu = ($parent ? $parent['name'] . ' · ' : '') . $leaf['name']; - $code = $leaf['code']; // canonical key + $parent = $catMap->get($leaf['parent_id']); + $nameRu = ($parent ? $parent['name'] . ' · ' : '') . $leaf['name']; + $code = $leaf['code']; $existing = LaborCategory::withoutGlobalScope(TenantScope::class) ->where('company_id', $company->id) @@ -78,75 +107,63 @@ class ImportLaborCatalogCommand extends Command if ($existing) { if ($force) { $existing->update(['name_ru' => $nameRu]); - $this->line(" Updated cat: {$code}"); } } else { - LaborCategory::withoutGlobalScope(TenantScope::class)->create([ + $existing = LaborCategory::withoutGlobalScope(TenantScope::class)->create([ 'company_id' => $company->id, 'name' => $code, 'name_ro' => $nameRu, 'name_ru' => $nameRu, 'sort_order' => $sortBase + ($leaf['sort_order'] ?? 0), ]); - $this->line(" Created cat: {$code} ({$nameRu})"); } - $catCodeToName[$leaf['id']] = $code; + $catCodeToId[$leaf['id']] = $code; $sortBase += 5; } - // ── 2. Upsert labors ───────────────────────────────────────────── - $this->info('Importing operations...'); + // ── 2. Labors ───────────────────────────────────────────────────── $created = 0; - $updated = 0; $skipped = 0; foreach ($operations as $op) { - $catId = $op['service_category_id']; - $catKey = $catCodeToName[$catId] ?? null; - if (! $catKey) { + $catKey = $catCodeToId[$op['service_category_id']] ?? null; + if (! $catKey) { $skipped++; continue; } + + $existing = Labor::withoutGlobalScope(TenantScope::class) + ->where('company_id', $company->id) + ->where('code', $op['code']) + ->exists(); + + if ($existing) { + if ($force) { + Labor::withoutGlobalScope(TenantScope::class) + ->where('company_id', $company->id) + ->where('code', $op['code']) + ->update([ + 'name_ru' => $op['name'], + 'hours' => round($op['norm_minutes'] / 60, 2), + 'price' => round($op['base_price_minor'] / 100, 2), + ]); + } $skipped++; continue; } - $hours = round(($op['norm_minutes'] ?? 0) / 60, 2); - $price = round(($op['base_price_minor'] ?? 0) / 100, 2); - $nameRu = $op['name']; - $code = $op['code']; - - $existing = Labor::withoutGlobalScope(TenantScope::class) - ->where('company_id', $company->id) - ->where('code', $code) - ->first(); - - if ($existing) { - if ($force) { - $existing->update([ - 'name_ru' => $nameRu, - 'hours' => $hours, - 'price' => $price, - ]); - $updated++; - } else { - $skipped++; - } - } else { - Labor::withoutGlobalScope(TenantScope::class)->create([ - 'company_id' => $company->id, - 'category' => $catKey, - 'code' => $code, - 'name_ro' => $nameRu, - 'name_ru' => $nameRu, - 'pricing_mode' => 'hourly', - 'hours' => $hours, - 'price' => $price, - 'is_active' => (bool) ($op['is_active'] ?? true), - ]); - $created++; - } + Labor::withoutGlobalScope(TenantScope::class)->create([ + 'company_id' => $company->id, + 'category' => $catKey, + 'code' => $op['code'], + 'name_ro' => $op['name'], + 'name_ru' => $op['name'], + 'pricing_mode' => 'hourly', + 'hours' => round($op['norm_minutes'] / 60, 2), + 'price' => round($op['base_price_minor'] / 100, 2), + 'is_active' => (bool) ($op['is_active'] ?? true), + ]); + $created++; } - $this->info("Done. Created: {$created}, Updated: {$updated}, Skipped: {$skipped}"); - return 0; + $this->line(" Created: {$created} Skipped/existing: {$skipped}"); } } diff --git a/app/Filament/Tenant/Resources/LaborResource.php b/app/Filament/Tenant/Resources/LaborResource.php index d608bf3..aa19cc8 100644 --- a/app/Filament/Tenant/Resources/LaborResource.php +++ b/app/Filament/Tenant/Resources/LaborResource.php @@ -85,9 +85,9 @@ class LaborResource extends Resource ->label(__('Categorie')) ->badge() ->sortable() + ->toggleable(isToggledHiddenByDefault: true) ->formatStateUsing(function ($state) { - static $opts = null; - $opts ??= \App\Models\Tenant\LaborCategory::optionsForSelect(); + $opts = \App\Models\Tenant\LaborCategory::optionsForSelect(); return $opts[$state] ?? $state; }), Tables\Columns\TextColumn::make('name_ro') diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 13c50c1..a540363 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -17,6 +17,9 @@ elif [ "${RUN_MIGRATIONS:-true}" = "true" ]; then php artisan migrate --force --no-interaction || echo "[entrypoint] migrate failed (non-fatal)" fi +# Import labor catalog for all companies (idempotent — skips existing records by code). +php artisan catalog:import-labors --all --no-interaction 2>&1 | tail -5 || echo "[entrypoint] catalog import failed (non-fatal)" + # Run seeders if requested. Uses firstOrCreate so it's idempotent. if [ "${RUN_SEED:-false}" = "true" ] && [ "${RUN_FRESH_MIGRATE:-false}" != "true" ]; then echo "[entrypoint] Running database seed..."