Additional Changes
This commit is contained in:
@@ -13,6 +13,16 @@ use Modules\ProjectManagement\Enums\ProjectStatus;
|
||||
use Modules\ProjectManagement\Enums\WeatherCondition;
|
||||
use Modules\ProjectManagement\Events\ProjectStatusChanged;
|
||||
use Modules\ProjectManagement\Models\Project;
|
||||
use Modules\ProjectManagement\Models\ProjectMilestone;
|
||||
use Modules\ProjectManagement\Models\Task;
|
||||
use Modules\Labors\Models\Labor;
|
||||
use Modules\MasterData\Models\EquipmentRate;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
@@ -21,6 +31,11 @@ class ProjectController extends Controller
|
||||
$user = auth()->user();
|
||||
|
||||
$query = Project::query()
|
||||
->where('current_wizard_step', '>=', 7)
|
||||
->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
|
||||
@@ -28,6 +43,9 @@ class ProjectController extends Controller
|
||||
$query->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
|
||||
@@ -38,8 +56,11 @@ class ProjectController extends Controller
|
||||
->withQueryString()
|
||||
->through(fn ($p) => $p->append(['capitalization_percentage', 'is_over_budget']));
|
||||
|
||||
$drafts = $draftsQuery->latest()->get();
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Index', [
|
||||
'projects' => $projects,
|
||||
'drafts' => $drafts,
|
||||
'filters' => $request->only(['search', 'status']),
|
||||
'statuses' => collect(ProjectStatus::cases())->map(fn ($s) => [
|
||||
'value' => $s->value,
|
||||
@@ -51,9 +72,11 @@ class ProjectController extends Controller
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -68,8 +91,17 @@ class ProjectController extends Controller
|
||||
'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
|
||||
@@ -80,21 +112,28 @@ class ProjectController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-seed default milestones
|
||||
$this->seedDefaultMilestones($project);
|
||||
|
||||
return redirect()->route('projects.show', $project)
|
||||
->with('success', "Project \"{$project->name}\" created.");
|
||||
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:id,project_id,status', // Only load status for counts
|
||||
'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']);
|
||||
$project->append([
|
||||
'capitalization_percentage',
|
||||
'is_over_budget',
|
||||
'rollup_capitalization',
|
||||
'rollup_estimated_value',
|
||||
'rollup_contract_value',
|
||||
]);
|
||||
|
||||
$taskStats = [
|
||||
'total' => $project->tasks->count(),
|
||||
@@ -104,6 +143,8 @@ class ProjectController extends Controller
|
||||
'closed' => $project->tasks->where('status', 'closed')->count(),
|
||||
];
|
||||
|
||||
$estimationTotals = $this->getWizardTotals($project);
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Overview', [
|
||||
'project' => $project,
|
||||
'taskStats' => $taskStats,
|
||||
@@ -111,6 +152,8 @@ class ProjectController extends Controller
|
||||
'value' => $s->value,
|
||||
'label' => $s->label(),
|
||||
]),
|
||||
'estimationTotals' => $estimationTotals,
|
||||
'tab' => request()->query('tab', 'overview'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -170,18 +213,62 @@ class ProjectController extends Controller
|
||||
|
||||
public function edit(Project $project)
|
||||
{
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee'])->with('employeeProfile')->select('id', 'ulid', 'name', 'email')->get();
|
||||
$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',
|
||||
]);
|
||||
|
||||
$project->load('personnel:id,name');
|
||||
$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',
|
||||
@@ -191,8 +278,18 @@ class ProjectController extends Controller
|
||||
'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)
|
||||
@@ -288,4 +385,734 @@ class ProjectController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user