diff --git a/app/(app)/apps/invoices/components/invoice-generator.tsx b/app/(app)/apps/invoices/components/invoice-generator.tsx index 4ee985d..049798b 100644 --- a/app/(app)/apps/invoices/components/invoice-generator.tsx +++ b/app/(app)/apps/invoices/components/invoice-generator.tsx @@ -25,7 +25,13 @@ function invoiceFormReducer(state: InvoiceFormData, action: any): InvoiceFormDat case "UPDATE_FIELD": return { ...state, [action.field]: action.value } case "ADD_ITEM": - return { ...state, items: [...state.items, { description: "", quantity: 1, unitPrice: 0, subtotal: 0 }] } + return { + ...state, + items: [ + ...state.items, + { name: "", subtitle: "", showSubtitle: false, quantity: 1, unitPrice: 0, subtotal: 0 }, + ], + } case "UPDATE_ITEM": { const items = [...state.items] items[action.index] = { ...items[action.index], [action.field]: action.value } @@ -146,7 +152,6 @@ export function InvoiceGenerator({ } try { - // Get existing templates const result = await addNewTemplateAction(user, { id: `tmpl_${Math.random().toString(36).substring(2, 15)}`, name: newTemplateName, diff --git a/app/(app)/apps/invoices/components/invoice-page.tsx b/app/(app)/apps/invoices/components/invoice-page.tsx index c204b75..bd77a9e 100644 --- a/app/(app)/apps/invoices/components/invoice-page.tsx +++ b/app/(app)/apps/invoices/components/invoice-page.tsx @@ -7,7 +7,9 @@ import { X } from "lucide-react" import { InputHTMLAttributes, memo, useCallback, useMemo } from "react" export interface InvoiceItem { - description: string + name: string + subtitle: string + showSubtitle: boolean quantity: number unitPrice: number subtotal: number @@ -67,49 +69,92 @@ const ItemRow = memo(function ItemRow({ }: { item: InvoiceItem index: number - onChange: (index: number, field: keyof InvoiceItem, value: string | number) => void + onChange: (index: number, field: keyof InvoiceItem, value: string | number | boolean) => void onRemove: (index: number) => void currency: string }) { return ( - - - onChange(index, "description", e.target.value)} - className="w-full min-w-64" - placeholder="Item description" - required - /> - - - onChange(index, "quantity", Number(e.target.value))} - className="w-20 text-right" - required - /> - - - onChange(index, "unitPrice", Number(e.target.value))} - className="w-24 text-right" - required - /> - - {formatCurrency(item.subtotal * 100, currency)} - +
+ {/* Mobile view label (visible only on small screens) */} +
+ Item - - +
+ + {/* Item name and subtitle */} +
+
+ onChange(index, "name", e.target.value)} + className="w-full min-w-0 font-semibold" + placeholder="Item name" + required + /> +
+ {!item.showSubtitle ? ( + + ) : ( + onChange(index, "subtitle", e.target.value)} + className="w-full mt-1 text-xs text-muted-foreground" + placeholder="Detailed description (optional)" + /> + )} +
+
+
+ + {/* Mobile labels for small screens */} +
+
Quantity
+
Unit Price
+
Subtotal
+
+ + {/* Quantity, Unit Price, Subtotal, and Remove button */} +
+
+ onChange(index, "quantity", Number(e.target.value))} + className="w-full text-right" + required + /> +
+
+ onChange(index, "unitPrice", Number(e.target.value))} + className="w-full text-right" + required + /> +
+
+ {formatCurrency(item.subtotal * 100, currency)} +
+
+ +
+
+
) }) @@ -196,7 +241,7 @@ export function InvoicePage({ invoiceData, dispatch, currencies }: InvoicePagePr const addItem = useCallback(() => dispatch({ type: "ADD_ITEM" }), [dispatch]) const removeItem = useCallback((index: number) => dispatch({ type: "REMOVE_ITEM", index }), [dispatch]) const updateItem = useCallback( - (index: number, field: keyof InvoiceItem, value: string | number) => + (index: number, field: keyof InvoiceItem, value: string | number | boolean) => dispatch({ type: "UPDATE_ITEM", index, field, value }), [dispatch] ) @@ -350,125 +395,60 @@ export function InvoicePage({ invoiceData, dispatch, currencies }: InvoicePagePr - {/* Items Table */} + {/* Items Section - Refactored to use only flex divs */}
- {/* Table for desktop/tablet */} -
- - - - - - - - - - - - {invoiceData.items.map((item, index) => ( - - ))} - -
- dispatch({ type: "UPDATE_FIELD", field: "itemLabel", value: e.target.value })} - className="text-xs font-medium text-gray-500 uppercase tracking-wider" - /> - - - dispatch({ type: "UPDATE_FIELD", field: "quantityLabel", value: e.target.value }) - } - className="text-xs font-medium text-gray-500 uppercase tracking-wider" - /> - - - dispatch({ type: "UPDATE_FIELD", field: "unitPriceLabel", value: e.target.value }) - } - className="text-xs font-medium text-gray-500 uppercase tracking-wider" - /> - - - dispatch({ type: "UPDATE_FIELD", field: "subtotalLabel", value: e.target.value }) - } - className="text-xs font-medium text-gray-500 uppercase tracking-wider" - /> -
+ {/* Header row for column titles */} +
+
+ dispatch({ type: "UPDATE_FIELD", field: "itemLabel", value: e.target.value })} + className="text-xs font-medium text-gray-500 uppercase tracking-wider" + /> +
+
+ dispatch({ type: "UPDATE_FIELD", field: "quantityLabel", value: e.target.value })} + className="text-xs font-medium text-gray-500 uppercase tracking-wider text-right w-full" + /> +
+
+ dispatch({ type: "UPDATE_FIELD", field: "unitPriceLabel", value: e.target.value })} + className="text-xs font-medium text-gray-500 uppercase tracking-wider text-right w-full" + /> +
+
+ dispatch({ type: "UPDATE_FIELD", field: "subtotalLabel", value: e.target.value })} + className="text-xs font-medium text-gray-500 uppercase tracking-wider text-right w-full" + /> +
+
- {/* Flex list for mobile */} -
+ + {/* Invoice items */} +
{invoiceData.items.map((item, index) => ( -
- -
- - updateItem(index, "description", e.target.value)} - className="w-full min-w-0" - placeholder="Item description" - required - /> -
-
-
- - updateItem(index, "quantity", Number(e.target.value))} - className="w-full text-right" - required - /> -
-
- - updateItem(index, "unitPrice", Number(e.target.value))} - className="w-full text-right" - required - /> -
-
- - - {formatCurrency(item.subtotal * 100, invoiceData.currency)} - -
-
-
+ ))}
+ diff --git a/app/(app)/apps/invoices/components/invoice-pdf.tsx b/app/(app)/apps/invoices/components/invoice-pdf.tsx index b409656..dc618f9 100644 --- a/app/(app)/apps/invoices/components/invoice-pdf.tsx +++ b/app/(app)/apps/invoices/components/invoice-pdf.tsx @@ -210,6 +210,14 @@ const styles = StyleSheet.create({ fontSize: 12, color: "#000000", }, + colName: { + fontWeight: "semibold", + }, + itemSubtitle: { + fontSize: 10, + color: "#6B7280", + marginTop: 2, + }, notes: { marginBottom: 30, fontSize: 12, @@ -342,7 +350,10 @@ export function InvoicePDF({ data }: { data: InvoiceFormData }): ReactElement { {data.items.map((item: InvoiceItem, index: number) => ( - {item.description} + + {item.name} + {item.showSubtitle && item.subtitle && {item.subtitle}} + {item.quantity} {formatCurrency(item.unitPrice * 100, data.currency)} diff --git a/app/(app)/apps/invoices/default-templates.ts b/app/(app)/apps/invoices/default-templates.ts index fcbacf2..a473852 100644 --- a/app/(app)/apps/invoices/default-templates.ts +++ b/app/(app)/apps/invoices/default-templates.ts @@ -21,7 +21,7 @@ export default function defaultTemplates(user: User, settings: SettingsMap): Inv companyDetailsLabel: "Bill From", billTo: "", billToLabel: "Bill To", - items: [{ description: "", quantity: 1, unitPrice: 0, subtotal: 0 }], + items: [{ name: "", subtitle: "", showSubtitle: false, quantity: 1, unitPrice: 0, subtotal: 0 }], taxIncluded: true, additionalTaxes: [{ name: "VAT", rate: 0, amount: 0 }], additionalFees: [], @@ -48,7 +48,7 @@ export default function defaultTemplates(user: User, settings: SettingsMap): Inv companyDetailsLabel: "Rechnungssteller", billTo: "", billToLabel: "Rechnungsempfänger", - items: [{ description: "", quantity: 1, unitPrice: 0, subtotal: 0 }], + items: [{ name: "", subtitle: "", showSubtitle: false, quantity: 1, unitPrice: 0, subtotal: 0 }], taxIncluded: true, additionalTaxes: [{ name: "MwSt", rate: 19, amount: 0 }], additionalFees: [],