chore: update document approval workflow and bug fixes
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
<?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;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$query = Project::query()
|
||||
->with(['personnel:id,name']);
|
||||
|
||||
// Non-platform admins/owners only see projects they are assigned to
|
||||
if ($user && !$user->hasRole('Super Admin') && !$user->hasRole('Owner')) {
|
||||
$query->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)
|
||||
->withQueryString()
|
||||
->through(fn ($p) => $p->append(['capitalization_percentage', 'is_over_budget']));
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Index', [
|
||||
'projects' => $projects,
|
||||
'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();
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Create', [
|
||||
'employees' => $employees,
|
||||
]);
|
||||
}
|
||||
|
||||
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 = 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']);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-seed default milestones
|
||||
$this->seedDefaultMilestones($project);
|
||||
|
||||
return redirect()->route('projects.show', $project)
|
||||
->with('success', "Project \"{$project->name}\" created.");
|
||||
}
|
||||
|
||||
public function show(Project $project)
|
||||
{
|
||||
$project->load([
|
||||
'contractor:id,company_name',
|
||||
'tasks:id,project_id,status', // Only load status for counts
|
||||
]);
|
||||
|
||||
$project->append(['capitalization_percentage', 'is_over_budget']);
|
||||
|
||||
$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(),
|
||||
];
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Overview', [
|
||||
'project' => $project,
|
||||
'taskStats' => $taskStats,
|
||||
'allowedTransitions' => collect($project->status->allowedTransitions())->map(fn ($s) => [
|
||||
'value' => $s->value,
|
||||
'label' => $s->label(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$employees = User::whereIn('user_type', ['admin', 'employee'])->with('employeeProfile')->select('id', 'ulid', 'name', 'email')->get();
|
||||
|
||||
$project->load('personnel:id,name');
|
||||
|
||||
return Inertia::render('ProjectManagement::Projects/Edit', [
|
||||
'project' => $project,
|
||||
'employees' => $employees,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Project $project)
|
||||
{
|
||||
$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->update($validated);
|
||||
|
||||
return redirect()->route('projects.show', $project)
|
||||
->with('success', "Project \"{$project->name}\" updated.");
|
||||
}
|
||||
|
||||
public function destroy(Project $project)
|
||||
{
|
||||
$name = $project->name;
|
||||
$project->delete();
|
||||
|
||||
return redirect()->route('projects.index')
|
||||
->with('success', "Project \"{$name}\" deleted.");
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user