Skip to content

Commit 04b5df1

Browse files
authored
Merge pull request #167 from 5elc0uth/feat/73-advanced-invoice-editing
feat: add advanced invoice editing with version history and re-signature
2 parents 2239576 + 44e1dc0 commit 04b5df1

File tree

3 files changed

+282
-33
lines changed

3 files changed

+282
-33
lines changed

Desktop.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[.ShellClassInfo]
2+
IconFile=C:\Program Files\FolderPainter\Icons\Pack_01\04.ico
3+
IconIndex=0

frontend/app/dashboard/invoices/[id]/page.tsx

Lines changed: 233 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,59 @@ import { useParams } from 'next/navigation';
44
import { useAgenticPay } from '@/lib/hooks/useAgenticPay';
55
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
66
import { 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';
710
import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb';
811
import { ArrowLeft, Download } from 'lucide-react';
912
import Link from 'next/link';
1013
import { 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

1825
export 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

Comments
 (0)