1170 lines
48 KiB
PHP
1170 lines
48 KiB
PHP
<?php
|
|
|
|
namespace Modules\ProjectManagement\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Modules\MasterData\Models\Material;
|
|
use Modules\MaterialLogistics\Models\ProjectInventory;
|
|
use Modules\ProjectManagement\Enums\DelayReason;
|
|
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\Team;
|
|
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
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$user = auth()->user();
|
|
|
|
$query = Project::query()
|
|
->where('current_wizard_step', '>=', 7)
|
|
->whereNotIn('status', ['completed', 'closed'])
|
|
->with(['contractor:id,company_name', 'personnel:id,name']);
|
|
|
|
$historyQuery = Project::query()
|
|
->where('current_wizard_step', '>=', 7)
|
|
->whereIn('status', ['completed', 'closed'])
|
|
->with(['contractor:id,company_name', 'personnel:id,name']);
|
|
|
|
$draftsQuery = Project::query()
|
|
->where('current_wizard_step', '<', 7)
|
|
->with(['contractor:id,company_name', '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', 'profile_picture')->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['contract_value'] = $validated['contract_value'] ?? 0;
|
|
$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', 'profile_picture')
|
|
->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', 'profile_picture')->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',
|
|
'personnel:id,ulid,name,email',
|
|
]);
|
|
|
|
$employees = User::whereIn('user_type', ['admin', 'employee'])->with('employeeProfile')->select('id', 'ulid', 'name', 'email', 'profile_picture')->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();
|
|
$teams = Team::with('users:id,ulid,name,email')->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,
|
|
'teams' => $teams,
|
|
]);
|
|
}
|
|
|
|
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',
|
|
'team_ulids' => 'nullable|array',
|
|
'team_ulids.*' => 'string|exists:teams,ulid',
|
|
'user_ulids' => 'nullable|array',
|
|
'user_ulids.*' => 'string|exists:users,ulid',
|
|
]);
|
|
|
|
\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();
|
|
|
|
// Synchronize project personnel pool
|
|
$userIds = [];
|
|
|
|
// Add users from selected teams
|
|
if (!empty($validated['team_ulids'])) {
|
|
$teams = Team::whereIn('ulid', $validated['team_ulids'])->with('users')->get();
|
|
foreach ($teams as $team) {
|
|
foreach ($team->users as $user) {
|
|
$userIds[$user->id] = ['role' => 'member'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add individual users
|
|
if (!empty($validated['user_ulids'])) {
|
|
$users = User::whereIn('ulid', $validated['user_ulids'])->get();
|
|
foreach ($users as $user) {
|
|
$userIds[$user->id] = ['role' => 'member'];
|
|
}
|
|
}
|
|
|
|
// Keep existing PM if present
|
|
$pm = $project->personnel()->wherePivot('role', 'pm')->first();
|
|
if ($pm) {
|
|
$userIds[$pm->id] = ['role' => 'pm'];
|
|
}
|
|
|
|
// Sync the project personnel
|
|
if (isset($validated['team_ulids']) || isset($validated['user_ulids'])) {
|
|
$project->personnel()->sync($userIds);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|