user(); $query = Project::query() ->where('current_wizard_step', '>=', 7) ->whereNotIn('status', ['completed', 'closed']) ->with(['personnel:id,name']); $historyQuery = Project::query() ->where('current_wizard_step', '>=', 7) ->whereIn('status', ['completed', 'closed']) ->with(['personnel:id,name']); $draftsQuery = Project::query() ->where('current_wizard_step', '<', 7) ->with(['personnel:id,name']); // Non-platform admins only see projects they are assigned to if ($user && !$user->hasRole('Super Admin')) { $query->whereHas('personnel', function ($q) use ($user) { $q->where('users.id', $user->id); }); $historyQuery->whereHas('personnel', function ($q) use ($user) { $q->where('users.id', $user->id); }); $draftsQuery->whereHas('personnel', function ($q) use ($user) { $q->where('users.id', $user->id); }); } $projects = $query ->when($request->search, fn ($q, $s) => $q->where('name', 'like', "%{$s}%")->orWhere('code', 'like', "%{$s}%")) ->when($request->status, fn ($q, $s) => $q->where('status', $s)) ->latest() ->paginate(15, ['*'], 'page') ->withQueryString() ->through(fn ($p) => $p->append(['capitalization_percentage', 'is_over_budget'])); $history = $historyQuery ->when($request->search, fn ($q, $s) => $q->where('name', 'like', "%{$s}%")->orWhere('code', 'like', "%{$s}%")) ->when($request->status, fn ($q, $s) => $q->where('status', $s)) ->latest() ->paginate(15, ['*'], 'history_page') ->withQueryString() ->through(fn ($p) => $p->append(['capitalization_percentage', 'is_over_budget'])); $drafts = $draftsQuery->latest()->get(); return Inertia::render('ProjectManagement::Projects/Index', [ 'projects' => $projects, 'history' => $history, 'drafts' => $drafts, 'filters' => $request->only(['search', 'status']), 'statuses' => collect(ProjectStatus::cases())->map(fn ($s) => [ 'value' => $s->value, 'label' => $s->label(), ]), ]); } public function create() { $employees = User::whereIn('user_type', ['admin', 'employee'])->with('employeeProfile')->select('id', 'ulid', 'name', 'email')->get(); $projects = Project::active()->with('parentProject:id,ulid,name,code')->get(['id', 'ulid', 'name', 'code', 'parent_project_id', 'project_type']); return Inertia::render('ProjectManagement::Projects/Create', [ 'employees' => $employees, 'projects' => $projects, ]); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'client_name' => 'nullable|string|max:255', 'description' => 'nullable|string', 'location' => 'nullable|string|max:255', 'contract_value' => 'nullable|numeric|min:0', 'contract_duration' => 'nullable|integer|min:0', 'start_date' => 'nullable|date', 'target_end_date' => 'nullable|date|after_or_equal:start_date', 'project_type' => 'required|string|in:standard,special,extension', 'parent_project_id' => 'nullable|string', 'is_unprofitable' => 'nullable|boolean', ]); if (!empty($validated['parent_project_id'])) { $validated['parent_project_id'] = Project::resolveUlidToId($validated['parent_project_id']); } $validated['is_unprofitable'] = $validated['is_unprofitable'] ?? false; $validated['current_wizard_step'] = 2; $project = Project::create($validated); // Assign PM if provided if ($request->pm_id) { $pmId = User::resolveUlidToId($request->pm_id); if ($pmId) { $project->personnel()->attach($pmId, ['role' => 'pm']); } } return redirect()->route('projects.wizard', [$project, 'step' => 2]) ->with('success', "Project \"{$project->name}\" details saved. Please add tasks."); } public function show(Project $project) { $project->load([ 'contractor:id,company_name', 'tasks' => fn ($q) => $q->with(['taskLabors.labor', 'taskEquipments.equipment'])->orderBy('sort_order'), 'parentProject:id,ulid,name,code', 'extensionProjects:id,parent_project_id,ulid,name,code,status,contract_value,total_capitalization,start_date,target_end_date', 'milestones' => fn ($q) => $q->orderBy('sort_order'), 'materialsEstimates.material', ]); $project->append([ 'capitalization_percentage', 'is_over_budget', 'rollup_capitalization', 'rollup_estimated_value', 'rollup_contract_value', ]); $taskStats = [ 'total' => $project->tasks->count(), 'pending' => $project->tasks->where('status', 'pending')->count(), 'in_progress' => $project->tasks->where('status', 'in_progress')->count(), 'completed' => $project->tasks->where('status', 'completed')->count(), 'closed' => $project->tasks->where('status', 'closed')->count(), ]; $estimationTotals = $this->getWizardTotals($project); return Inertia::render('ProjectManagement::Projects/Overview', [ 'project' => $project, 'taskStats' => $taskStats, 'allowedTransitions' => collect($project->status->allowedTransitions())->map(fn ($s) => [ 'value' => $s->value, 'label' => $s->label(), ]), 'estimationTotals' => $estimationTotals, 'tab' => request()->query('tab', 'overview'), ]); } public function team(Project $project) { $project->load(['personnel:id,name,email']); $employees = User::whereIn('user_type', ['admin', 'employee']) ->with('employeeProfile') ->select('id', 'ulid', 'name', 'email') ->get(); return Inertia::render('ProjectManagement::Projects/Team', [ 'project' => $project, 'employees' => $employees, 'allowedTransitions' => collect($project->status->allowedTransitions())->map(fn ($s) => [ 'value' => $s->value, 'label' => $s->label(), ]), ]); } public function materials(Project $project) { // Materials available on-site (dispatched & received at project) $availableMaterials = ProjectInventory::where('project_id', $project->id) ->where('on_hand_qty', '>', 0) ->with([ 'material:id,ulid,name,sku,unit,unit_cost,category,type', 'material.components.component:id,name,unit' ]) ->get() ->map(fn ($inv) => [ 'id' => $inv->material->id, 'ulid' => $inv->material->ulid, 'name' => $inv->material->name, 'sku' => $inv->material->sku, 'unit' => $inv->material->unit, 'unit_cost' => $inv->material->unit_cost, 'category' => $inv->material->category, 'type' => $inv->material->type, 'components' => $inv->material->components, 'available_qty' => $inv->available_qty, 'on_hand_qty' => (float) $inv->on_hand_qty, 'allocated_qty' => (float) $inv->allocated_qty, ]); return Inertia::render('ProjectManagement::Projects/Materials', [ 'project' => $project, 'availableMaterials' => $availableMaterials, 'allowedTransitions' => collect($project->status->allowedTransitions())->map(fn ($s) => [ 'value' => $s->value, 'label' => $s->label(), ]), ]); } public function edit(Project $project) { $project->load([ 'personnel:id,name', 'parentProject:id,ulid', 'milestones' => fn ($q) => $q->orderBy('sort_order'), 'tasks' => fn ($q) => $q->with(['taskLabors.labor.skills', 'taskEquipments', 'taskIssues'])->orderBy('sort_order'), 'materialsEstimates.material', ]); $employees = User::whereIn('user_type', ['admin', 'employee'])->with('employeeProfile')->select('id', 'ulid', 'name', 'email')->get(); $projects = Project::active()->with('parentProject:id,ulid,name,code')->where('id', '!=', $project->id)->get(['id', 'ulid', 'name', 'code', 'parent_project_id', 'project_type']); $materials = \Modules\MasterData\Models\Material::where('type', 'single') ->where('status', 'active') ->select('id', 'ulid', 'name', 'unit', 'unit_cost') ->get(); $materialGroups = \Modules\MasterData\Models\Material::with('components.component:id,ulid,name,sku,category,unit,unit_cost') ->whereIn('type', ['kit', 'assembly']) ->where('status', 'active') ->orderBy('name') ->get() ->map(function ($kit) { return [ 'id' => $kit->id, 'ulid' => $kit->ulid, 'name' => $kit->name, 'description' => $kit->description ?? '', 'status' => $kit->status, 'materials' => $kit->components->map(function ($comp) { return array_merge($comp->component->toArray(), [ 'pivot' => ['quantity' => $comp->quantity] ]); })->toArray() ]; }); $labors = \Modules\Labors\Models\Labor::with('skills')->active()->get(); $equipments = \Modules\Equipments\Models\Equipment::with('specifications')->active()->get(); return Inertia::render('ProjectManagement::Projects/Edit', [ 'project' => $project, 'employees' => $employees, 'projects' => $projects, 'materials' => $materials, 'materialGroups' => $materialGroups, 'labors' => $labors, 'equipments' => $equipments, ]); } public function update(Request $request, Project $project) { if ($project->current_wizard_step >= 8) { return redirect()->back()->with('error', 'Cannot update details: This project has been approved and locked.'); } $validated = $request->validate([ 'name' => 'required|string|max:255', 'client_name' => 'nullable|string|max:255', 'description' => 'nullable|string', 'location' => 'nullable|string|max:255', 'contract_value' => 'nullable|numeric|min:0', 'contract_duration' => 'nullable|integer|min:0', 'start_date' => 'nullable|date', 'target_end_date' => 'nullable|date|after_or_equal:start_date', 'project_type' => 'required|string|in:standard,special,extension', 'parent_project_id' => 'nullable|string', 'is_unprofitable' => 'nullable|boolean', ]); if (!empty($validated['parent_project_id'])) { $validated['parent_project_id'] = Project::resolveUlidToId($validated['parent_project_id']); } else { $validated['parent_project_id'] = null; } $validated['is_unprofitable'] = $validated['is_unprofitable'] ?? false; $project->update($validated); return redirect()->route('projects.show', $project) ->with('success', "Project \"{$project->name}\" updated."); } public function transition(Request $request, Project $project) { $request->validate(['status' => 'required|string']); $newStatus = ProjectStatus::from($request->status); $oldStatus = $project->status->value; try { $project->transitionTo($newStatus); ProjectStatusChanged::dispatch($project, $oldStatus, $newStatus->value); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', "Project status changed to {$newStatus->label()}."); } public function addPersonnel(Request $request, Project $project) { $request->validate([ 'user_id' => 'required|string', 'role' => 'required|in:pm,engineer,laborer,member', ]); $userId = User::resolveUlidToId($request->user_id); if (!$userId) { return back()->with('error', 'User not found.'); } if ($project->personnel()->where('user_id', $userId)->exists()) { return back()->with('error', 'User is already assigned to this project.'); } $project->personnel()->attach($userId, ['role' => $request->role]); return back()->with('success', 'Team member added.'); } public function removePersonnel(Project $project, User $user) { $project->personnel()->detach($user->id); return back()->with('success', 'Team member removed.'); } private function seedDefaultMilestones(Project $project): void { $defaults = [ ['name' => 'Mobilization', 'weight_percentage' => 5], ['name' => 'Earthworks & Foundation', 'weight_percentage' => 15], ['name' => 'Structural Works', 'weight_percentage' => 25], ['name' => 'Roofing & Waterproofing', 'weight_percentage' => 15], ['name' => 'Architectural Finishing', 'weight_percentage' => 20], ['name' => 'MEP Rough-In', 'weight_percentage' => 10], ['name' => 'Final Inspection & Punch List', 'weight_percentage' => 5], ['name' => 'Turnover', 'weight_percentage' => 5], ]; $startDate = $project->start_date; $duration = $project->contract_duration; // in calendar days $count = count($defaults); foreach ($defaults as $i => $milestone) { $plannedDate = null; if ($startDate && $duration && $duration > 0) { $daysOffset = (int) round(($i + 1) / $count * $duration); $plannedDate = $startDate->copy()->addDays($daysOffset); } $project->milestones()->create([ 'name' => $milestone['name'], 'weight_percentage' => $milestone['weight_percentage'], 'sort_order' => $i, 'is_default' => true, 'planned_date' => $plannedDate, ]); } } public function wizard(Project $project, Request $request) { $step = (int) $request->query('step', $project->current_wizard_step); if ($step > $project->current_wizard_step && $project->status !== ProjectStatus::InProgress) { $step = $project->current_wizard_step; } $project->load([ 'milestones' => fn ($q) => $q->orderBy('sort_order'), 'tasks' => fn ($q) => $q->with(['taskLabors.labor.skills', 'taskEquipments', 'taskIssues'])->orderBy('sort_order'), 'materialsEstimates.material', ]); $employees = User::whereIn('user_type', ['admin', 'employee'])->with('employeeProfile')->select('id', 'ulid', 'name', 'email')->get(); $parentProjects = Project::active()->with('parentProject:id,ulid,name,code')->where('id', '!=', $project->id)->get(['id', 'ulid', 'name', 'code', 'parent_project_id', 'project_type']); $materials = \Modules\MasterData\Models\Material::where('type', 'single') ->where('status', 'active') ->select('id', 'ulid', 'name', 'unit', 'unit_cost') ->get(); $materialGroups = \Modules\MasterData\Models\Material::with('components.component:id,ulid,name,sku,category,unit,unit_cost') ->whereIn('type', ['kit', 'assembly']) ->where('status', 'active') ->orderBy('name') ->get() ->map(function ($kit) { return [ 'id' => $kit->id, 'ulid' => $kit->ulid, 'name' => $kit->name, 'description' => $kit->description ?? '', 'status' => $kit->status, 'materials' => $kit->components->map(function ($comp) { return array_merge($comp->component->toArray(), [ 'pivot' => ['quantity' => $comp->quantity] ]); })->toArray() ]; }); $labors = \Modules\Labors\Models\Labor::with('skills')->active()->get(); $equipments = \Modules\Equipments\Models\Equipment::with('specifications')->active()->get(); $workflowService = app(\Modules\ProjectManagement\Services\ProjectWorkflowService::class); $budget = $workflowService->getBudgetAnalysis($project); return Inertia::render('ProjectManagement::Projects/Wizard', [ 'project' => $project, 'step' => $step, 'employees' => $employees, 'projects' => $parentProjects, 'materials' => $materials, 'materialGroups' => $materialGroups, 'labors' => $labors, 'equipments' => $equipments, 'budget' => $budget, ]); } public function saveWizardTasks(Project $project, Request $request) { if ($project->current_wizard_step >= 8) { return redirect()->back()->with('error', 'Cannot modify tasks: This project has been approved and locked.'); } $validated = $request->validate([ 'prepopulate_milestones' => 'nullable|boolean', 'milestones' => 'nullable|array', 'milestones.*.ulid' => 'nullable|string', 'milestones.*.name' => 'required|string|max:255', 'milestones.*.weight_percentage' => 'required|numeric|min:0|max:100', 'tasks' => 'nullable|array', 'tasks.*.ulid' => 'nullable|string', 'tasks.*.name' => 'required|string|max:255', 'tasks.*.description' => 'nullable|string', 'tasks.*.milestone_ulid' => 'nullable|string', 'tasks.*.start_date' => 'nullable|date', 'tasks.*.end_date' => 'nullable|date|after_or_equal:tasks.*.start_date', ]); \DB::transaction(function () use ($project, $validated) { if (!empty($validated['prepopulate_milestones']) && !$project->milestones()->exists()) { $this->seedDefaultMilestones($project); } if (isset($validated['milestones'])) { $existingMilestoneIds = []; foreach ($validated['milestones'] as $index => $m) { $milestone = null; if (!empty($m['ulid'])) { $milestone = $project->milestones()->where('ulid', $m['ulid'])->first(); } if (!$milestone) { $milestone = $project->milestones()->create([ 'name' => $m['name'], 'weight_percentage' => $m['weight_percentage'], 'sort_order' => $index, ]); } else { $milestone->update([ 'name' => $m['name'], 'weight_percentage' => $m['weight_percentage'], 'sort_order' => $index, ]); } $existingMilestoneIds[] = $milestone->id; } $project->milestones()->whereNotIn('id', $existingMilestoneIds)->delete(); } if (isset($validated['tasks'])) { $existingTaskIds = []; foreach ($validated['tasks'] as $index => $t) { $milestoneId = null; if (!empty($t['milestone_ulid'])) { $milestoneId = \Modules\ProjectManagement\Models\ProjectMilestone::resolveUlidToId($t['milestone_ulid']); } $task = null; if (!empty($t['ulid'])) { $task = $project->tasks()->where('ulid', $t['ulid'])->first(); } $taskData = [ 'project_id' => $project->id, 'name' => $t['name'], 'description' => $t['description'] ?? null, 'milestone_id' => $milestoneId, 'start_date' => $t['start_date'] ?? null, 'end_date' => $t['end_date'] ?? null, 'sort_order' => $index, ]; if (!$task) { $task = \Modules\ProjectManagement\Models\Task::create($taskData); } else { $task->update($taskData); } $existingTaskIds[] = $task->id; } $project->tasks()->whereNotIn('id', $existingTaskIds)->delete(); } if ($project->current_wizard_step < 3) { $project->update(['current_wizard_step' => 3]); } }); return redirect()->route('projects.wizard', [$project, 'step' => 3]) ->with('success', 'Tasks saved successfully.'); } public function saveWizardEstimates(Project $project, Request $request) { if ($project->current_wizard_step >= 8) { return redirect()->back()->with('error', 'Cannot modify estimates: This project has been approved and locked.'); } $validated = $request->validate([ 'estimates' => 'nullable|array', 'estimates.*.material_ulid' => 'required|string', 'estimates.*.estimated_qty' => 'required|numeric|min:0', 'estimates.*.unit_cost' => 'required|numeric|min:0', ]); \DB::transaction(function () use ($project, $validated) { $existingEstimateIds = []; if (isset($validated['estimates'])) { foreach ($validated['estimates'] as $est) { $material = \Modules\MasterData\Models\Material::where('ulid', $est['material_ulid'])->firstOrFail(); $estimate = $project->materialsEstimates()->updateOrCreate( ['material_id' => $material->id], [ 'estimated_qty' => $est['estimated_qty'], 'unit_cost' => $est['unit_cost'], ] ); $existingEstimateIds[] = $estimate->id; } } $project->materialsEstimates()->whereNotIn('id', $existingEstimateIds)->delete(); if ($project->current_wizard_step < 4) { $project->update(['current_wizard_step' => 4]); } }); return redirect()->route('projects.wizard', [$project, 'step' => 4]) ->with('success', 'Materials estimates saved.'); } public function saveWizardLabor(Project $project, Request $request) { if ($project->current_wizard_step >= 8) { return redirect()->back()->with('error', 'Cannot modify labor allocations: This project has been approved and locked.'); } $validated = $request->validate([ 'labor' => 'nullable|array', 'labor.*.task_ulid' => 'required|string', 'labor.*.labor_ulid' => 'required|string', 'labor.*.estimated_hours' => 'required|numeric|min:0', ]); \DB::transaction(function () use ($project, $validated) { $existingLaborIds = []; if (isset($validated['labor'])) { foreach ($validated['labor'] as $lb) { $task = $project->tasks()->where('ulid', $lb['task_ulid'])->firstOrFail(); $laborRecord = \Modules\Labors\Models\Labor::where('ulid', $lb['labor_ulid'])->firstOrFail(); $taskLabor = $task->taskLabors()->updateOrCreate( ['labor_id' => $laborRecord->id], ['estimated_hours' => $lb['estimated_hours']] ); $existingLaborIds[] = $taskLabor->id; } } $taskIds = $project->tasks()->pluck('id'); \Modules\ProjectManagement\Models\TaskLabor::whereIn('task_id', $taskIds) ->whereNotIn('id', $existingLaborIds) ->delete(); if ($project->current_wizard_step < 5) { $project->update(['current_wizard_step' => 5]); } }); return redirect()->route('projects.wizard', [$project, 'step' => 5]) ->with('success', 'Manpower allocations saved.'); } public function saveWizardEquipment(Project $project, Request $request) { if ($project->current_wizard_step >= 8) { return redirect()->back()->with('error', 'Cannot modify equipment allocations: This project has been approved and locked.'); } $validated = $request->validate([ 'equipment' => 'nullable|array', 'equipment.*.task_ulid' => 'required|string', 'equipment.*.equipment_ulid' => 'required|string', 'equipment.*.estimated_hours' => 'required|numeric|min:0', ]); \DB::transaction(function () use ($project, $validated) { $existingEquipmentIds = []; if (isset($validated['equipment'])) { foreach ($validated['equipment'] as $eq) { $task = $project->tasks()->where('ulid', $eq['task_ulid'])->firstOrFail(); $equipmentRecord = \Modules\Equipments\Models\Equipment::where('ulid', $eq['equipment_ulid'])->firstOrFail(); $taskEquipment = $task->taskEquipments()->updateOrCreate( ['equipment_id' => $equipmentRecord->id], ['estimated_hours' => $eq['estimated_hours']] ); $existingEquipmentIds[] = $taskEquipment->id; } } $taskIds = $project->tasks()->pluck('id'); \Modules\ProjectManagement\Models\TaskEquipment::whereIn('task_id', $taskIds) ->whereNotIn('id', $existingEquipmentIds) ->delete(); if ($project->current_wizard_step < 6) { $project->update(['current_wizard_step' => 6]); } }); return redirect()->route('projects.wizard', [$project, 'step' => 6]) ->with('success', 'Equipment allocations saved.'); } public function submitWizardForApproval(Project $project, Request $request) { $validated = $request->validate([ 'approver_ids' => 'required|array', 'approver_ids.*' => 'string', 'notes' => 'nullable|string', ]); \DB::transaction(function () use ($project, $validated) { $approverIds = array_map(function ($ulid) { return User::resolveUlidToId($ulid); }, $validated['approver_ids']); $approvalService = app(\Modules\ApprovalWorkflow\Services\ApprovalService::class); $approvalService->createChain( $project, $approverIds, 'project_estimation', auth()->id(), $validated['notes'] ?? 'Project Setup and Estimation Approval Request' ); $project->update([ 'current_wizard_step' => 7, ]); }); return redirect()->route('projects.show', $project) ->with('success', 'Project estimation has been successfully submitted for approval.'); } private function getWizardTotals(Project $project): array { $materialsCost = $project->materialsEstimates->reduce(function ($sum, $est) { return $sum + ($est->estimated_qty * $est->unit_cost); }, 0); $laborCost = 0; foreach ($project->tasks as $task) { foreach ($task->taskLabors as $tl) { $laborCost += ($tl->estimated_hours * $tl->labor->hourly_rate); } } $equipmentCost = 0; foreach ($project->tasks as $task) { foreach ($task->taskEquipments as $te) { $equipmentCost += ($te->estimated_hours * $te->equipment->hourly_rate); } } $totalEstimatedCost = $materialsCost + $laborCost + $equipmentCost; $contractValue = (float) $project->contract_value; $margin = $contractValue - $totalEstimatedCost; $marginPct = $contractValue > 0 ? ($margin / $contractValue) * 100 : 0; return [ 'materials' => (float) $materialsCost, 'labor' => (float) $laborCost, 'equipment' => (float) $equipmentCost, 'sum' => (float) $totalEstimatedCost, 'margin' => (float) $margin, 'margin_pct' => (float) $marginPct, ]; } public function exportWizardPdf(Project $project) { $project->load([ 'materialsEstimates.material', 'tasks.taskLabors.labor', 'tasks.taskEquipments.equipment', ]); $totals = $this->getWizardTotals($project); $pdf = Pdf::loadView('projectmanagement::pdf.estimation', [ 'project' => $project, 'totals' => $totals, ]); return $pdf->download("ProjectEstimation_{$project->code}.pdf"); } public function exportWizardExcel(Project $project) { $project->load([ 'materialsEstimates.material', 'tasks.taskLabors.labor', 'tasks.taskEquipments.equipment', ]); $totals = $this->getWizardTotals($project); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Estimation Report'); $sheet->setShowGridlines(true); // Styling helpers $titleStyle = [ 'font' => [ 'name' => 'Arial', 'size' => 16, 'bold' => true, 'color' => ['rgb' => 'FFFFFF'], ], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '334155'], // Slate 700 ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], ]; $metaHeaderStyle = [ 'font' => [ 'name' => 'Arial', 'size' => 11, 'bold' => true, 'color' => ['rgb' => '334155'], ], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F1F5F9'], // Slate 100 ], 'borders' => [ 'bottom' => [ 'borderStyle' => Border::BORDER_MEDIUM, 'color' => ['rgb' => 'CBD5E1'], ], ], ]; $sectionHeaderStyle = [ 'font' => [ 'name' => 'Arial', 'size' => 12, 'bold' => true, 'color' => ['rgb' => 'FFFFFF'], ], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '475569'], // Slate 600 ], 'alignment' => [ 'vertical' => Alignment::VERTICAL_CENTER, ], ]; $boldLabelStyle = [ 'font' => [ 'name' => 'Arial', 'size' => 10, 'bold' => true, 'color' => ['rgb' => '1E293B'], ], ]; $tableHeaderStyle = [ 'font' => [ 'name' => 'Arial', 'size' => 10, 'bold' => true, 'color' => ['rgb' => '475569'], ], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F8FAFC'], ], 'borders' => [ 'bottom' => [ 'borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'E2E8F0'], ], ], ]; $thinBorderBottom = [ 'borders' => [ 'bottom' => [ 'borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'F1F5F9'], ], ], ]; $totalRowStyle = [ 'font' => [ 'bold' => true, ], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F8FAFC'], ], 'borders' => [ 'top' => [ 'borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'CBD5E1'], ], 'bottom' => [ 'borderStyle' => Border::BORDER_DOUBLE, 'color' => ['rgb' => '1E293B'], ], ], ]; // 1. Branding Header $sheet->mergeCells('A1:G1'); $sheet->setCellValue('A1', 'GREAT SWISS BUILDER CONSTRUCTION'); $sheet->getStyle('A1:G1')->applyFromArray($titleStyle); $sheet->getRowDimension(1)->setRowHeight(40); // 2. Metadata Block $sheet->mergeCells('A3:G3'); $sheet->setCellValue('A3', ' ESTIMATION PROJECT DETAILS'); $sheet->getStyle('A3:G3')->applyFromArray($metaHeaderStyle); $sheet->getRowDimension(3)->setRowHeight(25); // Row 4 $sheet->setCellValue('A4', 'Project Code:'); $sheet->setCellValue('B4', $project->code); $sheet->setCellValue('D4', 'Project Type:'); $sheet->setCellValue('E4', ucfirst($project->project_type)); $sheet->setCellValue('F4', 'Date Generated:'); $sheet->setCellValue('G4', now()->format('Y-m-d')); // Row 5 $sheet->setCellValue('A5', 'Project Name:'); $sheet->mergeCells('B5:C5'); $sheet->setCellValue('B5', $project->name); $sheet->setCellValue('D5', 'Client Name:'); $sheet->mergeCells('E5:G5'); $sheet->setCellValue('E5', $project->client_name ?: 'N/A'); // Row 6 $sheet->setCellValue('A6', 'Location:'); $sheet->mergeCells('B6:C6'); $sheet->setCellValue('B6', $project->location ?: 'N/A'); $sheet->setCellValue('D6', 'Contract Value:'); $sheet->setCellValue('E6', $project->contract_value); $sheet->getStyle('E6')->getNumberFormat()->setFormatCode('"₱"#,##0.00'); foreach (['A4', 'D4', 'F4', 'A5', 'D5', 'A6', 'D6'] as $cell) { $sheet->getStyle($cell)->applyFromArray($boldLabelStyle); } $rowIdx = 8; // 3. Materials Cost Center $sheet->mergeCells("A{$rowIdx}:G{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", ' 1. MATERIALS COST'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($sectionHeaderStyle); $sheet->getRowDimension($rowIdx)->setRowHeight(25); $rowIdx++; // Table Headers $sheet->setCellValue("A{$rowIdx}", '#'); $sheet->mergeCells("B{$rowIdx}:C{$rowIdx}"); $sheet->setCellValue("B{$rowIdx}", 'Material Name'); $sheet->setCellValue("D{$rowIdx}", 'Unit'); $sheet->setCellValue("E{$rowIdx}", 'Est. Qty'); $sheet->setCellValue("F{$rowIdx}", 'Unit Cost'); $sheet->setCellValue("G{$rowIdx}", 'Total Cost'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($tableHeaderStyle); $rowIdx++; $matIndex = 1; foreach ($project->materialsEstimates as $est) { $sheet->setCellValue("A{$rowIdx}", $matIndex++); $sheet->mergeCells("B{$rowIdx}:C{$rowIdx}"); $sheet->setCellValue("B{$rowIdx}", $est->material->name); $sheet->setCellValue("D{$rowIdx}", $est->material->unit); $sheet->setCellValue("E{$rowIdx}", $est->estimated_qty); $sheet->getStyle("E{$rowIdx}")->getNumberFormat()->setFormatCode('#,##0.00'); $sheet->setCellValue("F{$rowIdx}", $est->unit_cost); $sheet->getStyle("F{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); // Formula for total cost: E * F $sheet->setCellValue("G{$rowIdx}", "=E{$rowIdx}*F{$rowIdx}"); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($thinBorderBottom); $rowIdx++; } // Subtotal row for Materials $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Subtotal Materials Cost Center'); $sheet->setCellValue("G{$rowIdx}", $totals['materials']); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($totalRowStyle); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); $rowIdx += 2; // 4. Labor Cost Center $sheet->mergeCells("A{$rowIdx}:G{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", ' 2. LABOR MANPOWER COST'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($sectionHeaderStyle); $sheet->getRowDimension($rowIdx)->setRowHeight(25); $rowIdx++; // Table Headers $sheet->setCellValue("A{$rowIdx}", '#'); $sheet->setCellValue("B{$rowIdx}", 'Target Task'); $sheet->setCellValue("C{$rowIdx}", 'Labor Record'); $sheet->setCellValue("D{$rowIdx}", 'Category'); $sheet->setCellValue("E{$rowIdx}", 'Hourly Rate'); $sheet->setCellValue("F{$rowIdx}", 'Est. Hours'); $sheet->setCellValue("G{$rowIdx}", 'Total Cost'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($tableHeaderStyle); $rowIdx++; $labIndex = 1; foreach ($project->tasks as $task) { foreach ($task->taskLabors as $tl) { $sheet->setCellValue("A{$rowIdx}", $labIndex++); $sheet->setCellValue("B{$rowIdx}", $task->name); $sheet->setCellValue("C{$rowIdx}", $tl->labor->name); $sheet->setCellValue("D{$rowIdx}", ucfirst($tl->labor->category)); $sheet->setCellValue("E{$rowIdx}", $tl->labor->hourly_rate); $sheet->getStyle("E{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->setCellValue("F{$rowIdx}", $tl->estimated_hours); $sheet->getStyle("F{$rowIdx}")->getNumberFormat()->setFormatCode('#,##0.0'); // Formula: E * F $sheet->setCellValue("G{$rowIdx}", "=E{$rowIdx}*F{$rowIdx}"); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($thinBorderBottom); $rowIdx++; } } // Subtotal row for Labor $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Subtotal Labor Manpower Cost Center'); $sheet->setCellValue("G{$rowIdx}", $totals['labor']); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($totalRowStyle); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); $rowIdx += 2; // 5. Equipment Cost Center $sheet->mergeCells("A{$rowIdx}:G{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", ' 3. EQUIPMENT MACHINERY COST'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($sectionHeaderStyle); $sheet->getRowDimension($rowIdx)->setRowHeight(25); $rowIdx++; // Table Headers $sheet->setCellValue("A{$rowIdx}", '#'); $sheet->setCellValue("B{$rowIdx}", 'Target Task'); $sheet->setCellValue("C{$rowIdx}", 'Equipment / Tool'); $sheet->setCellValue("D{$rowIdx}", 'Owner'); $sheet->setCellValue("E{$rowIdx}", 'Hourly Rate'); $sheet->setCellValue("F{$rowIdx}", 'Est. Hours'); $sheet->setCellValue("G{$rowIdx}", 'Total Cost'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($tableHeaderStyle); $rowIdx++; $eqIndex = 1; foreach ($project->tasks as $task) { foreach ($task->taskEquipments as $te) { $sheet->setCellValue("A{$rowIdx}", $eqIndex++); $sheet->setCellValue("B{$rowIdx}", $task->name); $sheet->setCellValue("C{$rowIdx}", $te->equipment->name); $sheet->setCellValue("D{$rowIdx}", $te->equipment->owner_name ?: 'Unspecified'); $sheet->setCellValue("E{$rowIdx}", $te->equipment->hourly_rate); $sheet->getStyle("E{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->setCellValue("F{$rowIdx}", $te->estimated_hours); $sheet->getStyle("F{$rowIdx}")->getNumberFormat()->setFormatCode('#,##0.0'); // Formula: E * F $sheet->setCellValue("G{$rowIdx}", "=E{$rowIdx}*F{$rowIdx}"); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($thinBorderBottom); $rowIdx++; } } // Subtotal row for Equipment $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Subtotal Equipment Machinery Cost Center'); $sheet->setCellValue("G{$rowIdx}", $totals['equipment']); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($totalRowStyle); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); $rowIdx += 2; // 6. Summary Block $sheet->mergeCells("A{$rowIdx}:G{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", ' 4. FINANCIAL SUMMARY'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($sectionHeaderStyle); $sheet->getRowDimension($rowIdx)->setRowHeight(25); $rowIdx++; $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Total Summed Budget Estimation (Materials + Labor + Equipment):'); $sheet->setCellValue("G{$rowIdx}", $totals['sum']); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($thinBorderBottom); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); $rowIdx++; $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Registered Project Contract Value:'); $sheet->setCellValue("G{$rowIdx}", $project->contract_value); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($thinBorderBottom); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); $rowIdx++; $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Projected Gross Margin:'); $sheet->setCellValue("G{$rowIdx}", $totals['margin']); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('"₱"#,##0.00'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($thinBorderBottom); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); $rowIdx++; $sheet->mergeCells("A{$rowIdx}:F{$rowIdx}"); $sheet->setCellValue("A{$rowIdx}", 'Projected Profit Margin Percentage:'); $sheet->setCellValue("G{$rowIdx}", $totals['margin_pct'] / 100); $sheet->getStyle("G{$rowIdx}")->getNumberFormat()->setFormatCode('0.00%'); $sheet->getStyle("A{$rowIdx}:G{$rowIdx}")->applyFromArray($totalRowStyle); $sheet->getStyle("A{$rowIdx}")->applyFromArray($boldLabelStyle); // Auto-fit column widths foreach (range('A', 'G') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } // Export response headers $filename = "EstimationReport_{$project->code}_" . now()->format('Ymd') . ".xlsx"; header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment;filename="' . $filename . '"'); header('Cache-Control: max-age=0'); $writer = new Xlsx($spreadsheet); $writer->save('php://output'); exit; } }