diff --git a/app/Console/Commands/ImportLaborCatalogCommand.php b/app/Console/Commands/ImportLaborCatalogCommand.php index 69366f1..e5c4800 100644 --- a/app/Console/Commands/ImportLaborCatalogCommand.php +++ b/app/Console/Commands/ImportLaborCatalogCommand.php @@ -85,41 +85,37 @@ class ImportLaborCatalogCommand extends Command $operations = $data['operations'] ?? []; $catMap = collect($allCats)->keyBy('id'); - $usedCatIds = collect($operations)->pluck('service_category_id')->unique(); - $leafCats = $catMap->only($usedCatIds->all()); + // Build root-category map: leaf cat_id → root code (prefix before first dot) + $rootCats = collect($allCats)->filter(fn ($c) => $c['parent_id'] === null)->keyBy('id'); + $leafToRoot = []; // leaf_cat_id → root_code + foreach ($allCats as $cat) { + $rootCode = explode('.', $cat['code'])[0]; + $leafToRoot[$cat['id']] = $rootCode; + } + $rootsNeeded = collect($operations) + ->map(fn ($op) => $leafToRoot[$op['service_category_id']] ?? null) + ->filter()->unique(); - $this->line("── {$company->name} (id={$company->id}) ── cats:{$leafCats->count()} ops:" . count($operations)); + $this->line("── {$company->name} (id={$company->id}) ── root-cats:{$rootsNeeded->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']; + // ── 1. Root labor categories (17 tops) ─────────────────────────── + foreach ($rootCats as $root) { + if (! $rootsNeeded->contains($root['code'])) continue; $existing = LaborCategory::withoutGlobalScope(TenantScope::class) ->where('company_id', $company->id) - ->where('name', $code) - ->first(); + ->where('name', $root['code']) + ->exists(); - if ($existing) { - if ($force) { - $existing->update(['name_ru' => $nameRu]); - } - } else { - $existing = LaborCategory::withoutGlobalScope(TenantScope::class)->create([ + if (! $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), + 'name' => $root['code'], + 'name_ro' => $root['name'], + 'name_ru' => $root['name'], + 'sort_order' => $root['sort_order'], ]); } - - $catCodeToId[$leaf['id']] = $code; - $sortBase += 5; } // ── 2. Labors ───────────────────────────────────────────────────── @@ -127,7 +123,7 @@ class ImportLaborCatalogCommand extends Command $skipped = 0; foreach ($operations as $op) { - $catKey = $catCodeToId[$op['service_category_id']] ?? null; + $catKey = $leafToRoot[$op['service_category_id']] ?? null; if (! $catKey) { $skipped++; continue; } $existing = Labor::withoutGlobalScope(TenantScope::class) diff --git a/app/Filament/Tenant/Resources/LaborResource.php b/app/Filament/Tenant/Resources/LaborResource.php index aa19cc8..971bd54 100644 --- a/app/Filament/Tenant/Resources/LaborResource.php +++ b/app/Filament/Tenant/Resources/LaborResource.php @@ -121,6 +121,7 @@ class LaborResource extends Resource Tables\Grouping\Group::make('category') ->label(__('Categorie')) ->titlePrefixedWithLabel(false) + ->collapsible() ->getTitleFromRecordUsing(function ($record) { $opts = \App\Models\Tenant\LaborCategory::optionsForSelect(); return $opts[$record->category] ?? $record->category; diff --git a/database/migrations/2026_09_08_100000_restructure_labor_categories_to_root.php b/database/migrations/2026_09_08_100000_restructure_labor_categories_to_root.php new file mode 100644 index 0000000..9283ac6 --- /dev/null +++ b/database/migrations/2026_09_08_100000_restructure_labor_categories_to_root.php @@ -0,0 +1,81 @@ + 'diag', 'sort' => 10, 'name_ru' => 'Диагностика', 'name_en' => 'Diagnostics'], + ['code' => 'to', 'sort' => 20, 'name_ru' => 'Техническое обслуживание', 'name_en' => 'Maintenance'], + ['code' => 'engine', 'sort' => 30, 'name_ru' => 'Двигатель и навесное', 'name_en' => 'Engine'], + ['code' => 'fuel', 'sort' => 40, 'name_ru' => 'Топливная система', 'name_en' => 'Fuel system'], + ['code' => 'trans', 'sort' => 50, 'name_ru' => 'Трансмиссия', 'name_en' => 'Transmission'], + ['code' => 'chassis', 'sort' => 60, 'name_ru' => 'Ходовая часть', 'name_en' => 'Suspension'], + ['code' => 'steering', 'sort' => 70, 'name_ru' => 'Рулевое управление', 'name_en' => 'Steering'], + ['code' => 'brakes', 'sort' => 80, 'name_ru' => 'Тормозная система', 'name_en' => 'Brakes'], + ['code' => 'wheels', 'sort' => 90, 'name_ru' => 'Колёса и развал', 'name_en' => 'Wheels & Alignment'], + ['code' => 'electric', 'sort' => 100, 'name_ru' => 'Электрика и электроника', 'name_en' => 'Electrical'], + ['code' => 'climate', 'sort' => 110, 'name_ru' => 'Климат и комфорт', 'name_en' => 'Climate & Comfort'], + ['code' => 'body', 'sort' => 120, 'name_ru' => 'Кузовные работы', 'name_en' => 'Bodywork'], + ['code' => 'hybrid', 'sort' => 130, 'name_ru' => 'Гибриды и электромобили', 'name_en' => 'Hybrid & EV'], + ['code' => 'detailing', 'sort' => 140, 'name_ru' => 'Детейлинг и мойка', 'name_en' => 'Detailing & Wash'], + ['code' => 'extra', 'sort' => 150, 'name_ru' => 'Дополнительное оборудование', 'name_en' => 'Extra equipment'], + ['code' => 'service', 'sort' => 160, 'name_ru' => 'Сервисные операции', 'name_en' => 'Service operations'], + ['code' => 'safety', 'sort' => 170, 'name_ru' => 'Пассивная безопасность', 'name_en' => 'Passive safety'], + ]; + + public function up(): void + { + $companies = DB::table('companies')->pluck('id'); + + foreach ($companies as $companyId) { + // 1. Insert root categories (skip if already exists by name=code) + foreach (self::ROOTS as $idx => $r) { + $exists = DB::table('labor_categories') + ->where('company_id', $companyId) + ->where('name', $r['code']) + ->exists(); + + if (! $exists) { + DB::table('labor_categories')->insert([ + 'company_id' => $companyId, + 'name' => $r['code'], + 'name_ro' => $r['name_ru'], // RO translation TBD by user + 'name_ru' => $r['name_ru'], + 'name_en' => $r['name_en'], + 'sort_order' => $r['sort'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + // 2. Re-point labors: "diag.scan" → "diag" (take prefix before first dot) + // Only applies to leaf codes that contain a dot. + $leafLabors = DB::table('labors') + ->where('company_id', $companyId) + ->where('category', 'like', '%.%') + ->get(['id', 'category']); + + foreach ($leafLabors as $labor) { + $rootCode = explode('.', $labor->category)[0]; + DB::table('labors') + ->where('id', $labor->id) + ->update(['category' => $rootCode]); + } + + // 3. Delete leaf labor_categories (those with a dot in their name) + DB::table('labor_categories') + ->where('company_id', $companyId) + ->where('name', 'like', '%.%') + ->delete(); + } + } + + public function down(): void + { + // Irreversible restructuring — data would need re-import + } +}; diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index a540363..13c50c1 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -17,9 +17,6 @@ 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..."