Skip to content

Commit f4c5cd9

Browse files
committed
Phases 171-175: Project Management Module — 21 tests passing
5 migrations (projects, project_members, tasks, milestones, time_entries), 4 models (Project/Task/Milestone/TimeEntry), ProjectPolicy, 5 controllers (Project/Task/Milestone/TimeEntry/Dashboard), PMServiceProvider registered, 9 React pages (Dashboard, Projects CRUD, Tasks CRUD, TimeEntries), Sidebar PM section. 21/21 feature tests passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent dab22c7 commit f4c5cd9

29 files changed

Lines changed: 2538 additions & 0 deletions

erp/app/Modules/Core/Providers/CoreServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use App\Modules\Inventory\Providers\InventoryServiceProvider;
1212
use App\Modules\Manufacturing\Providers\ManufacturingServiceProvider;
1313
use App\Modules\CRM\Providers\CRMServiceProvider;
14+
use App\Modules\PM\Providers\PMServiceProvider;
1415
use Illuminate\Support\Facades\Gate;
1516
use Illuminate\Support\ServiceProvider;
1617

@@ -23,6 +24,7 @@ public function register(): void
2324
$this->app->register(HRServiceProvider::class);
2425
$this->app->register(ManufacturingServiceProvider::class);
2526
$this->app->register(CRMServiceProvider::class);
27+
$this->app->register(PMServiceProvider::class);
2628
}
2729

