diff --git a/Desktop.ini b/Desktop.ini new file mode 100644 index 0000000..bb9c4c3 --- /dev/null +++ b/Desktop.ini @@ -0,0 +1,3 @@ +[.ShellClassInfo] +IconFile=C:\Program Files\FolderPainter\Icons\Pack_01\04.ico +IconIndex=0 diff --git a/frontend/app/dashboard/invoices/[id]/page.tsx b/frontend/app/dashboard/invoices/[id]/page.tsx index 193c709..8fa09ae 100644 --- a/frontend/app/dashboard/invoices/[id]/page.tsx +++ b/frontend/app/dashboard/invoices/[id]/page.tsx @@ -4,26 +4,59 @@ import { useParams } from 'next/navigation'; import { useAgenticPay } from '@/lib/hooks/useAgenticPay'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { ArrowLeft, Download, Pencil, X, Check, History, PenLine } from 'lucide-react'; import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb'; import { ArrowLeft, Download } from 'lucide-react'; import Link from 'next/link'; import { Skeleton } from '@/components/ui/skeleton'; -import { - formatDateInTimeZone, - formatDateTimeInTimeZone, - formatTimeInTimeZone, -} from '@/lib/utils'; -import { useAuthStore } from '@/store/useAuthStore'; +import { useState, useEffect } from 'react'; + +interface InvoiceVersion { + timestamp: string; + workDescription: string; + hoursWorked: number; + hourlyRate: number; + calculatedAmount: number; + signedAt: string; +} export default function InvoiceDetailPage() { const params = useParams(); const rawId = params.id as string; const projectId = rawId.startsWith('INV-') ? rawId.replace('INV-', '') : rawId; - const timezone = useAuthStore((state) => state.timezone); const { useProjectDetail } = useAgenticPay(); const { project, loading } = useProjectDetail(projectId); + const [isEditing, setIsEditing] = useState(false); + const [showHistory, setShowHistory] = useState(false); + const [requiresSignature, setRequiresSignature] = useState(false); + const [isSigned, setIsSigned] = useState(false); + const [versionHistory, setVersionHistory] = useState([]); + + const [editedValues, setEditedValues] = useState({ + workDescription: 'Verified work', + hoursWorked: 0, + hourlyRate: 0, + }); + + const calculatedAmount = + editedValues.hoursWorked > 0 && editedValues.hourlyRate > 0 + ? editedValues.hoursWorked * editedValues.hourlyRate + : null; + + useEffect(() => { + if (!rawId) return; + try { + const stored = localStorage.getItem(`invoice-history-${rawId}`); + if (stored) setVersionHistory(JSON.parse(stored)); + } catch { + // ignore + } + }, [rawId]); + if (loading) { return (
@@ -54,26 +87,172 @@ export default function InvoiceDetailPage() { const status = project.status === 'completed' ? 'paid' : 'pending'; const generatedAt = new Date(project.createdAt); - const handlePrint = () => { - window.print(); + const handlePrint = () => window.print(); + + const handleSaveEdits = () => { + setIsEditing(false); + setRequiresSignature(true); + setIsSigned(false); }; + const handleSign = () => { + const newVersion: InvoiceVersion = { + timestamp: new Date().toISOString(), + workDescription: editedValues.workDescription, + hoursWorked: editedValues.hoursWorked, + hourlyRate: editedValues.hourlyRate, + calculatedAmount: calculatedAmount ?? Number(project.totalAmount), + signedAt: new Date().toLocaleString(), + }; + + const updated = [newVersion, ...versionHistory]; + setVersionHistory(updated); + localStorage.setItem(`invoice-history-${rawId}`, JSON.stringify(updated)); + setRequiresSignature(false); + setIsSigned(true); + }; + + const displayAmount = isSigned && calculatedAmount + ? calculatedAmount + : project.totalAmount; + return ( -
- - - - - +
+
+ + + +
+ + {!isEditing && !requiresSignature && ( + + )} +
+
+ + {showHistory && ( + + + Version History + + + {versionHistory.length === 0 ? ( +

No previous versions yet.

+ ) : ( +
+ {versionHistory.map((version, index) => ( +
+
+ + Version {versionHistory.length - index} + + {version.signedAt} +
+

Description: {version.workDescription}

+

+ Hours: {version.hoursWorked} x Rate: {version.hourlyRate} ={' '} + {version.calculatedAmount} +

+
+ ))} +
+ )} +
+
+ )} + + {requiresSignature && ( + + +
+ +
+

Re-signature Required

+

+ Invoice was edited. Please confirm and sign to apply changes. +

+
+
+ +
+
+ )} + + {isEditing && ( + + + Edit Invoice Details + + +
+ + + setEditedValues({ ...editedValues, workDescription: e.target.value }) + } + /> +
+
+
+ + + setEditedValues({ ...editedValues, hoursWorked: Number(e.target.value) }) + } + /> +
+
+ + + setEditedValues({ ...editedValues, hourlyRate: Number(e.target.value) }) + } + /> +
+
+ {calculatedAmount !== null && ( +
+

Recalculated Amount

+

+ {calculatedAmount} {project.currency} +

+
+ )} +
+ + +
+
+
+ )} @@ -102,9 +281,9 @@ export default function InvoiceDetailPage() { Generated

- {formatDateInTimeZone(generatedAt, timezone)} + {generatedAt.toLocaleDateString()}

-

{formatTimeInTimeZone(generatedAt, timezone)}

+

{generatedAt.toLocaleTimeString()}

@@ -126,10 +305,12 @@ export default function InvoiceDetailPage() {

Amount Due

- {project.totalAmount} {project.currency} + {displayAmount} {project.currency}

- Payment for the completed work recorded in AgenticPay. + {isSigned && calculatedAmount + ? editedValues.workDescription + : 'Payment for the completed work recorded in AgenticPay.'}

@@ -173,17 +354,37 @@ export default function InvoiceDetailPage() {
Generated - {formatDateTimeInTimeZone(generatedAt, timezone)} + {generatedAt.toLocaleString()}
Work Scope - Full Project + + {isSigned && editedValues.workDescription + ? editedValues.workDescription + : 'Full Project'} +
+ {isSigned && editedValues.hoursWorked > 0 && ( + <> +
+ Hours Worked + + {editedValues.hoursWorked} + +
+
+ Hourly Rate + + {editedValues.hourlyRate} {project.currency} + +
+ + )}
Total Due - {project.totalAmount} {project.currency} + {displayAmount} {project.currency}
@@ -204,4 +405,4 @@ export default function InvoiceDetailPage() {
); -} +} \ No newline at end of file diff --git a/frontend/components/layout/Header.tsx b/frontend/components/layout/Header.tsx index 9923ba2..83788ae 100644 --- a/frontend/components/layout/Header.tsx +++ b/frontend/components/layout/Header.tsx @@ -75,9 +75,16 @@ export function Header() { const { name, email, address, timezone, logout, setTimezone } = useAuthStore(); const { isDark, mode, setIsDark } = useThemeStore(); const { disconnect } = useDisconnect(); - const { isOnline, queueLength, isSyncing } = useOfflineStatus(); const router = useRouter(); const pathname = usePathname(); + const [breadcrumbs, setBreadcrumbs] = useState([]); + const [themeSettingsOpen, setThemeSettingsOpen] = useState(false); + const [timezoneSettingsOpen, setTimezoneSettingsOpen] = useState(false); + + useEffect(() => { + const items = getDashboardBreadcrumbs(pathname); + setBreadcrumbs(items); + }, [pathname]); const breadcrumbs = getDashboardBreadcrumbs(pathname); @@ -123,6 +130,8 @@ export function Header() {
+ +
{(!isOnline || queueLength > 0 || isSyncing) && (
@@ -170,6 +179,20 @@ export function Header() { variant="ghost" size="icon" onClick={mode === 'manual' ? handleManualToggle : undefined} + title={ + mode === 'manual' + ? isDark + ? 'Switch to light mode' + : 'Switch to dark mode' + : `Auto: ${mode} mode` + } + className="relative" + > + {isDark ? ( + + ) : ( + + )} className="relative" > {isDark ? : } @@ -180,6 +203,12 @@ export function Header() { )} + @@ -193,6 +222,9 @@ export function Header() {
+

+ {name || 'User'} +

{name || 'User'}

{shortAddress}

@@ -207,6 +239,18 @@ export function Header() {
+ + + Profile + + setTimezoneSettingsOpen(true)}> + + Timezone Settings + + + + + Logout Profile setTimezoneSettingsOpen(true)}> Timezone Settings @@ -240,6 +284,7 @@ export function Header() {
)} + setThemeSettingsOpen(false)} />