From c460108fba724b80cad9dbd8f7a321a6cff0173e Mon Sep 17 00:00:00 2001 From: Adams Okwori Date: Sat, 28 Mar 2026 06:46:01 +0100 Subject: [PATCH 1/3] feat: add advanced invoice editing with version history and re-signature --- frontend/app/dashboard/invoices/[id]/page.tsx | 243 +++++++++++++++- frontend/components/layout/Header.tsx | 270 ++++++------------ frontend/package-lock.json | 34 +-- 3 files changed, 312 insertions(+), 235 deletions(-) diff --git a/frontend/app/dashboard/invoices/[id]/page.tsx b/frontend/app/dashboard/invoices/[id]/page.tsx index 13ce1a4..b52871f 100644 --- a/frontend/app/dashboard/invoices/[id]/page.tsx +++ b/frontend/app/dashboard/invoices/[id]/page.tsx @@ -4,9 +4,21 @@ 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 { ArrowLeft, Download } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { ArrowLeft, Download, Pencil, X, Check, History, PenLine } from 'lucide-react'; import Link from 'next/link'; import { Skeleton } from '@/components/ui/skeleton'; +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(); @@ -16,6 +28,33 @@ export default function InvoiceDetailPage() { 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 (
@@ -46,18 +85,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} +

+
+ )} +
+ + +
+
+
+ )} @@ -110,10 +303,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.'}

@@ -162,12 +357,32 @@ export default function InvoiceDetailPage() {
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}
@@ -188,4 +403,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 7a0d17d..1ce1f38 100644 --- a/frontend/components/layout/Header.tsx +++ b/frontend/components/layout/Header.tsx @@ -16,57 +16,15 @@ import { import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Bell, LogOut, User, Settings, Sun, Moon, Clock } from 'lucide-react'; import { toast } from 'sonner'; -<<<<<<< feat/qr-code - -======= ->>>>>>> main -// 1. I added useNetwork to the existing wagmi import import { useDisconnect, useAccount } from 'wagmi'; import { web3auth } from '@/lib/web3auth'; import { ThemeSettingsModal } from '@/components/theme/ThemeSettingsModal'; -// 2. I built the isolated NetworkIndicator component right here -const NetworkIndicator = () => { - // We use useAccount() in Wagmi v2 instead of useNetwork() - const { chain, isConnected } = useAccount(); - - // Hide it if the wallet isn't connected yet - if (!isConnected) return null; - - // In v2, if connected but 'chain' is undefined, it means they are on an unsupported network - if (!chain) { - return ( -
- - Wrong Network -
- ); - } - - // Handle Mainnet (Green) vs Testnet (Yellow) states - const isTestnet = chain.testnet === true; - const bgColor = isTestnet - ? 'bg-yellow-100 text-yellow-800 border-yellow-200' - : 'bg-green-100 text-green-800 border-green-200'; - const dotColor = isTestnet ? 'bg-yellow-500' : 'bg-green-500'; - - return ( -
- - {chain.name} -
- ); -}; - -// 2. I built the isolated NetworkIndicator component right here const NetworkIndicator = () => { - // We use useAccount() in Wagmi v2 instead of useNetwork() const { chain, isConnected } = useAccount(); - // Hide it if the wallet isn't connected yet if (!isConnected) return null; - // In v2, if connected but 'chain' is undefined, it means they are on an unsupported network if (!chain) { return (
@@ -76,10 +34,9 @@ const NetworkIndicator = () => { ); } - // Handle Mainnet (Green) vs Testnet (Yellow) states const isTestnet = chain.testnet === true; - const bgColor = isTestnet - ? 'bg-yellow-100 text-yellow-800 border-yellow-200' + const bgColor = isTestnet + ? 'bg-yellow-100 text-yellow-800 border-yellow-200' : 'bg-green-100 text-green-800 border-green-200'; const dotColor = isTestnet ? 'bg-yellow-500' : 'bg-green-500'; @@ -111,7 +68,6 @@ export function Header() { const handleManualToggle = () => { const next = !isDark; setIsDark(next); - // Apply to DOM immediately (ThemeProvider's effect will also fire) document.documentElement.classList.toggle('dark', next); }; @@ -137,157 +93,95 @@ export function Header() {
-
- - {/* 3. I dropped the new component right here! */} - - -
- {/* Notifications */} - - - {/* Dark mode toggle — only interactive label when manual */} - +
+ -<<<<<<< feat/qr-code -
- - {/* 3. I dropped the new component right here! */} - +
+ - {/* Notifications */} - + - {/* User menu */} - - - - - - -
-

{name || 'User'}

-

{email || 'No email'}

-

{shortAddress}

-
-
- - - - Profile - - - - Settings - - - - - Logout - -
-
-======= - {/* Theme schedule settings */} - - {/* User menu */} - - - - - - -
-

{name || 'User'}

-

{email || 'No email'}

-

{shortAddress}

-
-
- - - - Profile - - - - Settings - - - - - Logout - -
-
+ + + + + + +
+

{name || 'User'}

+

{email || 'No email'}

+

{shortAddress}

+
+
+ + + + Profile + + + + Settings + + + + + Logout + +
+
+
->>>>>>> main
setThemeSettingsOpen(false)} /> ); -<<<<<<< feat/qr-code -} -======= -} ->>>>>>> main +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7707d62..44431b7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,6 +29,7 @@ "lucide-react": "^0.562.0", "next": "^16.2.1", "next-themes": "^0.4.6", + "qrcode.react": "^4.2.0", "react": "^19.2.3", "react-day-picker": "^9.13.0", "react-dom": "^19.2.3", @@ -2054,9 +2055,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -9711,23 +9709,6 @@ "node": ">= 6" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/framer-motion": { "version": "12.23.26", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", @@ -10208,19 +10189,6 @@ "node": ">=0.10.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/idb-keyval": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", From 5d1035eb8aef2fc116836908fec8555b49648f6d Mon Sep 17 00:00:00 2001 From: Adams Okwori Date: Sat, 28 Mar 2026 07:04:58 +0100 Subject: [PATCH 2/3] fix: resolve merge conflicts in invoice page and header --- Desktop.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Desktop.ini 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 From 44e1dc04bff542ce7a079ac5f91cafd8d151519e Mon Sep 17 00:00:00 2001 From: Adams Okwori Date: Sun, 29 Mar 2026 18:55:11 +0100 Subject: [PATCH 3/3] Trigger CI rerun