From f7b46986c5c48f2499a252390a8875e46961a78d Mon Sep 17 00:00:00 2001 From: davidels1 <87242587+davidels1@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:55:42 +0200 Subject: [PATCH] Add GBP cost price to stock and purchase orders Introduces a new 'cost_price_gbp' field to the stock items schema and database migration. Updates stock item forms to support both ZAR and GBP cost prices. Purchase order forms and PDF generation now use GBP pricing, with enhanced supplier and order metadata. Adds a searchable stock item selector for purchase orders and improves type definitions and UI labels for clarity. --- .../1760602672_add_cost_price_gbp.js | 27 ++ .../PurchaseOrderDataTable.tsx | 335 ++++++++++++------ .../purchase-orders/PurchaseOrderForm.tsx | 168 +++++++-- .../purchase-orders/StockItemSearch.tsx | 108 ++++++ src/components/purchase-orders/columns.tsx | 12 +- src/components/stock/AddItemForm.tsx | 54 ++- src/components/stock/EditItemForm.tsx | 52 ++- src/components/stock/columns.tsx | 1 + src/pages/PurchaseOrders.tsx | 15 +- src/pages/Stock.tsx | 1 + 10 files changed, 605 insertions(+), 168 deletions(-) create mode 100644 pocketbase/pb_migrations/1760602672_add_cost_price_gbp.js create mode 100644 src/components/purchase-orders/StockItemSearch.tsx diff --git a/pocketbase/pb_migrations/1760602672_add_cost_price_gbp.js b/pocketbase/pb_migrations/1760602672_add_cost_price_gbp.js new file mode 100644 index 0000000..580de87 --- /dev/null +++ b/pocketbase/pb_migrations/1760602672_add_cost_price_gbp.js @@ -0,0 +1,27 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2382354120"); + + // Add cost_price_gbp field + collection.fields.addAt(6, new Field({ + "hidden": false, + "id": "", + "max": null, + "min": 0, + "name": "cost_price_gbp", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2382354120"); + + // Remove cost_price_gbp field + collection.fields.removeById("number_cost_price_gbp"); + + return app.save(collection); +}); diff --git a/src/components/purchase-orders/PurchaseOrderDataTable.tsx b/src/components/purchase-orders/PurchaseOrderDataTable.tsx index 11f504e..e3dfe92 100644 --- a/src/components/purchase-orders/PurchaseOrderDataTable.tsx +++ b/src/components/purchase-orders/PurchaseOrderDataTable.tsx @@ -87,126 +87,259 @@ export function PurchaseOrderDataTable({ } const generatePDF = (order: PurchaseOrder) => { - const doc = new jsPDF(); + try { + console.log("Generating PDF for order:", order); + const doc = new jsPDF(); - // Set font and colors - doc.setFont("helvetica"); - const primaryColor = "#ffc905"; // Your primary color - const textColor = "#333333"; + // Modern color scheme - Changed to black + const brandBlack = "#000000"; // Changed from purple to black + const accentYellow = "#ffc905"; // Accent color + const darkText = "#1a1a1a"; + const lightGray = "#f5f5f5"; + const mediumGray = "#666666"; - // --- Company Details --- - const companyName = "JEI SA"; - const companyAddress = "47 Station St, Oberholzer, Carletonville, 2499"; - const companyPhone = "018 786 2897"; + // Create branded logo box + doc.setFillColor(brandBlack); + doc.roundedRect(14, 10, 20, 20, 3, 3, 'F'); + doc.setFont("helvetica", "bold"); + doc.setFontSize(16); + doc.setTextColor(255, 255, 255); + doc.text("JEI", 24, 23, { align: "center" }); // --- Modern Header with black background --- + doc.setFillColor(brandBlack); + doc.rect(0, 0, 210, 45, 'F'); + + doc.setFillColor(accentYellow); + doc.rect(0, 45, 210, 2, 'F'); // Accent line - // --- Header Section --- - doc.setFontSize(24); - doc.setTextColor(primaryColor); - doc.text("PURCHASE ORDER", 105, 20, { align: "center" }); + // Company name and details + doc.setFont("helvetica", "bold"); + doc.setFontSize(20); + doc.setTextColor(255, 255, 255); + doc.text("JEI SA", 40, 20); + + doc.setFont("helvetica", "normal"); + doc.setFontSize(9); + doc.setTextColor(220, 220, 220); + doc.text("47 Station St, Oberholzer", 40, 26); + doc.text("Carletonville, 2499", 40, 31); + doc.text("Tel: 018 786 2897", 40, 36); - doc.setFontSize(10); - doc.setTextColor(textColor); - doc.text(companyName, 14, 30); // Company Name - doc.text(companyAddress, 14, 35); - doc.text(companyPhone, 14, 40); + // Purchase Order title - positioned to the right + doc.setFont("helvetica", "bold"); + doc.setFontSize(24); + doc.setTextColor(255, 255, 255); + doc.text("PURCHASE ORDER", 120, 25); - doc.setFontSize(10); - doc.text(`PO Number: ${order.id}`, 150, 30); - doc.text(`Date: ${new Date(order.orderDate).toLocaleDateString()}`, 150, 35); - doc.text(`Status: ${order.status}`, 150, 40); + // --- PO Details Box (Top Right) - Extended for more fields --- + doc.setFillColor(250, 250, 250); + doc.setDrawColor(200, 200, 200); + doc.roundedRect(125, 55, 71, 44, 2, 2, 'FD'); // Increased height for more fields + + doc.setFont("helvetica", "bold"); + doc.setFontSize(9); + doc.setTextColor(mediumGray); + doc.text("PO Number:", 128, 61); + doc.text("Date:", 128, 69); + doc.text("Account Ref:", 128, 77); + doc.text("Sales Order No:", 128, 85); + doc.text("Status:", 128, 93); + + doc.setFont("helvetica", "normal"); + doc.setTextColor(darkText); + doc.text(order.orderNumber || order.id, 193, 61, { align: "right" }); + doc.text(new Date(order.orderDate).toLocaleDateString(), 193, 69, { align: "right" }); + doc.text(order.accountRef || 'N/A', 193, 77, { align: "right" }); + doc.text(order.salesOrderNo || 'N/A', 193, 85, { align: "right" }); + + // Status badge + const statusColors: Record = { + 'Pending': '#FFA500', + 'Ordered': '#4CAF50', + 'Received': '#2196F3' + }; + const statusColor = statusColors[order.status] || '#999999'; + doc.setFillColor(statusColor); + doc.setTextColor(255, 255, 255); + doc.setFont("helvetica", "bold"); + const statusWidth = doc.getTextWidth(order.status) + 4; + doc.roundedRect(196 - statusWidth, 89, statusWidth, 6, 1, 1, 'F'); + doc.text(order.status, 194, 93, { align: "right" }); - // --- Supplier Details --- - doc.setFontSize(12); - doc.setTextColor(primaryColor); - doc.text("SUPPLIER DETAILS", 14, 65); - doc.setDrawColor(primaryColor); - doc.line(14, 66, 50, 66); // Underline + // --- Supplier Details Box --- + let y = 95; + doc.setFont("helvetica", "bold"); + doc.setFontSize(11); + doc.setTextColor(brandBlack); + doc.text("SUPPLIER INFORMATION", 14, y); + + doc.setDrawColor(accentYellow); + doc.setLineWidth(1.5); + doc.line(14, y + 2, 75, y + 2); - doc.setFontSize(10); - doc.setTextColor(textColor); - doc.text(`Supplier Name: ${order.supplierName}`, 14, 75); - // Add more supplier details if available in the future - doc.text("Address: Supplier Address Line 1", 14, 80); - doc.text("City, Postal Code", 14, 85); - doc.text("Contact: Supplier Contact Person", 14, 90); - doc.text("Email: supplier@example.com", 14, 95); + y += 10; + doc.setFillColor(lightGray); + doc.roundedRect(14, y, 182, 38, 2, 2, 'F'); // Increased height for supplier info + + y += 6; + doc.setFont("helvetica", "bold"); + doc.setFontSize(10); + doc.setTextColor(darkText); + + // Use actual supplier data from expanded relation + const supplierName = order.supplier?.name || "N/A"; + doc.text(supplierName, 18, y); + + y += 6; + doc.setFont("helvetica", "normal"); + doc.setFontSize(8); + doc.setTextColor(mediumGray); + + if (order.supplier?.address) { + // Wrap long address text + const addressLines = doc.splitTextToSize(order.supplier.address, 160); + doc.text(addressLines, 18, y); + y += (addressLines.length * 4) + 2; + } + if (order.supplier?.email) { + doc.text(`Email: ${order.supplier.email}`, 18, y); + y += 5; + } + if (order.supplier?.phone) { + doc.text(`Tel: ${order.supplier.phone}`, 18, y); + } - // --- Items Table --- - let y = 110; - doc.setFontSize(12); - doc.setTextColor(primaryColor); - doc.text("ORDER ITEMS", 14, y); - doc.setDrawColor(primaryColor); - doc.line(14, y + 1, 45, y + 1); // Underline + // --- Items Table --- + y += 12; + doc.setFont("helvetica", "bold"); + doc.setFontSize(11); + doc.setTextColor(brandBlack); + doc.text("ORDER ITEMS", 14, y); + + doc.setDrawColor(accentYellow); + doc.setLineWidth(1.5); + doc.line(14, y + 2, 50, y + 2); - y += 10; // Space after title - - // Table Headers - doc.setFillColor(primaryColor); - doc.rect(14, y, 182, 8, "F"); // Background for header - doc.setTextColor("#FFFFFF"); // White text for header - doc.setFontSize(10); - doc.text("Item #", 16, y + 5); - doc.text("Description (SKU)", 40, y + 5); - doc.text("Qty", 130, y + 5); - doc.text("Unit Price", 150, y + 5); - doc.text("Total", 180, y + 5); - - y += 8; // Move past header - - doc.setTextColor(textColor); - doc.setFontSize(9); - let subtotal = 0; - - order.items.forEach((item, index) => { - const orderedQuantity = order.quantities[index] || 0; - const unitPrice = item.costPrice || 0; // Use costPrice from the item - const itemTotal = orderedQuantity * unitPrice; - subtotal += itemTotal; - - doc.text(String(index + 1), 16, y + 5); - doc.text(`${item.itemName} (${item.sku})`, 40, y + 5); - doc.text(String(orderedQuantity), 130, y + 5); - doc.text(unitPrice.toLocaleString("en-ZA", { style: "currency", currency: "ZAR" }), 150, y + 5); - doc.text(itemTotal.toLocaleString("en-ZA", { style: "currency", currency: "ZAR" }), 180, y + 5); y += 8; - }); - // --- Totals Section --- - y += 10; // Space before totals + // Modern table header + doc.setFillColor(brandBlack); + doc.roundedRect(14, y, 182, 10, 1, 1, 'F'); + + doc.setFont("helvetica", "bold"); + doc.setFontSize(9); + doc.setTextColor(255, 255, 255); + doc.text("#", 17, y + 6); + doc.text("Description", 28, y + 6); + doc.text("SKU", 100, y + 6); + doc.text("Qty", 130, y + 6, { align: "center" }); + doc.text("Unit Price", 155, y + 6, { align: "right" }); + doc.text("Total", 188, y + 6, { align: "right" }); - doc.setDrawColor(primaryColor); - doc.line(140, y, 196, y); // Line above subtotal + y += 10; + + // Table rows with alternating colors and proper text wrapping + doc.setFont("helvetica", "normal"); + doc.setFontSize(8); + let subtotal = 0; + let rowColor = true; - y += 5; - doc.setFontSize(10); - doc.text("Subtotal:", 140, y); - doc.text(subtotal.toLocaleString("en-ZA", { style: "currency", currency: "ZAR" }), 180, y); + order.items.forEach((item, index) => { + const orderedQuantity = order.quantities[index] || 0; + const unitPriceGBP = item.costPriceGBP || 0; // Use GBP price + const itemTotal = orderedQuantity * unitPriceGBP; + subtotal += itemTotal; - y += 7; - doc.text("Shipping:", 140, y); - doc.text("R0.00", 180, y); // Placeholder for shipping + // Calculate row height based on description length + const maxDescWidth = 65; + const wrappedDesc = doc.splitTextToSize(item.itemName, maxDescWidth); + const rowHeight = Math.max(10, wrappedDesc.length * 5 + 5); // Dynamic height - y += 7; - doc.text("Tax:", 140, y); - doc.text("R0.00", 180, y); // Placeholder for tax + // Alternating row background + if (rowColor) { + doc.setFillColor(250, 250, 250); + doc.rect(14, y, 182, rowHeight, 'F'); + } + rowColor = !rowColor; - y += 7; - doc.text("Other:", 140, y); - doc.text("R0.00", 180, y); // Placeholder for other charges + doc.setTextColor(darkText); + doc.text(String(index + 1), 17, y + 5); + + // Wrap long descriptions - allow multiple lines + doc.text(wrappedDesc, 28, y + 5); + + doc.setTextColor(mediumGray); + doc.setFontSize(7); + doc.text(item.sku, 100, y + 5); + doc.setFontSize(8); + + doc.setTextColor(darkText); + doc.text(String(orderedQuantity), 130, y + 5, { align: "center" }); + doc.text(`£${unitPriceGBP.toFixed(2)}`, 155, y + 5, { align: "right" }); + doc.text(`£${itemTotal.toFixed(2)}`, 188, y + 5, { align: "right" }); + + y += rowHeight; + }); - y += 10; - doc.setDrawColor(primaryColor); - doc.setLineWidth(0.5); - doc.line(140, y, 196, y); // Thicker line above total + // --- Modern Totals Section --- + y += 5; + + const carriageCharge = order.carriageCharge || 0; + const grandTotal = subtotal + carriageCharge; + + // Totals box - increased height to accommodate carriage charge + doc.setFillColor(lightGray); + doc.roundedRect(130, y, 66, 38, 2, 2, 'F'); + + y += 7; + doc.setFont("helvetica", "normal"); + doc.setFontSize(9); + doc.setTextColor(mediumGray); + doc.text("Subtotal:", 135, y); + doc.setTextColor(darkText); + doc.text(`£${subtotal.toFixed(2)}`, 188, y, { align: "right" }); - y += 5; - doc.setFontSize(14); - doc.setTextColor(primaryColor); - doc.text("TOTAL:", 140, y); - doc.text(subtotal.toLocaleString("en-ZA", { style: "currency", currency: "ZAR" }), 180, y); // Grand Total + y += 6; + doc.setTextColor(mediumGray); + doc.text("Carriage Charge:", 135, y); + doc.setTextColor(darkText); + doc.text(`£${carriageCharge.toFixed(2)}`, 188, y, { align: "right" }); - doc.save(`purchase_order_${order.id}.pdf`); + y += 6; + doc.setTextColor(mediumGray); + doc.text("Tax (VAT):", 135, y); + doc.setTextColor(darkText); + doc.text("£0.00", 188, y, { align: "right" }); + + y += 8; + + // Grand total highlight + doc.setFillColor(brandBlack); + doc.roundedRect(130, y - 3, 66, 10, 2, 2, 'F'); + + doc.setFont("helvetica", "bold"); + doc.setFontSize(11); + doc.setTextColor(255, 255, 255); + doc.text("GRAND TOTAL:", 135, y + 4); + doc.text(`£${grandTotal.toFixed(2)}`, 188, y + 4, { align: "right" }); + + // --- Footer (with more spacing from grand total) --- + y += 25; // Add more space between grand total and footer + doc.setFont("helvetica", "italic"); + doc.setFontSize(8); + doc.setTextColor(mediumGray); + doc.text("Thank you for your business!", 105, y, { align: "center" }); + doc.text("This is a computer-generated document and does not require a signature.", 105, y + 5, { align: "center" }); + + console.log("Saving PDF..."); + doc.save(`PO_${order.id}_${new Date().toISOString().split('T')[0]}.pdf`); + console.log("PDF saved successfully!"); + } catch (error: any) { + console.error("Error generating PDF:", error); + console.error("Error message:", error?.message); + console.error("Error stack:", error?.stack); + console.error("Order data:", order); + alert(`Failed to generate PDF: ${error?.message || 'Unknown error'}. Please check the console for details.`); + } }; const approveOrder = async (order: PurchaseOrder) => { diff --git a/src/components/purchase-orders/PurchaseOrderForm.tsx b/src/components/purchase-orders/PurchaseOrderForm.tsx index 0898c7e..965e4a2 100644 --- a/src/components/purchase-orders/PurchaseOrderForm.tsx +++ b/src/components/purchase-orders/PurchaseOrderForm.tsx @@ -25,6 +25,7 @@ import { } from "@/components/ui/select" // Import Select components import { showSuccess, showError } from "@/utils/toast" import pb from "@/lib/pocketbase" +import { StockItemSearch } from "./StockItemSearch" // Define the schema for a single item in the purchase order const itemSchema = z.object({ @@ -40,7 +41,11 @@ type ItemSchemaType = z.infer; // Define the schema for the purchase order form const purchaseOrderFormSchema = z.object({ + orderNumber: z.string().min(1, { message: "Order number is required." }), supplierName: z.string().min(1, { message: "Supplier name is required." }), + accountRef: z.string().optional(), + salesOrderNo: z.string().optional(), + carriageCharge: z.number().min(0).optional(), orderDate: z.date({ required_error: "Order date is required." }), items: z.array(itemSchema).min(1, { message: "At least one item is required." }), }) @@ -62,11 +67,43 @@ type StockItem = { name: string sku: string costPrice: number + costPriceGBP: number } export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormProps) { const [suppliers, setSuppliers] = useState([]) // State to hold suppliers const [stockItems, setStockItems] = useState([]) // State to hold stock items + const [orderNumber, setOrderNumber] = useState("") // State for unique order number + + // Generate unique order number + const generateOrderNumber = useCallback(async () => { + try { + // Get the latest purchase order to determine next number + const records = await pb.collection('purchase_orders').getList(1, 1, { + sort: '-created', + }); + + // Generate order number in format: PO-YYYYMMDD-XXX + const today = new Date(); + const dateStr = today.toISOString().split('T')[0].replace(/-/g, ''); + + // Get today's orders to increment counter + const todayOrders = await pb.collection('purchase_orders').getList(1, 100, { + filter: `created >= "${today.toISOString().split('T')[0]} 00:00:00"`, + }); + + const counter = todayOrders.items.length + 1; + const orderNum = `PO-${dateStr}-${String(counter).padStart(3, '0')}`; + setOrderNumber(orderNum); + return orderNum; + } catch (error) { + console.error("Error generating order number:", error); + // Fallback to timestamp-based number + const fallbackNum = `PO-${Date.now()}`; + setOrderNumber(fallbackNum); + return fallbackNum; + } + }, []); // Fetch suppliers when the component mounts const fetchSuppliers = useCallback(async () => { @@ -95,7 +132,8 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro id: item.id, name: item.name, sku: item.sku, - costPrice: item.cost_price, // Assuming cost_price is the field in stock_items + costPrice: item.cost_price, // ZAR cost price + costPriceGBP: item.cost_price_gbp || 0, // GBP cost price })); setStockItems(formattedStockItems); } catch (error) { @@ -107,12 +145,17 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro useEffect(() => { fetchSuppliers(); fetchStockItems(); - }, [fetchSuppliers, fetchStockItems]); + generateOrderNumber(); + }, [fetchSuppliers, fetchStockItems, generateOrderNumber]); const form = useForm>({ resolver: zodResolver(purchaseOrderFormSchema), defaultValues: { + orderNumber: orderNumber, supplierName: "", + accountRef: "", + salesOrderNo: "", + carriageCharge: 0, orderDate: new Date(), items: [{ stockItemId: "", @@ -124,6 +167,13 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro }, }) + // Update order number when it's generated + React.useEffect(() => { + if (orderNumber) { + form.setValue('orderNumber', orderNumber); + } + }, [orderNumber, form]); + const { fields, append, remove } = useFieldArray({ control: form.control, name: "items", @@ -171,7 +221,8 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro handleItemChange(index, "stockItemId", selectedStockItem.id); handleItemChange(index, "itemName", selectedStockItem.name); handleItemChange(index, "sku", selectedStockItem.sku); - handleItemChange(index, "costPrice", selectedStockItem.costPrice); + // Use GBP price for purchase orders + handleItemChange(index, "costPrice", selectedStockItem.costPriceGBP); // Quantity remains as is or defaults to 1 if not set if (form.getValues("items")[index].quantity === 0) { handleItemChange(index, "quantity", 1); @@ -190,28 +241,68 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro // Prepare data for PocketBase const purchaseOrderData = { - supplierName: values.supplierName, // This will be the supplier's name for display - supplier: selectedSupplier.id, // This will be the relation to the suppliers collection + order_number: values.orderNumber, + supplierName: selectedSupplier.id, // This is the relation field to suppliers collection + account_ref: values.accountRef || "", + sales_order_no: values.salesOrderNo || "", + carriage_charge: values.carriageCharge || 0, orderDate: values.orderDate.toISOString(), - status: "Pending Approval", + status: "Pending", // Must match one of: Pending, Ordered, Received items: values.items.map(item => item.stockItemId), - quantities: values.items.map(item => item.quantity), // Keep quantities array as well if needed elsewhere + quantities: values.items.map(item => item.quantity), }; + console.log("Attempting to create purchase order with data:", purchaseOrderData); + console.log("Selected supplier:", selectedSupplier); + console.log("Form values:", values); + await pb.collection('purchase_orders').create(purchaseOrderData); - showSuccess("Purchase order created successfully!"); + showSuccess(`Purchase order ${values.orderNumber} created successfully!`); form.reset(); + // Generate new order number for next use + await generateOrderNumber(); setOpen(false); refreshData(); - } catch (error) { + } catch (error: any) { console.error("Error creating purchase order:", error); - showError("Failed to create purchase order. Please check the form and try again."); + console.error("Error data:", error.data); + console.error("Error response:", error.response); + + // Log detailed field errors + if (error.data?.data) { + console.error("Field validation errors:", JSON.stringify(error.data.data, null, 2)); + } + + // Create a more detailed error message + let errorMessage = "Failed to create purchase order"; + if (error.data?.data) { + const fieldErrors = Object.entries(error.data.data).map(([field, details]: [string, any]) => { + return `${field}: ${details.message || JSON.stringify(details)}`; + }); + errorMessage += ":\n" + fieldErrors.join("\n"); + } + + showError(errorMessage); } } return (
+ ( + + Order Number + + + + + + )} + /> + +
+ ( + + Account Ref + + + + + + )} + /> + + ( + + Sales Order No + + + + + + )} + /> +
+ {/* Items Section */}

