Restructure catalog to root categories + collapsible groups
- Migration: replace 45 leaf categories (diag.scan, engine.mech…) with 17 root categories (diag, engine…); update labors.category by taking the prefix before the first dot; delete the leaf LaborCategory rows - ImportLaborCatalogCommand: rewritten to use root categories directly so future imports go to the right level from the start - LaborResource: add ->collapsible() to groups so user can collapse/ expand each category section to see only headers or only one group - entrypoint.sh: remove one-time catalog import (was already done) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -85,41 +85,37 @@ class ImportLaborCatalogCommand extends Command
|
|||||||
$operations = $data['operations'] ?? [];
|
$operations = $data['operations'] ?? [];
|
||||||
$catMap = collect($allCats)->keyBy('id');
|
$catMap = collect($allCats)->keyBy('id');
|
||||||
|
|
||||||
$usedCatIds = collect($operations)->pluck('service_category_id')->unique();
|
// Build root-category map: leaf cat_id → root code (prefix before first dot)
|
||||||
$leafCats = $catMap->only($usedCatIds->all());
|
$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 ───────────────────────────────────────────
|
// ── 1. Root labor categories (17 tops) ───────────────────────────
|
||||||
$catCodeToId = [];
|
foreach ($rootCats as $root) {
|
||||||
$sortBase = 100;
|
if (! $rootsNeeded->contains($root['code'])) continue;
|
||||||
|
|
||||||
foreach ($leafCats as $leaf) {
|
|
||||||
$parent = $catMap->get($leaf['parent_id']);
|
|
||||||
$nameRu = ($parent ? $parent['name'] . ' · ' : '') . $leaf['name'];
|
|
||||||
$code = $leaf['code'];
|
|
||||||
|
|
||||||
$existing = LaborCategory::withoutGlobalScope(TenantScope::class)
|
$existing = LaborCategory::withoutGlobalScope(TenantScope::class)
|
||||||
->where('company_id', $company->id)
|
->where('company_id', $company->id)
|
||||||
->where('name', $code)
|
->where('name', $root['code'])
|
||||||
->first();
|
->exists();
|
||||||
|
|
||||||
if ($existing) {
|
if (! $existing) {
|
||||||
if ($force) {
|
LaborCategory::withoutGlobalScope(TenantScope::class)->create([
|
||||||
$existing->update(['name_ru' => $nameRu]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$existing = LaborCategory::withoutGlobalScope(TenantScope::class)->create([
|
|
||||||
'company_id' => $company->id,
|
'company_id' => $company->id,
|
||||||
'name' => $code,
|
'name' => $root['code'],
|
||||||
'name_ro' => $nameRu,
|
'name_ro' => $root['name'],
|
||||||
'name_ru' => $nameRu,
|
'name_ru' => $root['name'],
|
||||||
'sort_order' => $sortBase + ($leaf['sort_order'] ?? 0),
|
'sort_order' => $root['sort_order'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$catCodeToId[$leaf['id']] = $code;
|
|
||||||
$sortBase += 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. Labors ─────────────────────────────────────────────────────
|
// ── 2. Labors ─────────────────────────────────────────────────────
|
||||||
@@ -127,7 +123,7 @@ class ImportLaborCatalogCommand extends Command
|
|||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
|
|
||||||
foreach ($operations as $op) {
|
foreach ($operations as $op) {
|
||||||
$catKey = $catCodeToId[$op['service_category_id']] ?? null;
|
$catKey = $leafToRoot[$op['service_category_id']] ?? null;
|
||||||
if (! $catKey) { $skipped++; continue; }
|
if (! $catKey) { $skipped++; continue; }
|
||||||
|
|
||||||
$existing = Labor::withoutGlobalScope(TenantScope::class)
|
$existing = Labor::withoutGlobalScope(TenantScope::class)
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class LaborResource extends Resource
|
|||||||
Tables\Grouping\Group::make('category')
|
Tables\Grouping\Group::make('category')
|
||||||
->label(__('Categorie'))
|
->label(__('Categorie'))
|
||||||
->titlePrefixedWithLabel(false)
|
->titlePrefixedWithLabel(false)
|
||||||
|
->collapsible()
|
||||||
->getTitleFromRecordUsing(function ($record) {
|
->getTitleFromRecordUsing(function ($record) {
|
||||||
$opts = \App\Models\Tenant\LaborCategory::optionsForSelect();
|
$opts = \App\Models\Tenant\LaborCategory::optionsForSelect();
|
||||||
return $opts[$record->category] ?? $record->category;
|
return $opts[$record->category] ?? $record->category;
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/** Root categories from psauto-katalog-rabot-full.json (parent_id=null nodes). */
|
||||||
|
private const ROOTS = [
|
||||||
|
['code' => '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
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,9 +17,6 @@ elif [ "${RUN_MIGRATIONS:-true}" = "true" ]; then
|
|||||||
php artisan migrate --force --no-interaction || echo "[entrypoint] migrate failed (non-fatal)"
|
php artisan migrate --force --no-interaction || echo "[entrypoint] migrate failed (non-fatal)"
|
||||||
fi
|
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.
|
# Run seeders if requested. Uses firstOrCreate so it's idempotent.
|
||||||
if [ "${RUN_SEED:-false}" = "true" ] && [ "${RUN_FRESH_MIGRATE:-false}" != "true" ]; then
|
if [ "${RUN_SEED:-false}" = "true" ] && [ "${RUN_FRESH_MIGRATE:-false}" != "true" ]; then
|
||||||
echo "[entrypoint] Running database seed..."
|
echo "[entrypoint] Running database seed..."
|
||||||
|
|||||||
Reference in New Issue
Block a user