diff --git a/app/Http/Controllers/Settings/ProjectSettingController.php b/app/Http/Controllers/Settings/ProjectSettingController.php index 49792d6..f1e7698 100644 --- a/app/Http/Controllers/Settings/ProjectSettingController.php +++ b/app/Http/Controllers/Settings/ProjectSettingController.php @@ -7,7 +7,14 @@ use App\Models\Settings\Company; use App\Models\Settings\Department; use App\Models\Settings\Location; use App\Models\Settings\ProjectSetting; +use App\Services\ProjectImportService; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; +use PhpOffice\PhpSpreadsheet\Style\Fill; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class ProjectSettingController extends Controller { @@ -167,4 +174,100 @@ class ProjectSettingController extends Controller $department->delete(); return response()->json(['ok' => true]); } + + // ── Import ──────────────────────────────────────────────────────────────── + public function import(Request $request): JsonResponse + { + $request->validate(['file' => 'required|file|mimes:xlsx,xls|max:10240']); + + try { + $stats = app(ProjectImportService::class)->import( + $request->file('file')->getPathname() + ); + + $parts = []; + if ($stats['projects_created']) $parts[] = "{$stats['projects_created']} project(s)"; + if ($stats['departments_created']) $parts[] = "{$stats['departments_created']} department(s)"; + if ($stats['companies_created']) $parts[] = "{$stats['companies_created']} new company(s)"; + + $message = $parts + ? 'Imported: ' . implode(', ', $parts) . ($stats['skipped'] ? " — {$stats['skipped']} row(s) skipped" : '') + : 'Nothing new to import' . ($stats['skipped'] ? " ({$stats['skipped']} rows already exist)" : ''); + + return response()->json(['success' => true, 'message' => $message, 'stats' => $stats]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 422); + } + } + + public function downloadTemplate(): \Symfony\Component\HttpFoundation\BinaryFileResponse + { + $path = storage_path('app/projects_template.xlsx'); + $this->buildTemplate($path); + return response()->download($path, 'projects_template.xlsx'); + } + + private function buildTemplate(string $path): void + { + $spreadsheet = new Spreadsheet(); + + $headerStyle = [ + 'font' => ['bold' => true, 'color' => ['rgb' => '1e293b']], + 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'DBEAFE']], + 'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT], + ]; + $noteStyle = [ + 'font' => ['italic' => true, 'color' => ['rgb' => '64748b'], 'size' => 10], + ]; + + // ── Sheet 1: Projects ────────────────────────────────────────────── + $s1 = $spreadsheet->getActiveSheet()->setTitle('Projects'); + $s1->setCellValue('A1', 'Company Name') + ->setCellValue('B1', 'Project Name'); + $s1->getStyle('A1:B1')->applyFromArray($headerStyle); + + // Sample rows + $samples = [ + ['Miknas Industrial', 'New Warehouse'], + ['Steel tech', 'Factory Extension'], + ['Steel tech', 'New Office Block'], + ]; + foreach ($samples as $i => $row) { + $s1->setCellValue('A' . ($i + 2), $row[0]); + $s1->setCellValue('B' . ($i + 2), $row[1]); + } + + $s1->setCellValue('A6', '* Delete sample rows before importing. Company will be created if it does not exist.'); + $s1->getStyle('A6')->applyFromArray($noteStyle); + $s1->mergeCells('A6:B6'); + + $s1->getColumnDimension('A')->setWidth(32); + $s1->getColumnDimension('B')->setWidth(32); + + // ── Sheet 2: Departments ─────────────────────────────────────────── + $s2 = new Worksheet($spreadsheet, 'Departments'); + $spreadsheet->addSheet($s2); + $s2->setCellValue('A1', 'Company Name') + ->setCellValue('B1', 'Department Name'); + $s2->getStyle('A1:B1')->applyFromArray($headerStyle); + + $deptSamples = [ + ['Miknas Industrial', 'Finance'], + ['Miknas Industrial', 'Operations'], + ['Steel tech', 'Production'], + ]; + foreach ($deptSamples as $i => $row) { + $s2->setCellValue('A' . ($i + 2), $row[0]); + $s2->setCellValue('B' . ($i + 2), $row[1]); + } + + $s2->setCellValue('A6', '* Delete sample rows before importing. Company will be created if it does not exist.'); + $s2->getStyle('A6')->applyFromArray($noteStyle); + $s2->mergeCells('A6:B6'); + + $s2->getColumnDimension('A')->setWidth(32); + $s2->getColumnDimension('B')->setWidth(32); + + (new Xlsx($spreadsheet))->save($path); + } } diff --git a/app/Services/ProjectImportService.php b/app/Services/ProjectImportService.php new file mode 100644 index 0000000..7b774c8 --- /dev/null +++ b/app/Services/ProjectImportService.php @@ -0,0 +1,164 @@ + 0, + 'projects_created' => 0, + 'departments_created' => 0, + 'skipped' => 0, + ]; + + /** @var array */ + private array $companyCache = []; + + public function import(string $filePath): array + { + $spreadsheet = IOFactory::load($filePath); + + $projectSheet = $this->findSheet($spreadsheet, ['projects', 'project']); + $deptSheet = $this->findSheet($spreadsheet, ['departments', 'department', 'depts', 'dept']); + + if ($projectSheet) { + $this->importProjects($projectSheet); + } + if ($deptSheet) { + $this->importDepartments($deptSheet); + } + + // Single-sheet file with no named tabs → treat as projects + if (!$projectSheet && !$deptSheet) { + $this->importProjects($spreadsheet->getActiveSheet()); + } + + return $this->stats; + } + + private function findSheet($spreadsheet, array $names): ?\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + { + foreach ($spreadsheet->getSheetNames() as $i => $sheetName) { + if (in_array(strtolower(trim($sheetName)), $names)) { + return $spreadsheet->getSheet($i); + } + } + return null; + } + + private function importProjects(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet): void + { + $rows = $sheet->toArray(null, true, true, false); + $headers = $this->normalizeHeaders((array) array_shift($rows)); + + $coIdx = $this->findCol($headers, ['company', 'company name', 'company name', 'companyname']); + $projIdx = $this->findCol($headers, ['project', 'project name', 'project name', 'projectname']); + + if ($coIdx === null || $projIdx === null) { + return; + } + + foreach ($rows as $row) { + $coName = $this->str($row[$coIdx] ?? null); + $projName = $this->str($row[$projIdx] ?? null); + + if (!$coName || !$projName) { + $this->stats['skipped']++; + continue; + } + + $company = $this->findOrCreateCompany($coName); + $existing = ProjectSetting::whereRaw('LOWER(name) = ?', [strtolower($projName)]) + ->where('company_id', $company->id)->first(); + + if ($existing) { + $this->stats['skipped']++; + continue; + } + + ProjectSetting::create(['name' => $projName, 'company_id' => $company->id, 'is_active' => true]); + $this->stats['projects_created']++; + } + } + + private function importDepartments(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet): void + { + $rows = $sheet->toArray(null, true, true, false); + $headers = $this->normalizeHeaders((array) array_shift($rows)); + + $coIdx = $this->findCol($headers, ['company', 'company name', 'companyname']); + $deptIdx = $this->findCol($headers, ['department', 'department name', 'departmentname', 'dept', 'dept name']); + + if ($coIdx === null || $deptIdx === null) { + return; + } + + foreach ($rows as $row) { + $coName = $this->str($row[$coIdx] ?? null); + $deptName = $this->str($row[$deptIdx] ?? null); + + if (!$coName || !$deptName) { + $this->stats['skipped']++; + continue; + } + + $company = $this->findOrCreateCompany($coName); + $existing = Department::whereRaw('LOWER(name) = ?', [strtolower($deptName)]) + ->where('company_id', $company->id)->first(); + + if ($existing) { + $this->stats['skipped']++; + continue; + } + + Department::create(['name' => $deptName, 'company_id' => $company->id, 'is_active' => true]); + $this->stats['departments_created']++; + } + } + + private function findOrCreateCompany(string $name): Company + { + $key = strtolower($name); + if (isset($this->companyCache[$key])) { + return $this->companyCache[$key]; + } + + $company = Company::whereRaw('LOWER(name) = ?', [$key])->first(); + if (!$company) { + $company = Company::create(['name' => $name, 'is_active' => true]); + $this->stats['companies_created']++; + } + + $this->companyCache[$key] = $company; + return $company; + } + + private function normalizeHeaders(array $row): array + { + return array_map( + fn ($h) => strtolower(trim(str_replace(['*', '_', '-'], ' ', (string) ($h ?? '')))), + $row + ); + } + + private function findCol(array $headers, array $aliases): ?int + { + foreach ($headers as $i => $h) { + if (in_array($h, $aliases)) { + return $i; + } + } + return null; + } + + private function str(mixed $value): ?string + { + $v = trim((string) ($value ?? '')); + return $v === '' ? null : $v; + } +} diff --git a/resources/views/settings/projects/index.blade.php b/resources/views/settings/projects/index.blade.php index 02619f1..f218a1f 100644 --- a/resources/views/settings/projects/index.blade.php +++ b/resources/views/settings/projects/index.blade.php @@ -36,6 +36,16 @@

Projects, Locations & Departments

Manage projects with their sub-locations and departments.

+
+ + + Template + + +
{{-- Stat boxes --}} @@ -326,6 +336,68 @@ $allDeptsJson = json_encode($allDeptsData); @endforelse {{-- end companies-list --}} +{{-- ═══════════════ Import Modal ═══════════════ --}} + + + {{-- ═══════════════ Location Map Modal ═══════════════ --}}
@@ -416,10 +488,101 @@ $allDeptsJson = json_encode($allDeptsData);