Order Items

@@ -269,23 +390,12 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro render={({ field }) => ( Item Name - + handleStockItemSelect(index, selectedItemName)} + placeholder="Search by SKU or name..." + /> )} @@ -322,9 +432,9 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro name={`items.${index}.costPrice`} render={({ field }) => ( - Unit Price (ZAR) + Unit Price (£ GBP) - handleItemChange(index, "costPrice", e.target.value)} /> + handleItemChange(index, "costPrice", e.target.value)} /> diff --git a/src/components/purchase-orders/StockItemSearch.tsx b/src/components/purchase-orders/StockItemSearch.tsx new file mode 100644 index 0000000..de75bae --- /dev/null +++ b/src/components/purchase-orders/StockItemSearch.tsx @@ -0,0 +1,108 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +// Define the type for a stock item +type StockItem = { + id: string + name: string + sku: string + costPrice: number + costPriceGBP: number +} + +interface StockItemSearchProps { + stockItems: StockItem[]; + selectedItemName: string; + onSelectItem: (itemName: string) => void; + placeholder: string; +} + +export function StockItemSearch({ stockItems, selectedItemName, onSelectItem, placeholder }: StockItemSearchProps) { + const [open, setOpen] = React.useState(false) + const [search, setSearch] = React.useState("") + + return ( + + + + + + + + + No items found. + + {stockItems + .filter(item => + item.name.toLowerCase().includes(search.toLowerCase()) || + item.sku.toLowerCase().includes(search.toLowerCase()) + ) + .map((item) => ( + { + onSelectItem(value); + setOpen(false); + }} + > + +
+
{item.name}
+
+ SKU: {item.sku} | GBP: £{item.costPriceGBP.toFixed(2)} | ZAR: R{item.costPrice.toFixed(2)} +
+
+
+ ))} +
+
+
+
+
+ ) +} diff --git a/src/components/purchase-orders/columns.tsx b/src/components/purchase-orders/columns.tsx index dea4aa7..dca9a78 100644 --- a/src/components/purchase-orders/columns.tsx +++ b/src/components/purchase-orders/columns.tsx @@ -27,8 +27,18 @@ export type PurchaseOrder = { orderDate: string status: string itemCount: number - items: Array<{ stockItemId: string; itemName: string; quantity: number; sku: string; costPrice: number }> // Added costPrice + orderNumber?: string + accountRef?: string + salesOrderNo?: string + carriageCharge?: number + items: Array<{ stockItemId: string; itemName: string; quantity: number; sku: string; costPrice: number; costPriceGBP: number }> // Added costPrice and costPriceGBP quantities: number[] + supplier?: { + name: string + address: string + email: string + phone: string + } | null } // Define the props for the actions column diff --git a/src/components/stock/AddItemForm.tsx b/src/components/stock/AddItemForm.tsx index 3989213..db2fce8 100644 --- a/src/components/stock/AddItemForm.tsx +++ b/src/components/stock/AddItemForm.tsx @@ -24,8 +24,9 @@ const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), sku: z.string().min(2, { message: "SKU must be at least 2 characters." }), category: z.string({ required_error: "Please select or create a category." }), - quantity: z.coerce.number().int().positive({ message: "Quantity must be a positive number." }), - costPrice: z.coerce.number().positive({ message: "Cost price must be a positive number." }), + quantity: z.coerce.number().int().min(0, { message: "Quantity must be 0 or greater." }), + costPrice: z.coerce.number().min(0, { message: "Cost price (ZAR) must be 0 or greater." }), + costPriceGBP: z.coerce.number().min(0, { message: "Cost price (GBP) must be 0 or greater." }), sellingPrice: z.coerce.number().positive({ message: "Selling price must be a positive number." }), }) @@ -64,8 +65,9 @@ export function AddItemForm({ setOpen, refreshData }: AddItemFormProps) { defaultValues: { name: "", sku: "", - quantity: 1, + quantity: 0, costPrice: 0.00, + costPriceGBP: 0.00, sellingPrice: 0.00, }, }) @@ -78,6 +80,7 @@ export function AddItemForm({ setOpen, refreshData }: AddItemFormProps) { category: values.category, quantity: values.quantity, cost_price: values.costPrice, + cost_price_gbp: values.costPriceGBP, selling_price: values.sellingPrice, }); @@ -166,26 +169,41 @@ export function AddItemForm({ setOpen, refreshData }: AddItemFormProps) { )} /> -
- ( - - Cost Price - - - - - - )} - /> +
+
+ ( + + Cost Price (£ GBP) + + + + + + )} + /> + ( + + Cost Price (R ZAR) + + + + + + )} + /> +
( - Selling Price + Selling Price (R ZAR) diff --git a/src/components/stock/EditItemForm.tsx b/src/components/stock/EditItemForm.tsx index 98b6ba6..954bef1 100644 --- a/src/components/stock/EditItemForm.tsx +++ b/src/components/stock/EditItemForm.tsx @@ -25,8 +25,9 @@ const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), sku: z.string().min(2, { message: "SKU must be at least 2 characters." }), category: z.string({ required_error: "Please select or create a category." }), - quantity: z.coerce.number().int().positive({ message: "Quantity must be a positive number." }), - costPrice: z.coerce.number().positive({ message: "Cost price must be a positive number." }), + quantity: z.coerce.number().int().min(0, { message: "Quantity must be 0 or greater." }), + costPrice: z.coerce.number().min(0, { message: "Cost price (ZAR) must be 0 or greater." }), + costPriceGBP: z.coerce.number().min(0, { message: "Cost price (GBP) must be 0 or greater." }), sellingPrice: z.coerce.number().positive({ message: "Selling price must be a positive number." }), }) @@ -69,6 +70,7 @@ export function EditItemForm({ setOpen, refreshData, item }: EditItemFormProps) category: item.category.toLowerCase(), // Ensure category is lowercase for combobox value quantity: item.quantity, costPrice: item.costPrice, + costPriceGBP: item.costPriceGBP || 0, sellingPrice: item.sellingPrice, }, }) @@ -81,6 +83,7 @@ export function EditItemForm({ setOpen, refreshData, item }: EditItemFormProps) category: values.category, quantity: values.quantity, cost_price: values.costPrice, + cost_price_gbp: values.costPriceGBP, selling_price: values.sellingPrice, }); showSuccess("Item updated successfully!"); @@ -160,26 +163,41 @@ export function EditItemForm({ setOpen, refreshData, item }: EditItemFormProps) )} /> -
- ( - - Cost Price - - - - - - )} - /> +
+
+ ( + + Cost Price (£ GBP) + + + + + + )} + /> + ( + + Cost Price (R ZAR) + + + + + + )} + /> +
( - Selling Price + Selling Price (R ZAR) diff --git a/src/components/stock/columns.tsx b/src/components/stock/columns.tsx index baa798d..70e070c 100644 --- a/src/components/stock/columns.tsx +++ b/src/components/stock/columns.tsx @@ -29,6 +29,7 @@ export type StockItem = { category: string quantity: number costPrice: number + costPriceGBP: number sellingPrice: number } diff --git a/src/pages/PurchaseOrders.tsx b/src/pages/PurchaseOrders.tsx index ec388fe..a905e53 100644 --- a/src/pages/PurchaseOrders.tsx +++ b/src/pages/PurchaseOrders.tsx @@ -26,7 +26,7 @@ const PurchaseOrders = () => { try { const records = await pb.collection('purchase_orders').getList(1, 50, { sort: '-orderDate', - expand: 'items', // Ensure items are expanded for PDF generation + expand: 'items,supplierName', // Expand both items and supplier for PDF generation }); const formattedData = records.items.map((record) => ({ @@ -35,14 +35,25 @@ const PurchaseOrders = () => { orderDate: record.orderDate, status: record.status, itemCount: record.items?.length || 0, + orderNumber: record.order_number || record.id, // Use order_number field + accountRef: record.account_ref || '', + salesOrderNo: record.sales_order_no || '', + carriageCharge: record.carriage_charge || 0, items: (record.expand?.items || []).map((item: any) => ({ stockItemId: item.id, itemName: item.name, quantity: item.quantity, // This is the quantity in stock_items, not the ordered quantity sku: item.sku, - costPrice: item.cost_price, // Include costPrice from expanded item + costPrice: item.cost_price, // ZAR price + costPriceGBP: item.cost_price_gbp || 0, // GBP price })), quantities: record.quantities || [], // This array holds the ordered quantities + supplier: record.expand?.supplierName ? { + name: record.expand.supplierName.supplier_name || '', + address: record.expand.supplierName.suppier_address || '', // Note: typo in DB field name "suppier_address" + email: record.expand.supplierName.supplier_email || '', + phone: record.expand.supplierName.supplier_phone_number || '', + } : null, })); setPurchaseOrders(formattedData); diff --git a/src/pages/Stock.tsx b/src/pages/Stock.tsx index 30dbfb8..65ffdc3 100644 --- a/src/pages/Stock.tsx +++ b/src/pages/Stock.tsx @@ -32,6 +32,7 @@ const Stock = () => { category: item.category, quantity: item.quantity, costPrice: item.cost_price, + costPriceGBP: item.cost_price_gbp || 0, sellingPrice: item.selling_price, })); setStockItems(formattedData);