2830
public function boot(): void
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Modules\PM\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\PM\Models\Milestone;
7+
use App\Modules\PM\Models\Project;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
11+
class MilestoneController extends Controller
12+
{
13+
public function store(Request $request, Project $project): RedirectResponse
14+
{
15+
$validated = $request->validate([
16+
'name' => 'required|string|max:255',
17+
'description' => 'nullable|string',
18+
'due_date' => 'nullable|date',
19+
]);
20+
21+
$project->milestones()->create([
22+
...$validated,
23+
'tenant_id' => auth()->user()->tenant_id,
24+
'created_by' => auth()->id(),
25+
]);
26+
27+
return redirect()->back()->with('success', 'Milestone created successfully.');
28+
}
29+
30+
public function complete(Project $project, Milestone $milestone): RedirectResponse
31+
{
32+
$milestone->complete();
33+
34+
return redirect()->back()->with('success', 'Milestone completed.');
35+
}
36+
37+
public function destroy(Project $project, Milestone $milestone): RedirectResponse
38+
{
39+
$milestone->delete();
40+
41+
return redirect()->back()->with('success', 'Milestone deleted.');
42+
}
43+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace App\Modules\PM\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\PM\Models\Project;
7+
use App\Modules\PM\Models\Task;
8+
use App\Modules\PM\Models\TimeEntry;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class PMDashboardController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$projects = Project::withCount(['tasks', 'members'])->with('manager')->get();
18+
19+
$stats = [
20+
'total_projects' => $projects->count(),
21+
'draft_projects' => $projects->where('status', 'draft')->count(),
22+
'active_projects' => $projects->where('status', 'active')->count(),
23+
'on_hold_projects'=> $projects->where('status', 'on_hold')->count(),
24+
'completed_projects' => $projects->where('status', 'completed')->count(),
25+
'cancelled_projects' => $projects->where('status', 'cancelled')->count(),
26+
'total_tasks' => Task::count(),
27+
'overdue_tasks' => Task::whereNotNull('due_date')
28+
->where('due_date', '<', now()->toDateString())
29+
->whereNotIn('status', ['done', 'cancelled'])
30+
->count(),
31+
'hours_this_month' => TimeEntry::whereYear('date', now()->year)
32+
->whereMonth('date', now()->month)
33+
->sum('hours'),
34+
];
35+
36+
$recentProjects = Project::with('manager')
37+
->withCount('tasks')
38+
->orderByDesc('created_at')
39+
->limit(10)
40+
->get();
41+
42+
return Inertia::render('PM/Dashboard', [
43+
'stats' => $stats,
44+
'recentProjects' => $recentProjects,
45+
]);
46+
}
47+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace App\Modules\PM\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\User;
7+
use App\Modules\PM\Models\Project;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class ProjectController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$this->authorize('viewAny', Project::class);
18+
19+
$projects = Project::with(['manager'])
20+
->withCount(['tasks', 'members'])
21+
->when($request->search, fn ($q) => $q->where(function ($q) use ($request) {
22+
$q->where('name', 'like', "%{$request->search}%")
23+
->orWhere('code', 'like', "%{$request->search}%");
24+
}))
25+
->when($request->status, fn ($q) => $q->where('status', $request->status))
26+
->orderByDesc('created_at')
27+
->paginate(20)
28+
->withQueryString();
29+
30+
return Inertia::render('PM/Projects/Index', [
31+
'projects' => $projects,
32+
'filters' => $request->only(['search', 'status']),
33+
]);
34+
}
35+
36+
public function create(): Response
37+
{
38+
$this->authorize('create', Project::class);
39+
40+
return Inertia::render('PM/Projects/Create', [
41+
'users' => User::orderBy('name')->get(['id', 'name']),
42+
]);
43+
}
44+
45+
public function store(Request $request): RedirectResponse
46+
{
47+
$this->authorize('create', Project::class);
48+
49+
$validated = $request->validate([
50+
'name' => 'required|string|max:255',
51+
'code' => 'nullable|string|max:100',
52+
'description' => 'nullable|string',
53+
'status' => 'nullable|in:draft,active,on_hold,completed,cancelled',
54+
'priority' => 'nullable|in:low,medium,high,critical',
55+
'budget' => 'nullable|numeric|min:0',
56+
'start_date' => 'nullable|date',
57+
'end_date' => 'nullable|date|after_or_equal:start_date',
58+
'client_name' => 'nullable|string|max:255',
59+
'manager_id' => 'nullable|exists:users,id',
60+
]);
61+
62+
$project = Project::create([
63+
...$validated,
64+
'tenant_id' => auth()->user()->tenant_id,
65+
'created_by' => auth()->id(),
66+
]);
67+
68+
// Generate code if not provided
69+
if (empty($project->code)) {
70+
$project->code = $project->generateCode();
71+
$project->save();
72+
}
73+
74+
return redirect()->route('pm.projects.show', $project)
75+
->with('success', 'Project created successfully.');
76+
}
77+
78+
public function show(Project $project): Response
79+
{
80+
$this->authorize('view', $project);
81+
82+
$project->load([
83+
'manager',
84+
'tasks.assignee',
85+
'milestones',
86+
'members',
87+
'timeEntries.user',
88+
]);
89+
90+
return Inertia::render('PM/Projects/Show', [
91+
'project' => $project,
92+
]);
93+
}
94+
95+
public function edit(Project $project): Response
96+
{
97+
$this->authorize('update', $project);
98+
99+
return Inertia::render('PM/Projects/Edit', [
100+
'project' => $project,
101+
'users' => User::orderBy('name')->get(['id', 'name']),
102+
]);
103+
}
104+
105+
public function update(Request $request, Project $project): RedirectResponse
106+
{
107+
$this->authorize('update', $project);
108+
109+
$validated = $request->validate([
110+
'name' => 'required|string|max:255',
111+
'code' => 'nullable|string|max:100',
112+
'description' => 'nullable|string',
113+
'status' => 'nullable|in:draft,active,on_hold,completed,cancelled',
114+
'priority' => 'nullable|in:low,medium,high,critical',
115+
'budget' => 'nullable|numeric|min:0',
116+
'start_date' => 'nullable|date',
117+
'end_date' => 'nullable|date',
118+
'client_name' => 'nullable|string|max:255',
119+
'manager_id' => 'nullable|exists:users,id',
120+
]);
121+
122+
$project->update($validated);
123+
124+
return redirect()->route('pm.projects.show', $project)
125+
->with('success', 'Project updated successfully.');
126+
}
127+
128+
public function destroy(Project $project): RedirectResponse
129+
{
130+
$this->authorize('delete', $project);
131+
132+
$project->delete();
133+
134+
return redirect()->route('pm.projects.index')
135+
->with('success', 'Project deleted successfully.');
136+
}
137+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace App\Modules\PM\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\User;
7+
use App\Modules\PM\Models\Project;
8+
use App\Modules\PM\Models\Task;
9+
use Illuminate\Http\RedirectResponse;
10+
use Illuminate\Http\Request;
11+
use Inertia\Inertia;
12+
use Inertia\Response;
13+
14+
class TaskController extends Controller
15+
{
16+
public function index(Request $request, Project $project): Response
17+
{
18+
$tasks = $project->tasks()
19+
->with('assignee')
20+
->orderBy('sequence')
21+
->orderByDesc('created_at')
22+
->get();
23+
24+
return Inertia::render('PM/Tasks/Index', [
25+
'project' => $project,
26+
'tasks' => $tasks,
27+
]);
28+
}
29+
30+
public function create(Project $project): Response
31+
{
32+
return Inertia::render('PM/Tasks/Create', [
33+
'project' => $project,
34+
'users' => User::orderBy('name')->get(['id', 'name']),
35+
]);
36+
}
37+
38+
public function store(Request $request, Project $project): RedirectResponse
39+
{
40+
$validated = $request->validate([
41+
'title' => 'required|string|max:255',
42+
'description' => 'nullable|string',
43+
'status' => 'nullable|in:todo,in_progress,review,done,cancelled',
44+
'priority' => 'nullable|in:low,medium,high,urgent',
45+
'assignee_id' => 'nullable|exists:users,id',
46+
'due_date' => 'nullable|date',
47+
'estimated_hours' => 'nullable|numeric|min:0',
48+
]);
49+
50+
$project->tasks()->create([
51+
...$validated,
52+
'tenant_id' => auth()->user()->tenant_id,
53+
'created_by' => auth()->id(),
54+
]);
55+
56+
return redirect()->route('pm.projects.show', $project)
57+
->with('success', 'Task created successfully.');
58+
}
59+
60+
public function show(Project $project, Task $task): Response
61+
{
62+
$task->load(['assignee', 'creator', 'timeEntries.user']);
63+
64+
return Inertia::render('PM/Tasks/Show', [
65+
'project' => $project,
66+
'task' => $task,
67+
'users' => User::orderBy('name')->get(['id', 'name']),
68+
]);
69+
}
70+
71+
public function edit(Project $project, Task $task): Response
72+
{
73+
return Inertia::render('PM/Tasks/Edit', [
74+
'project' => $project,
75+
'task' => $task,
76+
'users' => User::orderBy('name')->get(['id', 'name']),
77+
]);
78+
}
79+
80+
public function update(Request $request, Project $project, Task $task): RedirectResponse
81+
{
82+
$validated = $request->validate([
83+
'title' => 'required|string|max:255',
84+
'description' => 'nullable|string',
85+
'status' => 'nullable|in:todo,in_progress,review,done,cancelled',
86+
'priority' => 'nullable|in:low,medium,high,urgent',
87+
'assignee_id' => 'nullable|exists:users,id',
88+
'due_date' => 'nullable|date',
89+
'estimated_hours' => 'nullable|numeric|min:0',
90+
]);
91+
92+
$task->update($validated);
93+
94+
return redirect()->route('pm.projects.tasks.show', [$project, $task])
95+
->with('success', 'Task updated successfully.');
96+
}
97+
98+
public function destroy(Project $project, Task $task): RedirectResponse
99+
{
100+
$task->delete();
101+
102+
return redirect()->route('pm.projects.show', $project)
103+
->with('success', 'Task deleted successfully.');
104+
}
105+
106+
public function complete(Project $project, Task $task): RedirectResponse
107+
{
108+
$task->complete();
109+
110+
return redirect()->back()->with('success', 'Task marked as done.');
111+
}
112+
}

0 commit comments

Comments
 (0)