feat: implement initial project, document, and resource management modules with routing, controllers, and UI components

This commit is contained in:
Ajjj
2026-08-05 03:26:06 +08:00
parent c03de704e2
commit 80fa96097c
16 changed files with 718 additions and 320 deletions

View File

@@ -95,6 +95,22 @@ class ProjectController extends Controller
]);
}
public function discard(Request $request, Project $project)
{
abort_unless(
$request->user()->can('projects.delete') || $request->user()->can('delete projects'),
403
);
if ($project->current_wizard_step >= 7) {
return back()->with('error', 'Only draft projects can be discarded.');
}
$project->delete();
return redirect()->route('projects.index')->with('success', 'Draft project discarded successfully.');
}
public function store(Request $request)
{
$validated = $request->validate([

View File

@@ -72,7 +72,7 @@ export default function ProjectLayout({ project, allowedTransitions, children, c
>
<Head title={project ? `${project.name} - ${currentTab.charAt(0).toUpperCase() + currentTab.slice(1)}` : 'Select Project'} />
<div className={currentTab === 'tasks' ? "h-[calc(100vh-73px)] flex flex-col" : "py-6"}>
<div className={currentTab === 'tasks' ? "h-[calc(100vh-73px)] flex flex-col pt-2" : "py-6"}>
<div className={currentTab === 'tasks' ? "w-full h-full p-4 flex flex-col" : "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"}>
{!project ? (
<div className="py-10">

View File

@@ -12,7 +12,7 @@ import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/Components/ui/table';
import { PaginatedData, PageProps } from '@/types';
import { Plus, Eye, Pencil, FolderKanban } from 'lucide-react';
import { Plus, Eye, Pencil, FolderKanban, Trash2 } from 'lucide-react';
import { FormEvent, useMemo, useState } from 'react';
interface Project {
@@ -177,17 +177,17 @@ export default function Index({ projects, history, drafts = [], filters, statuse
{activeTab === 'active' && (
<>
<Table>
<Table containerClassName="overflow-x-visible" className="table-fixed text-xs [&_th]:px-1.5 [&_td]:px-1.5">
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Project & Contractor</TableHead>
<TableHead>Client</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Contract Value</TableHead>
<TableHead className="text-right">Capitalization</TableHead>
<TableHead className="text-right">Progress</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead className="w-[11%]">Code</TableHead>
<TableHead className="w-[22%]">Project & Contractor</TableHead>
<TableHead className="w-[13%]">Client</TableHead>
<TableHead className="w-[11%] text-center">Status</TableHead>
<TableHead className="w-[14%] text-right">Contract Value</TableHead>
<TableHead className="w-[14%] text-right">Capitalization</TableHead>
<TableHead className="w-[10%] text-right">Progress</TableHead>
<TableHead className="w-[5%] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -200,27 +200,29 @@ export default function Index({ projects, history, drafts = [], filters, statuse
) : (
projects.data.map((project) => (
<TableRow key={project.id}>
<TableCell className="font-mono text-sm">{project.code}</TableCell>
<TableCell className="font-medium">
<div>{project.name}</div>
<TableCell className="truncate font-mono text-[11px]">{project.code}</TableCell>
<TableCell className="truncate font-medium">
<div className="truncate" title={project.name}>{project.name}</div>
{project.contractor && (
<div className="text-[11px] text-blue-600 font-normal flex items-center gap-1 mt-0.5">
<span>🏢 {project.contractor.company_name}</span>
</div>
)}
</TableCell>
<TableCell className="text-gray-500">{project.client_name || '-'}</TableCell>
<TableCell>
<Badge variant={statusVariant(project.status)}>
{statusLabel(project.status)}
</Badge>
<TableCell className="truncate text-gray-500" title={project.client_name || undefined}>{project.client_name || '-'}</TableCell>
<TableCell className="text-center">
<div className="flex justify-center">
<Badge variant={statusVariant(project.status)}>
{statusLabel(project.status)}
</Badge>
</div>
</TableCell>
<TableCell className="text-right">
{formatCurrency(project.contract_value)}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="w-12 h-1.5 rounded-full bg-gray-200 overflow-hidden">
<div className="w-8 h-1.5 rounded-full bg-gray-200 overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
project.is_over_budget ? 'bg-red-500' : project.capitalization_percentage >= 80 ? 'bg-amber-500' : 'bg-emerald-500'
@@ -236,8 +238,8 @@ export default function Index({ projects, history, drafts = [], filters, statuse
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="w-16 h-2 rounded-full bg-gray-200 overflow-hidden">
<div className="flex items-center justify-end gap-1">
<div className="w-10 h-1.5 rounded-full bg-gray-200 overflow-hidden">
<div
className="h-full bg-green-500 rounded-full transition-all"
style={{ width: `${Math.min(Number(project.milestone_completion ?? project.completion_percentage ?? 0), 100)}%` }}
@@ -437,12 +439,27 @@ export default function Index({ projects, history, drafts = [], filters, statuse
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Link href={route('projects.wizard', [project.ulid, { step: project.current_wizard_step }])}>
<Button variant="outline" size="sm" className="h-8 border-emerald-500 text-emerald-600 hover:bg-emerald-50 text-xs">
Resume Setup
</Button>
</Link>
</div>
<Link href={route('projects.wizard', [project.ulid, { step: project.current_wizard_step }])}>
<Button variant="outline" size="sm" className="h-8 border-emerald-500 text-emerald-600 hover:bg-emerald-50 text-xs">
Resume Setup
</Button>
</Link>
{can('delete', 'projects') && (
<Button
variant="ghost"
size="icon-sm"
title="Discard draft project"
className="text-rose-500 hover:bg-rose-50 hover:text-rose-700"
onClick={() => {
if (confirm(`Discard draft project "${project.name}"? This action cannot be undone.`)) {
router.delete(route('projects.discard', project.ulid));
}
}}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))

View File

@@ -592,11 +592,6 @@ export default function Wizard({ project, step: currentStep, employees, projects
<div className="space-y-3">
{localTasks.map((t, idx) => (
<div key={idx} className="bg-slate-50/50 p-4 rounded-xl border border-slate-200/60 space-y-3 relative">
<div className="absolute right-3 top-3">
<Button variant="ghost" size="icon" onClick={() => removeTask(idx)} className="h-8 w-8 text-rose-500 hover:text-rose-700 hover:bg-rose-50">
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="md:col-span-2">
<Label className="text-[10px] text-slate-500">Task Name</Label>
@@ -607,24 +602,37 @@ export default function Wizard({ project, step: currentStep, employees, projects
className="h-8 text-xs border-slate-200 mt-1"
/>
</div>
<div>
<Label className="text-[10px] text-slate-500">Milestone</Label>
<Select
value={t.milestone_ulid}
onValueChange={val => updateTask(idx, 'milestone_ulid', val || '')}
items={localMilestones.map((m, mIdx) => ({ value: m.ulid || String(mIdx), label: m.name || `Milestone ${mIdx+1}` }))}
<div className="flex items-end gap-2">
<div className="min-w-0 flex-1">
<Label className="text-[10px] text-slate-500">Milestone</Label>
<Select
value={t.milestone_ulid}
onValueChange={val => updateTask(idx, 'milestone_ulid', val || '')}
items={localMilestones.map((m, mIdx) => ({ value: m.ulid || String(mIdx), label: m.name || `Milestone ${mIdx+1}` }))}
>
<SelectTrigger className="h-8 text-xs border-slate-200 mt-1">
<SelectValue placeholder="Select Milestone" />
</SelectTrigger>
<SelectContent>
{localMilestones.map((m, mIdx) => (
<SelectItem key={mIdx} value={m.ulid || String(mIdx)}>
{m.name || `Milestone ${mIdx+1}`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={`Delete task ${idx + 1}`}
title="Delete task"
onClick={() => removeTask(idx)}
className="mb-0 h-8 w-8 shrink-0 rounded-lg p-2 text-rose-500 hover:bg-rose-50 hover:text-rose-700"
>
<SelectTrigger className="h-8 text-xs border-slate-200 mt-1">
<SelectValue placeholder="Select Milestone" />
</SelectTrigger>
<SelectContent>
{localMilestones.map((m, mIdx) => (
<SelectItem key={mIdx} value={m.ulid || String(mIdx)}>
{m.name || `Milestone ${mIdx+1}`}
</SelectItem>
))}
</SelectContent>
</Select>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">

View File

@@ -5,6 +5,7 @@ use Modules\ProjectManagement\Http\Controllers\ProjectController;
Route::middleware(['web', 'auth', 'permission:projects.access'])->group(function () {
Route::resource('projects', ProjectController::class)->except(['destroy']);
Route::delete('projects/{project}/discard', [ProjectController::class, 'discard'])->name('projects.discard');
Route::patch('projects/{project}/transition', [ProjectController::class, 'transition'])->name('projects.transition');
// Project Wizard Routes