7ed60b1dbf
- 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>
166 lines
6.0 KiB
PHP
166 lines
6.0 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Central\Company;
|
|
use App\Models\Scopes\TenantScope;
|
|
use App\Models\Tenant\Labor;
|
|
use App\Models\Tenant\LaborCategory;
|
|
use App\Tenancy\TenantManager;
|
|
use Illuminate\Console\Command;
|
|
|
|
class ImportLaborCatalogCommand extends Command
|
|
{
|
|
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 (categories + operations) from JSON into labor_categories and labors tables';
|
|
|
|
public function handle(): int
|
|
{
|
|
$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);
|
|
if (! $data) {
|
|
$this->error('Failed to parse JSON file.');
|
|
return 1;
|
|
}
|
|
|
|
if ($this->option('all')) {
|
|
$companies = Company::all();
|
|
foreach ($companies as $company) {
|
|
$this->importForCompany($company, $data);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
$arg = $this->argument('company');
|
|
if (! $arg) {
|
|
$this->error('Provide a company slug/id, or use --all.');
|
|
return 1;
|
|
}
|
|
|
|
$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');
|
|
|
|
$allCats = $data['categories'] ?? [];
|
|
$operations = $data['operations'] ?? [];
|
|
$catMap = collect($allCats)->keyBy('id');
|
|
|
|
// 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}) ── root-cats:{$rootsNeeded->count()} ops:" . count($operations));
|
|
|
|
// ── 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', $root['code'])
|
|
->exists();
|
|
|
|
if (! $existing) {
|
|
LaborCategory::withoutGlobalScope(TenantScope::class)->create([
|
|
'company_id' => $company->id,
|
|
'name' => $root['code'],
|
|
'name_ro' => $root['name'],
|
|
'name_ru' => $root['name'],
|
|
'sort_order' => $root['sort_order'],
|
|
]);
|
|
}
|
|
}
|
|
|
|
// ── 2. Labors ─────────────────────────────────────────────────────
|
|
$created = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($operations as $op) {
|
|
$catKey = $leafToRoot[$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;
|
|
}
|
|
|
|
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->line(" Created: {$created} Skipped/existing: {$skipped}");
|
|
}
|
|
}
|