@@ -4,26 +4,59 @@ import { useParams } from 'next/navigation';
44import { useAgenticPay } from '@/lib/hooks/useAgenticPay' ;
55import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card' ;
66import { Button } from '@/components/ui/button' ;
7+ import { Input } from '@/components/ui/input' ;
8+ import { Label } from '@/components/ui/label' ;
9+ import { ArrowLeft , Download , Pencil , X , Check , History , PenLine } from 'lucide-react' ;
710import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb' ;
811import { ArrowLeft , Download } from 'lucide-react' ;
912import Link from 'next/link' ;
1013import { Skeleton } from '@/components/ui/skeleton' ;
11- import {
12- formatDateInTimeZone ,
13- formatDateTimeInTimeZone ,
14- formatTimeInTimeZone ,
15- } from '@/lib/utils' ;
16- import { useAuthStore } from '@/store/useAuthStore' ;
14+ import { useState , useEffect } from 'react' ;
15+
16+ interface InvoiceVersion {
17+ timestamp : string ;
18+ workDescription : string ;
19+ hoursWorked : number ;
20+ hourlyRate : number ;
21+ calculatedAmount : number ;
22+ signedAt : string ;
23+ }
1724
1825export default function InvoiceDetailPage ( ) {
1926 const params = useParams ( ) ;
2027 const rawId = params . id as string ;
2128 const projectId = rawId . startsWith ( 'INV-' ) ? rawId . replace ( 'INV-' , '' ) : rawId ;
22- const timezone = useAuthStore ( ( state ) => state . timezone ) ;
2329
2430 const { useProjectDetail } = useAgenticPay ( ) ;
2531 const { project, loading } = useProjectDetail ( projectId ) ;
2632
33+ const [ isEditing , setIsEditing ] = useState ( false ) ;
34+ const [ showHistory , setShowHistory ] = useState ( false ) ;
35+ const [ requiresSignature , setRequiresSignature ] = useState ( false ) ;
36+ const [ isSigned , setIsSigned ] = useState ( false ) ;
37+ const [ versionHistory , setVersionHistory ] = useState < InvoiceVersion [ ] > ( [ ] ) ;
38+
39+ const [ editedValues , setEditedValues ] = useState ( {
40+ workDescription : 'Verified work' ,
41+ hoursWorked : 0 ,
42+ hourlyRate : 0 ,
43+ } ) ;
44+
45+ const calculatedAmount =
46+ editedValues . hoursWorked > 0 && editedValues . hourlyRate > 0
47+ ? editedValues . hoursWorked * editedValues . hourlyRate
48+ : null ;
49+
50+ useEffect ( ( ) => {
51+ if ( ! rawId ) return ;
52+ try {
53+ const stored = localStorage . getItem ( `invoice-history-${ rawId } ` ) ;
54+ if ( stored ) setVersionHistory ( JSON . parse ( stored ) ) ;
55+ } catch {
56+ // ignore
57+ }
58+ } , [ rawId ] ) ;
59+
2760 if ( loading ) {
2861 return (
2962 < div className = "space-y-6" >
@@ -54,26 +87,172 @@ export default function InvoiceDetailPage() {
5487 const status = project . status === 'completed' ? 'paid' : 'pending' ;
5588 const generatedAt = new Date ( project . createdAt ) ;
5689
57- const handlePrint = ( ) => {
58- window . print ( ) ;
90+ const handlePrint = ( ) => window . print ( ) ;
91+
92+ const handleSaveEdits = ( ) => {
93+ setIsEditing ( false ) ;
94+ setRequiresSignature ( true ) ;
95+ setIsSigned ( false ) ;
5996 } ;
6097
98+ const handleSign = ( ) => {
99+ const newVersion : InvoiceVersion = {
100+ timestamp : new Date ( ) . toISOString ( ) ,
101+ workDescription : editedValues . workDescription ,
102+ hoursWorked : editedValues . hoursWorked ,
103+ hourlyRate : editedValues . hourlyRate ,
104+ calculatedAmount : calculatedAmount ?? Number ( project . totalAmount ) ,
105+ signedAt : new Date ( ) . toLocaleString ( ) ,
106+ } ;
107+
108+ const updated = [ newVersion , ...versionHistory ] ;
109+ setVersionHistory ( updated ) ;
110+ localStorage . setItem ( `invoice-history-${ rawId } ` , JSON . stringify ( updated ) ) ;
111+ setRequiresSignature ( false ) ;
112+ setIsSigned ( true ) ;
113+ } ;
114+
115+ const displayAmount = isSigned && calculatedAmount
116+ ? calculatedAmount
117+ : project . totalAmount ;
118+
61119 return (
62- < div className = "space-y-6 invoice-print-page" >
63- < PageBreadcrumb
64- items = { [
65- { label : 'Dashboard' , href : '/dashboard' } ,
66- { label : 'Invoices' , href : '/dashboard/invoices' } ,
67- ] }
68- currentPage = { `Invoice ${ rawId } ` }
69- />
70-
71- < Link href = "/dashboard/invoices" className = "no-print inline-flex" >
72- < Button variant = "ghost" className = "mb-4" >
73- < ArrowLeft className = "mr-2 h-4 w-4" />
74- Back to Invoices
75- </ Button >
76- </ Link >
120+ < div className = "invoice-print-page space-y-6" >
121+ < div className = "no-print flex items-center justify-between" >
122+ < Link href = "/dashboard/invoices" className = "inline-flex" >
123+ < Button variant = "ghost" className = "mb-4" >
124+ < ArrowLeft className = "mr-2 h-4 w-4" />
125+ Back to Invoices
126+ </ Button >
127+ </ Link >
128+ < div className = "flex gap-2 mb-4" >
129+ < Button variant = "outline" onClick = { ( ) => setShowHistory ( ! showHistory ) } >
130+ < History className = "mr-2 h-4 w-4" />
131+ Version History ({ versionHistory . length } )
132+ </ Button >
133+ { ! isEditing && ! requiresSignature && (
134+ < Button onClick = { ( ) => setIsEditing ( true ) } >
135+ < Pencil className = "mr-2 h-4 w-4" />
136+ Edit Invoice
137+ </ Button >
138+ ) }
139+ </ div >
140+ </ div >
141+
142+ { showHistory && (
143+ < Card className = "border-blue-200 bg-blue-50" >
144+ < CardHeader >
145+ < CardTitle className = "text-base text-blue-800" > Version History</ CardTitle >
146+ </ CardHeader >
147+ < CardContent >
148+ { versionHistory . length === 0 ? (
149+ < p className = "text-sm text-blue-600" > No previous versions yet.</ p >
150+ ) : (
151+ < div className = "space-y-3" >
152+ { versionHistory . map ( ( version , index ) => (
153+ < div key = { index } className = "rounded-lg border border-blue-200 bg-white p-4 text-sm" >
154+ < div className = "flex justify-between" >
155+ < span className = "font-semibold text-slate-700" >
156+ Version { versionHistory . length - index }
157+ </ span >
158+ < span className = "text-slate-500" > { version . signedAt } </ span >
159+ </ div >
160+ < p className = "mt-1 text-slate-600" > Description: { version . workDescription } </ p >
161+ < p className = "text-slate-600" >
162+ Hours: { version . hoursWorked } x Rate: { version . hourlyRate } ={ ' ' }
163+ < strong > { version . calculatedAmount } </ strong >
164+ </ p >
165+ </ div >
166+ ) ) }
167+ </ div >
168+ ) }
169+ </ CardContent >
170+ </ Card >
171+ ) }
172+
173+ { requiresSignature && (
174+ < Card className = "border-yellow-300 bg-yellow-50" >
175+ < CardContent className = "flex items-center justify-between p-4" >
176+ < div className = "flex items-center gap-3" >
177+ < PenLine className = "h-5 w-5 text-yellow-700" />
178+ < div >
179+ < p className = "font-semibold text-yellow-800" > Re-signature Required</ p >
180+ < p className = "text-sm text-yellow-700" >
181+ Invoice was edited. Please confirm and sign to apply changes.
182+ </ p >
183+ </ div >
184+ </ div >
185+ < Button onClick = { handleSign } className = "bg-yellow-600 hover:bg-yellow-700" >
186+ < Check className = "mr-2 h-4 w-4" />
187+ Confirm and Sign
188+ </ Button >
189+ </ CardContent >
190+ </ Card >
191+ ) }
192+
193+ { isEditing && (
194+ < Card className = "border-slate-300" >
195+ < CardHeader >
196+ < CardTitle className = "text-base" > Edit Invoice Details</ CardTitle >
197+ </ CardHeader >
198+ < CardContent className = "space-y-4" >
199+ < div >
200+ < Label > Work Description</ Label >
201+ < Input
202+ className = "mt-1"
203+ value = { editedValues . workDescription }
204+ onChange = { ( e ) =>
205+ setEditedValues ( { ...editedValues , workDescription : e . target . value } )
206+ }
207+ />
208+ </ div >
209+ < div className = "grid grid-cols-2 gap-4" >
210+ < div >
211+ < Label > Hours Worked</ Label >
212+ < Input
213+ className = "mt-1"
214+ type = "number"
215+ min = "0"
216+ value = { editedValues . hoursWorked }
217+ onChange = { ( e ) =>
218+ setEditedValues ( { ...editedValues , hoursWorked : Number ( e . target . value ) } )
219+ }
220+ />
221+ </ div >
222+ < div >
223+ < Label > Hourly Rate ({ project . currency } )</ Label >
224+ < Input
225+ className = "mt-1"
226+ type = "number"
227+ min = "0"
228+ value = { editedValues . hourlyRate }
229+ onChange = { ( e ) =>
230+ setEditedValues ( { ...editedValues , hourlyRate : Number ( e . target . value ) } )
231+ }
232+ />
233+ </ div >
234+ </ div >
235+ { calculatedAmount !== null && (
236+ < div className = "rounded-lg bg-slate-50 p-4" >
237+ < p className = "text-sm text-slate-600" > Recalculated Amount</ p >
238+ < p className = "text-2xl font-bold text-slate-900" >
239+ { calculatedAmount } { project . currency }
240+ </ p >
241+ </ div >
242+ ) }
243+ < div className = "flex gap-2 pt-2" >
244+ < Button onClick = { handleSaveEdits } >
245+ < Check className = "mr-2 h-4 w-4" />
246+ Save Changes
247+ </ Button >
248+ < Button variant = "ghost" onClick = { ( ) => setIsEditing ( false ) } >
249+ < X className = "mr-2 h-4 w-4" />
250+ Cancel
251+ </ Button >
252+ </ div >
253+ </ CardContent >
254+ </ Card >
255+ ) }
77256
78257 < Card className = "invoice-print-card overflow-hidden border border-slate-200 shadow-sm" >
79258 < CardHeader className = "space-y-6 border-b border-slate-200 bg-slate-50/60" >
@@ -102,9 +281,9 @@ export default function InvoiceDetailPage() {
102281 Generated
103282 </ p >
104283 < p className = "mt-2 font-medium text-slate-900" >
105- { formatDateInTimeZone ( generatedAt , timezone ) }
284+ { generatedAt . toLocaleDateString ( ) }
106285 </ p >
107- < p className = "text-xs text-slate-500" > { formatTimeInTimeZone ( generatedAt , timezone ) } </ p >
286+ < p className = "text-xs text-slate-500" > { generatedAt . toLocaleTimeString ( ) } </ p >
108287 </ div >
109288 < div className = "print-break-inside-avoid rounded-xl border border-slate-200 bg-white p-4" >
110289 < p className = "text-xs font-semibold uppercase tracking-wide text-slate-500" >
@@ -126,10 +305,12 @@ export default function InvoiceDetailPage() {
126305 < div className = "print-break-inside-avoid rounded-2xl border border-slate-200 bg-slate-50 p-5 sm:col-span-2" >
127306 < p className = "mb-1 text-sm text-gray-600" > Amount Due</ p >
128307 < p className = "text-3xl font-bold tracking-tight text-slate-900" >
129- { project . totalAmount } { project . currency }
308+ { displayAmount } { project . currency }
130309 </ p >
131310 < p className = "mt-2 text-sm text-slate-500" >
132- Payment for the completed work recorded in AgenticPay.
311+ { isSigned && calculatedAmount
312+ ? editedValues . workDescription
313+ : 'Payment for the completed work recorded in AgenticPay.' }
133314 </ p >
134315 </ div >
135316 < div className = "print-break-inside-avoid rounded-2xl border border-slate-200 p-5" >
@@ -173,17 +354,37 @@ export default function InvoiceDetailPage() {
173354 < div className = "flex items-center justify-between gap-4 px-5 py-4 text-sm" >
174355 < span className = "text-slate-600" > Generated</ span >
175356 < span className = "text-right font-medium text-slate-900" >
176- { formatDateTimeInTimeZone ( generatedAt , timezone ) }
357+ { generatedAt . toLocaleString ( ) }
177358 </ span >
178359 </ div >
179360 < div className = "flex items-center justify-between gap-4 px-5 py-4 text-sm" >
180361 < span className = "text-slate-600" > Work Scope</ span >
181- < span className = "text-right font-medium text-slate-900" > Full Project</ span >
362+ < span className = "text-right font-medium text-slate-900" >
363+ { isSigned && editedValues . workDescription
364+ ? editedValues . workDescription
365+ : 'Full Project' }
366+ </ span >
182367 </ div >
368+ { isSigned && editedValues . hoursWorked > 0 && (
369+ < >
370+ < div className = "flex items-center justify-between gap-4 px-5 py-4 text-sm" >
371+ < span className = "text-slate-600" > Hours Worked</ span >
372+ < span className = "text-right font-medium text-slate-900" >
373+ { editedValues . hoursWorked }
374+ </ span >
375+ </ div >
376+ < div className = "flex items-center justify-between gap-4 px-5 py-4 text-sm" >
377+ < span className = "text-slate-600" > Hourly Rate</ span >
378+ < span className = "text-right font-medium text-slate-900" >
379+ { editedValues . hourlyRate } { project . currency }
380+ </ span >
381+ </ div >
382+ </ >
383+ ) }
183384 < div className = "flex items-center justify-between gap-4 px-5 py-4 text-base" >
184385 < span className = "font-semibold text-slate-900" > Total Due</ span >
185386 < span className = "text-right text-xl font-semibold text-slate-900" >
186- { project . totalAmount } { project . currency }
387+ { displayAmount } { project . currency }
187388 </ span >
188389 </ div >
189390 </ div >
@@ -204,4 +405,4 @@ export default function InvoiceDetailPage() {
204405 </ Card >
205406 </ div >
206407 ) ;
207- }
408+ }
0 commit comments