1+ import { Box , Text , Flex , Badge } from "@chakra-ui/react"
2+ import { type ProjectPublic } from "@/client"
3+
4+ interface ProjectTimelineProps {
5+ project : ProjectPublic
6+ }
7+
8+ export function ProjectTimeline ( { project } : ProjectTimelineProps ) {
9+ const startDate = project . start_date ? new Date ( project . start_date ) : null
10+ const deadline = project . deadline ? new Date ( project . deadline ) : null
11+ const today = new Date ( )
12+
13+ // Calculate progress based on dates
14+ const getDateProgress = ( ) => {
15+ if ( ! startDate || ! deadline ) return 0
16+ const total = deadline . getTime ( ) - startDate . getTime ( )
17+ const elapsed = today . getTime ( ) - startDate . getTime ( )
18+ return Math . max ( 0 , Math . min ( 100 , ( elapsed / total ) * 100 ) )
19+ }
20+
21+ const dateProgress = getDateProgress ( )
22+ const isOverdue = deadline && today > deadline
23+ const daysRemaining = deadline
24+ ? Math . ceil ( ( deadline . getTime ( ) - today . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) )
25+ : null
26+
27+ // Define milestones based on project phases
28+ const milestones = [
29+ { label : "Start" , date : startDate , position : 0 } ,
30+ { label : "25%" , date : null , position : 25 } ,
31+ { label : "50%" , date : null , position : 50 } ,
32+ { label : "75%" , date : null , position : 75 } ,
33+ { label : "Deadline" , date : deadline , position : 100 } ,
34+ ]
35+
36+ const getStatusColor = ( ) => {
37+ if ( isOverdue ) return "red.500"
38+ if ( dateProgress > 75 ) return "orange.500"
39+ if ( dateProgress > 50 ) return "yellow.500"
40+ return "green.500"
41+ }
42+
43+ const formatDate = ( date : Date | null ) => {
44+ if ( ! date ) return "Not set"
45+ return date . toLocaleDateString ( "en-US" , { month : "short" , day : "numeric" , year : "numeric" } )
46+ }
47+
48+ return (
49+ < Box width = "100%" >
50+ { /* Header */ }
51+ < Flex justify = "space-between" align = "center" mb = "6" >
52+ < Box >
53+ < Text fontSize = "lg" fontWeight = "bold" > Project Timeline</ Text >
54+ < Text fontSize = "sm" color = "fg.muted" >
55+ { formatDate ( startDate ) } → { formatDate ( deadline ) }
56+ </ Text >
57+ </ Box >
58+ < Box textAlign = "right" >
59+ { daysRemaining !== null && (
60+ < Badge colorScheme = { isOverdue ? "red" : daysRemaining < 7 ? "orange" : "green" } >
61+ { isOverdue ? `${ Math . abs ( daysRemaining ) } days overdue` : `${ daysRemaining } days left` }
62+ </ Badge >
63+ ) }
64+ < Text fontSize = "sm" color = "fg.muted" mt = "1" >
65+ Progress: { project . progress || 0 } %
66+ </ Text >
67+ </ Box >
68+ </ Flex >
69+
70+ { /* Timeline */ }
71+ < Box position = "relative" mt = "16" mb = "16" px = "4" >
72+ { /* Background track */ }
73+ < Box
74+ position = "absolute"
75+ top = "40px"
76+ left = "4"
77+ right = "4"
78+ height = "8px"
79+ bg = "bg.muted"
80+ borderRadius = "full"
81+ />
82+
83+ { /* Progress bar (based on actual progress) */ }
84+ < Box
85+ position = "absolute"
86+ top = "40px"
87+ left = "-6"
88+ width = { `calc(${ project . progress || 0 } % - -16px)` }
89+ height = "8px"
90+ bg = { getStatusColor ( ) }
91+ borderRadius = "full"
92+ transition = "width 0.3s ease"
93+ />
94+
95+ { /* Current date indicator (based on time elapsed) */ }
96+ { startDate && deadline && (
97+ < Box
98+ position = "absolute"
99+ top = "24px"
100+ left = { `calc(${ dateProgress } %)` }
101+ transform = "translateX(-75%)"
102+ zIndex = "3"
103+ >
104+ < Box
105+ width = "2px"
106+ height = "24px"
107+ bg = "blue.500"
108+ mb = "4"
109+ />
110+ < Text
111+ fontSize = "xs"
112+ fontWeight = "bold"
113+ color = "blue.500"
114+ textAlign = "center"
115+ whiteSpace = "nowrap"
116+ mt = "-1"
117+ >
118+ Today
119+ </ Text >
120+ </ Box >
121+ ) }
122+
123+ { /* Milestones */ }
124+ { milestones . map ( ( milestone , index ) => (
125+ < Box
126+ key = { index }
127+ position = "absolute"
128+ top = "36px"
129+ left = { `${ milestone . position } %` }
130+ transform = "translateX(-30%)"
131+ zIndex = "2"
132+ >
133+ { /* Milestone dot */ }
134+ < Box
135+ width = "16px"
136+ height = "16px"
137+ bg = { milestone . position <= ( project . progress || 0 ) ? getStatusColor ( ) : "bg.muted" }
138+ borderRadius = "full"
139+ borderWidth = "3px"
140+ borderColor = "white"
141+ boxShadow = "sm"
142+ />
143+
144+ { /* Milestone label */ }
145+ < Text
146+ fontSize = "xs"
147+ color = "fg.muted"
148+ textAlign = "center"
149+ whiteSpace = "nowrap"
150+ mt = "6"
151+ >
152+ { milestone . label }
153+ </ Text >
154+
155+ { /* Date if available */ }
156+ { milestone . date && (
157+ < Text
158+ fontSize = "xs"
159+ color = "fg.subtle"
160+ textAlign = "center"
161+ whiteSpace = "nowrap"
162+ mt = "1"
163+ >
164+ { formatDate ( milestone . date ) }
165+ </ Text >
166+ ) }
167+ </ Box >
168+ ) ) }
169+ </ Box >
170+
171+ { /* Status legend */ }
172+ < Flex gap = "4" mt = "8" fontSize = "sm" color = "fg.muted" justify = "center" >
173+ < Flex align = "center" gap = "2" >
174+ < Box width = "12px" height = "12px" bg = { getStatusColor ( ) } borderRadius = "full" />
175+ < Text > Current Progress</ Text >
176+ </ Flex >
177+ < Flex align = "center" gap = "2" >
178+ < Box width = "12px" height = "12px" bg = "blue.500" borderRadius = "full" />
179+ < Text > Today</ Text >
180+ </ Flex >
181+ </ Flex >
182+ </ Box >
183+ )
184+ }
0 commit comments