Files
GSB-Construction/Modules/TimelineScheduling/resources/js/Pages/Timeline/Index.tsx

258 lines
20 KiB
TypeScript

import ProjectLayout from '../../../../../ProjectManagement/resources/js/Layouts/ProjectLayout';
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Badge } from '@/Components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/Components/ui/dialog';
import { Input } from '@/Components/ui/input';
import { useForm, router } from '@inertiajs/react';
import { CheckCircle2, CloudRain, Clock, Milestone as MilestoneIcon, Plus, AlertTriangle, Pencil } from 'lucide-react';
import { useState } from 'react';
export default function Timeline({ project, milestones, milestoneStats, weatherConditions }: any) {
const [addMilestoneOpen, setAddMilestoneOpen] = useState(false);
const [editMilestoneId, setEditMilestoneId] = useState<string | null>(null);
if (!project) {
return <ProjectLayout project={null} currentTab="timeline" children={null} />;
}
return (
<ProjectLayout project={project} currentTab="timeline">
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Card>
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-emerald-50">
<CheckCircle2 className="h-5 w-5 text-emerald-600" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-500">Milestone Progress</p>
<p className="text-lg font-semibold">{milestoneStats?.completion_percentage?.toFixed(0) || 0}%</p>
<div className="mt-1.5 h-1.5 rounded-full bg-gray-200 overflow-hidden">
<div className="h-full rounded-full bg-emerald-500 transition-all duration-500" style={{ width: `${Math.min(milestoneStats?.completion_percentage || 0, 100)}%` }} />
</div>
<p className="text-xs text-gray-400 mt-1">{milestoneStats?.completed || 0} / {milestoneStats?.total || 0} milestones</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${milestoneStats?.weather_delay_days > 0 ? 'bg-blue-50' : 'bg-gray-100'}`}>
<CloudRain className={`h-5 w-5 ${milestoneStats?.weather_delay_days > 0 ? 'text-blue-600' : 'text-gray-400'}`} />
</div>
<div>
<p className="text-xs text-gray-500">Weather Delays</p>
<p className="text-lg font-semibold">{milestoneStats?.weather_delay_days || 0} day{(milestoneStats?.weather_delay_days || 0) !== 1 ? 's' : ''}</p>
<p className="text-xs text-gray-400">Total lost to weather</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gray-100">
<Clock className="h-5 w-5 text-gray-600" />
</div>
<div>
<p className="text-xs text-gray-500">Total Delay Entries</p>
<p className="text-lg font-semibold">
{project?.tasks?.reduce((sum: number, t: any) => sum + (t.delays?.length || 0), 0) || 0}
</p>
<p className="text-xs text-gray-400">Across all tasks</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Milestones Card */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<MilestoneIcon className="h-5 w-5 text-gray-500" /> Milestones
</CardTitle>
<Dialog open={addMilestoneOpen} onOpenChange={setAddMilestoneOpen}>
<DialogTrigger>
<Button size="sm">
<Plus className="mr-2 h-4 w-4" /> Add Milestone
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Add Custom Milestone</DialogTitle></DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
router.post(route('projects.milestones.store', project.ulid), {
name: fd.get('name'),
planned_date: fd.get('planned_date'),
weight_percentage: fd.get('weight_percentage'),
}, { onSuccess: () => setAddMilestoneOpen(false), preserveScroll: true });
}} className="space-y-4">
<div>
<label className="text-sm font-medium">Name *</label>
<Input name="name" required />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Planned Date</label>
<Input name="planned_date" type="date" />
</div>
<div>
<label className="text-sm font-medium">Weight %</label>
<Input name="weight_percentage" type="number" step="0.01" min="0" max="100" defaultValue="5" required />
</div>
</div>
<div className="flex justify-end">
<Button type="submit"><Plus className="mr-2 h-4 w-4" /> Add</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{milestones?.length === 0 ? (
<p className="text-sm text-gray-400 italic py-4 text-center">No milestones yet.</p>
) : (
<div className="relative">
{/* Vertical line */}
<div className="absolute left-4 top-2 bottom-2 w-0.5 bg-gray-200" />
<div className="space-y-0">
{milestones?.map((m: any, idx: number) => (
<div key={m.id} className="relative flex items-start gap-4 py-3 group">
{/* Node dot */}
<div className={`relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 transition-colors ${
m.is_completed ? 'border-emerald-500 bg-emerald-50' :
m.is_overdue ? 'border-amber-500 bg-amber-50' :
'border-gray-300 bg-white'
}`}>
{m.is_completed ? (
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
) : m.is_overdue ? (
<AlertTriangle className="h-4 w-4 text-amber-600" />
) : (
<span className="text-xs font-semibold text-gray-400">{idx + 1}</span>
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h4 className={`text-sm font-medium ${m.is_completed ? 'text-emerald-700' : 'text-gray-800'}`}>
{m.name}
</h4>
<Badge variant="outline" className="text-[10px] px-1.5">
{Number(m.weight_percentage).toFixed(0)}%
</Badge>
{m.weather_impacted && (
<span className="text-blue-500 text-xs flex items-center gap-0.5" title={`Weather delay: ${m.weather_delay_days}d — ${m.weather_notes || ''}`}>
<CloudRain className="h-3.5 w-3.5" /> {m.weather_delay_days}d
</span>
)}
</div>
<div className="flex items-center gap-3 text-xs text-gray-400 mt-0.5">
{m.planned_date && (
<span>Plan: {new Date(m.planned_date).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}</span>
)}
{m.actual_date && (
<span className="text-emerald-600">
Done: {new Date(m.actual_date).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}
{m.days_delayed > 0 && <span className="text-amber-500 ml-1">(+{m.days_delayed}d late)</span>}
</span>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Dialog open={editMilestoneId === m.ulid} onOpenChange={(open) => setEditMilestoneId(open ? m.ulid : null)}>
<DialogTrigger>
<Button variant="ghost" size="icon-sm" title="Edit">
<Pencil className="h-3.5 w-3.5" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Edit Milestone</DialogTitle></DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
router.put(route('projects.milestones.update', [project.ulid, m.ulid]), {
name: fd.get('name'),
planned_date: fd.get('planned_date') || null,
actual_date: fd.get('actual_date') || null,
weight_percentage: fd.get('weight_percentage'),
weather_impacted: fd.get('weather_impacted') === 'on',
weather_condition: fd.get('weather_condition') || null,
weather_delay_days: fd.get('weather_delay_days') || 0,
weather_notes: fd.get('weather_notes') || null,
}, { onSuccess: () => setEditMilestoneId(null), preserveScroll: true });
}} className="space-y-4">
<div>
<label className="text-sm font-medium">Name</label>
<Input name="name" defaultValue={m.name} required />
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium">Planned Date</label>
<Input name="planned_date" type="date" defaultValue={m.planned_date?.split('T')[0]} />
</div>
<div>
<label className="text-sm font-medium">Actual Date</label>
<Input name="actual_date" type="date" defaultValue={m.actual_date?.split('T')[0]} />
</div>
<div>
<label className="text-sm font-medium">Weight %</label>
<Input name="weight_percentage" type="number" step="0.01" min="0" max="100" defaultValue={m.weight_percentage} />
</div>
</div>
<div className="border-t pt-4 space-y-3">
<label className="flex items-center gap-2 text-sm font-medium">
<input type="checkbox" name="weather_impacted" defaultChecked={m.weather_impacted} className="rounded border-gray-300" />
<CloudRain className="h-4 w-4 text-blue-500" /> Weather Impacted
</label>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Condition</label>
<select name="weather_condition" defaultValue={m.weather_condition || ''} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm">
<option value="">N/A</option>
{weatherConditions?.map((wc: any) => (
<option key={wc.value} value={wc.value}>{wc.icon} {wc.label}</option>
))}
</select>
</div>
<div>
<label className="text-sm font-medium">Delay Days</label>
<Input name="weather_delay_days" type="number" min="0" defaultValue={m.weather_delay_days} />
</div>
</div>
<div>
<label className="text-sm font-medium">Weather Notes</label>
<Input name="weather_notes" defaultValue={m.weather_notes || ''} />
</div>
</div>
<div className="flex justify-end pt-2 border-t mt-4">
<Button type="submit">Save Changes</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>
</ProjectLayout>
);
}