Reverted all changes back to version 04dee1a0832bdeb0f99ed5e69532fcdeae8b659c

This commit is contained in:
[dyad] 2025-09-12 14:33:05 +02:00
parent 92f9ad0413
commit ec62bf32ca
5 changed files with 355 additions and 654 deletions

View file

@ -13,33 +13,26 @@ import {
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Columns } from "lucide-react"
import { format } from "date-fns" // Import format for date display
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Columns, Download, Eye } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { jsPDF } from "jspdf" // Import jsPDF
// Import PurchaseOrder type and createColumns function
import { jsPDF } from "jspdf"
import { ReceiveStockDialog } from "./ReceiveStockDialog"
import pb from "@/lib/pocketbase"
import { createColumns, PurchaseOrder } from "./columns"
import { ReceiveStockDialog } from "../receive-stock/ReceiveStockDialog" // Assuming ReceiveStockDialog is in this path
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" // Import Dialog components
import pb from "@/lib/pocketbase" // Import pb
import { showError, showSuccess } from "@/utils/toast" // Import toast functions
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
interface PurchaseOrderDataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
@ -50,7 +43,7 @@ interface PurchaseOrderDataTableProps<TData, TValue> {
}
export function PurchaseOrderDataTable<TData, TValue>({
columns,
columns: columnsProp,
data,
filterValue,
onFilterChange,
@ -83,12 +76,17 @@ export function PurchaseOrderDataTable<TData, TValue>({
setReceiveDialogOpen(false)
}
const handleViewOrder = useCallback((order: PurchaseOrder) => { // useCallback is now correctly imported and used
const handleViewOrder = (order: PurchaseOrder) => {
setSelectedOrder(order)
setViewDialogOpen(true)
}, []);
}
const generatePDF = useCallback((order: PurchaseOrder) => { // useCallback is now correctly imported and used
const closeViewDialog = () => {
setSelectedOrder(null)
setViewDialogOpen(false)
}
const generatePDF = (order: PurchaseOrder) => {
const doc = new jsPDF();
// Set font and colors
@ -126,7 +124,8 @@ export function PurchaseOrderDataTable<TData, TValue>({
doc.setFontSize(10);
doc.setTextColor(textColor);
doc.text(`Supplier Name: ${order.supplierName}`, 14, 75); // Use supplierName from expanded data
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);
@ -161,7 +160,7 @@ export function PurchaseOrderDataTable<TData, TValue>({
order.items.forEach((item, index) => {
const orderedQuantity = order.quantities[index] || 0;
const unitPrice = item.costPrice || 0;
const unitPrice = item.costPrice || 0; // Use costPrice from the item
const itemTotal = orderedQuantity * unitPrice;
subtotal += itemTotal;
@ -208,7 +207,7 @@ export function PurchaseOrderDataTable<TData, TValue>({
doc.text(subtotal.toLocaleString("en-ZA", { style: "currency", currency: "ZAR" }), 180, y); // Grand Total
doc.save(`purchase_order_${order.id}.pdf`);
}, []);
};
const approveOrder = async (order: PurchaseOrder) => {
try {
@ -216,7 +215,6 @@ export function PurchaseOrderDataTable<TData, TValue>({
refreshData()
} catch (error) {
console.error("Failed to approve order:", error)
showError("Failed to approve order.")
}
}
@ -226,21 +224,14 @@ export function PurchaseOrderDataTable<TData, TValue>({
refreshData()
} catch (error) {
console.error("Failed to reject order:", error)
showError("Failed to reject order.")
}
}
const handleViewOrder = useCallback((order: PurchaseOrder) => {
setSelectedOrder(order)
setViewDialogOpen(true)
}, []);
// Use the createColumns function to get the columns, passing the necessary callbacks
const columnsWithActions = React.useMemo(() => createColumns({ onDownloadPdf: generatePDF, onViewOrder: handleViewOrder }), [generatePDF, handleViewOrder]);
const table = useReactTable<PurchaseOrder>({
data: data as PurchaseOrder[],
columns: columnsWithActions, // Use the generated columns
columns: columnsWithActions,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
@ -258,18 +249,19 @@ export function PurchaseOrderDataTable<TData, TValue>({
})
React.useEffect(() => {
table.getColumn("supplier")?.setFilterValue(filterValue) // Filter by supplier name
table.getColumn("supplierName")?.setFilterValue(filterValue)
}, [filterValue, table])
return (
<>
<div>
<div className="flex items-center py-4">
<Input
<input
type="text"
placeholder="Filter by supplier name..."
value={filterValue}
onChange={(e) => onFilterChange(e.target.value)}
className="max-w-sm"
className="max-w-sm border border-gray-300 rounded px-2 py-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -300,53 +292,64 @@ export function PurchaseOrderDataTable<TData, TValue>({
</DropdownMenu>
</div>
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
<table className="w-full min-w-full table-auto">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} className="border-b px-4 py-2 text-left font-medium text-gray-900">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</TableHeader>
<TableBody>
</thead>
<tbody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
table.getRowModel().rows.map(row => {
const order = row.original as PurchaseOrder
return (
<tr key={row.id} className="border-b hover:bg-gray-50">
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-4 py-2 align-top">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
<td className="px-4 py-2 space-x-2">
{order.status === "Pending Approval" && (
<>
<Button size="sm" variant="outline" onClick={() => approveOrder(order)}>Approve</Button>
<Button size="sm" variant="destructive" onClick={() => rejectOrder(order)}>Reject</Button>
</>
)}
{order.status === "Approved" && (
<>
<Button size="sm" onClick={() => generatePDF(order)}>Download PDF</Button>
<Button size="sm" variant="secondary" onClick={() => openReceiveDialog(order)}>Receive Stock</Button>
</>
)}
{order.status === "Received" && (
<span className="text-green-600 font-semibold">Received</span>
)}
{order.status === "Rejected" && (
<span className="text-red-600 font-semibold">Rejected</span>
)}
</td>
</tr>
)
})
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
<tr>
<td colSpan={table.getAllColumns().length} className="h-24 text-center">No results.</td>
</tr>
)}
</TableBody>
</Table>
</tbody>
</table>
</div>
</div>
{/* View Order Dialog */}
<Dialog open={viewDialogOpen} onOpenChange={setViewDialogOpen}>
<Dialog open={viewDialogOpen} onOpenChange={closeViewDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Purchase Order Details</DialogTitle>
@ -354,7 +357,7 @@ export function PurchaseOrderDataTable<TData, TValue>({
</DialogHeader>
<div className="space-y-4">
<p><strong>Supplier:</strong> {selectedOrder?.supplierName}</p>
<p><strong>Order Date:</strong> {selectedOrder?.orderDate ? format(selectedOrder.orderDate, "PPP") : 'N/A'}</p>
<p><strong>Order Date:</strong> {selectedOrder?.orderDate ? new Date(selectedOrder.orderDate).toLocaleDateString() : 'N/A'}</p>
<p><strong>Status:</strong> {selectedOrder?.status}</p>
<h4 className="font-semibold">Items:</h4>
<ul className="list-disc list-inside">
@ -366,7 +369,7 @@ export function PurchaseOrderDataTable<TData, TValue>({
</ul>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button variant="outline" onClick={() => setViewDialogOpen(false)}>Close</Button> {/* Corrected function call */}
<Button variant="outline" onClick={closeViewDialog}>Close</Button>
</div>
</DialogContent>
</Dialog>

View file

@ -2,205 +2,207 @@
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { useForm, useFieldArray } from "react-hook-form"
import * as z from "zod"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar" // <-- Added Calendar import
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { SupplierSelect } from "@/components/suppliers/SupplierSelect";
import pb from "@/lib/pocketbase";
import { showError, showSuccess } from "@/utils/toast";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { showSuccess, showError } from "@/utils/toast"
import pb from "@/lib/pocketbase"
import { StockItem } from "@/components/stock/columns"
const purchaseOrderFormSchema = z.object({
supplier: z.string({
required_error: "Please select a supplier.",
}).min(1, { message: "Supplier is required." }),
orderDate: z.date({
required_error: "Order date is required.",
}),
expectedDeliveryDate: z.date({
required_error: "Expected delivery date is required.",
}),
notes: z.string().optional(),
// Corrected status options to match the schema
const statusOptions = ["Pending", "Ordered", "Received"] as const
const formSchema = z.object({
supplierName: z.string().min(2, { message: "Supplier name must be at least 2 characters." }),
status: z.enum(statusOptions),
items: z.array(z.object({
stockItemId: z.string({ required_error: "Select an item." }),
quantity: z.number().int().positive({ message: "Quantity must be positive." }),
})).min(1, { message: "At least one item is required." }),
})
type FormSchema = z.infer<typeof formSchema>
interface PurchaseOrderFormProps {
onFormSubmit: (data: z.infer<typeof purchaseOrderFormSchema>) => void;
setOpen: (open: boolean) => void;
refreshData: () => void;
setOpen: (open: boolean) => void
refreshData: () => void
}
export function PurchaseOrderForm({ onFormSubmit, setOpen, refreshData }: PurchaseOrderFormProps) {
const [selectedSupplierDetails, setSelectedSupplierDetails] = React.useState<any>(null);
export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormProps) {
const [stockItems, setStockItems] = React.useState<StockItem[]>([])
const form = useForm<z.infer<typeof purchaseOrderFormSchema>>({
resolver: zodResolver(purchaseOrderFormSchema),
React.useEffect(() => {
const fetchStockItems = async () => {
try {
const records = await pb.collection('stock_items').getFullList()
const items = records.map((item: any) => ({
id: item.id,
name: item.name,
sku: item.sku,
category: item.category,
quantity: item.quantity,
costPrice: item.cost_price,
sellingPrice: item.selling_price,
}))
setStockItems(items)
} catch (err) {
console.error("Failed to load stock items:", err)
showError("Failed to load stock items.")
}
}
fetchStockItems()
}, [])
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
supplier: "",
orderDate: new Date(),
expectedDeliveryDate: new Date(),
notes: "",
supplierName: "",
status: "Pending", // Set default status to "Pending"
items: [{ stockItemId: "", quantity: 1 }],
},
})
async function onSubmit(values: z.infer<typeof purchaseOrderFormSchema>) {
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "items",
})
async function onSubmit(values: FormSchema) {
try {
// Set orderDate to current date automatically
const orderDate = new Date().toISOString()
// Prepare items relation and quantities
const itemIds = values.items.map(i => i.stockItemId)
const quantities = values.items.map(i => i.quantity)
await pb.collection('purchase_orders').create({
supplier: values.supplier,
order_date: values.orderDate.toISOString(),
expected_delivery_date: values.expectedDeliveryDate.toISOString(),
notes: values.notes,
});
showSuccess("Purchase order created successfully!");
form.reset();
setSelectedSupplierDetails(null);
setOpen(false);
refreshData();
} catch (error) {
console.error("Error creating purchase order:", error);
let errorMessage = "Failed to create purchase order. Please try again.";
if (error.response && error.response.data && error.response.data.message) {
errorMessage = `Failed to create purchase order: ${error.response.data.message}`;
}
showError(errorMessage);
supplierName: values.supplierName,
orderDate,
status: values.status,
items: itemIds,
quantities: quantities,
})
showSuccess("Purchase order created successfully!")
form.reset()
setOpen(false)
refreshData()
} catch (err) {
console.error("Failed to create purchase order:", err)
showError("Failed to create purchase order. Please try again.")
}
}
const handleSupplierChange = (supplier: any) => {
setSelectedSupplierDetails(supplier);
form.setValue("supplier", supplier ? supplier.id : "");
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 max-w-md">
<FormField
control={form.control}
name="supplier"
name="supplierName"
render={({ field }) => (
<FormItem>
<FormLabel>Supplier</FormLabel>
<FormLabel>Supplier Name</FormLabel>
<FormControl>
<SupplierSelect
onSupplierChange={handleSupplierChange}
selectedSupplierId={field.value}
/>
<Input placeholder="Supplier name" {...field} />
</FormControl>
<FormDescription>Select the supplier for this purchase order.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="orderDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Order Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={
"w-[240px] pl-3 text-left font-normal"
}
>
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormDescription>Your order date.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expectedDeliveryDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Expected Delivery Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={
"w-[240px] pl-3 text-left font-normal"
}
>
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormDescription>Your expected delivery date.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Input placeholder="Any special instructions or notes" {...field} />
</FormControl>
<FormDescription>Optional notes for the purchase order.</FormDescription>
<FormLabel>Status</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
{statusOptions.map((status) => (
<SelectItem key={status} value={status}>
{status}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Create Purchase Order</Button>
<div>
<FormLabel>Items</FormLabel>
{fields.map((field, index) => (
<div key={field.id} className="flex space-x-2 items-center mb-2">
<FormField
control={form.control}
name={`items.${index}.stockItemId`}
render={({ field }) => (
<FormItem className="flex-1">
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Select item" />
</SelectTrigger>
<SelectContent>
{stockItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name} ({item.sku})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.quantity`}
render={({ field }) => (
<FormItem className="w-24">
<Input
type="number"
min={1}
{...field}
onChange={(e) => {
const val = parseInt(e.target.value, 10)
field.onChange(isNaN(val) ? "" : val)
}}
/>
<FormMessage />
</FormItem>
)}
/>
<Button
variant="destructive"
size="icon"
onClick={() => remove(index)}
aria-label="Remove item"
>
&times;
</Button>
</div>
))}
<Button type="button" onClick={() => append({ stockItemId: "", quantity: 1 })}>
Add Item
</Button>
</div>
<Button type="submit" className="w-full mt-4">
Create Purchase Order
</Button>
</form>
</Form>
)

View file

@ -1,10 +1,16 @@
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal } from "lucide-react"
import { format } from "date-fns" // Import format for date display
import { MoreHorizontal, Download, Eye } from "lucide-react" // Import Eye icon
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" // Import Dialog components
import {
DropdownMenu,
DropdownMenuContent,
@ -13,47 +19,41 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useState } from "react" // Import useState
// Define the PurchaseOrder type here if it's not defined elsewhere
export type PurchaseOrder = {
id: string
supplier: string
supplierId: string
orderDate: Date
expectedDeliveryDate: Date
supplierName: string
orderDate: string
status: string
notes: string
items: Array<{ stockItemId: string; itemName: string; sku: string; costPrice: number }>; // Added items and costPrice
quantities: Array<number>; // Added quantities
supplierName: string; // Added supplierName for PDF generation
itemCount: number
items: Array<{ stockItemId: string; itemName: string; quantity: number; sku: string; costPrice: number }> // Added costPrice
quantities: number[]
}
// Define a type for the props that the actions column will receive
// Define the props for the actions column
interface ActionsColumnProps {
onDownloadPdf: (order: PurchaseOrder) => void; // Function to handle PDF download
onViewOrder: (order: PurchaseOrder) => void; // Function to handle viewing order details
onDownloadPdf: (order: PurchaseOrder) => void;
onViewOrder: (order: PurchaseOrder) => void; // Add onViewOrder prop
}
// Export createColumns as a function that accepts props
// Export createColumns function
export const createColumns = ({ onDownloadPdf, onViewOrder }: ActionsColumnProps): ColumnDef<PurchaseOrder>[] => [
{
accessorKey: "supplier",
header: "Supplier",
accessorKey: "supplierName",
header: "Supplier Name",
},
{
accessorKey: "orderDate",
header: "Order Date",
cell: ({ row }) => {
const date = row.getValue("orderDate") as Date
return date ? format(date, "PPP") : "N/A"
},
},
{
accessorKey: "expectedDeliveryDate",
header: "Expected Delivery Date",
cell: ({ row }) => {
const date = row.getValue("expectedDeliveryDate") as Date
return date ? format(date, "PPP") : "N/A"
const date = new Date(row.getValue("orderDate"))
const formattedDate = date.toLocaleDateString("en-ZA", {
year: "numeric",
month: "short",
day: "numeric",
})
return <div className="font-medium">{formattedDate}</div>
},
},
{
@ -61,14 +61,15 @@ export const createColumns = ({ onDownloadPdf, onViewOrder }: ActionsColumnProps
header: "Status",
},
{
accessorKey: "notes",
header: "Notes",
accessorKey: "itemCount",
header: "Number of Items",
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const purchaseOrder = row.original
const order = row.original
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); // State for edit dialog, if needed later
return (
<DropdownMenu>
@ -81,15 +82,19 @@ export const createColumns = ({ onDownloadPdf, onViewOrder }: ActionsColumnProps
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(purchaseOrder.id)}
onClick={() => navigator.clipboard.writeText(order.id)}
>
Copy Purchase Order ID
Copy Order ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onViewOrder(purchaseOrder)}>View Details</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDownloadPdf(purchaseOrder)}>Download PDF</DropdownMenuItem>
<DropdownMenuLabel>Edit Purchase Order</DropdownMenuLabel>
<DropdownMenuLabel className="text-red-600">Delete Purchase Order</DropdownMenuLabel>
<DropdownMenuItem onClick={() => onViewOrder(order)}> {/* Call onViewOrder */}
<Eye className="mr-2 h-4 w-4" />
View Order
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDownloadPdf(order)}>
<Download className="mr-2 h-4 w-4" />
Download PDF
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)

View file

@ -1,76 +0,0 @@
"use client"
import React, { useState, useEffect, useCallback } from "react";
import pb from "@/lib/pocketbase";
import { Supplier } from "@/components/suppliers/columns"; // Assuming columns.tsx defines the Supplier type
import { showError } from "@/utils/toast";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface SupplierSelectProps {
onSupplierChange: (supplier: Supplier | null) => void;
selectedSupplierId?: string | null; // Optional prop to set initial value
}
export function SupplierSelect({ onSupplierChange, selectedSupplierId }: SupplierSelectProps) {
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [isLoading, setIsLoading] = useState(true);
const fetchSuppliers = useCallback(async () => {
setIsLoading(true);
try {
const records = await pb.collection('suppliers').getList(1, 50, {
sort: '-created',
});
const formattedData: Supplier[] = records.items.map((item: any) => ({
id: item.id,
name: item.supplier_name,
address: item.supplier_address,
phone: item.supplier_phone_number,
email: item.supplier_email,
}));
setSuppliers(formattedData);
} catch (error) {
console.error("Error fetching suppliers:", error);
showError("Failed to load suppliers for selection.");
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchSuppliers();
}, [fetchSuppliers]);
const handleValueChange = (value: string) => {
const selectedSupplier = suppliers.find(supplier => supplier.id === value) || null;
onSupplierChange(selectedSupplier);
};
return (
<Select onValueChange={handleValueChange} defaultValue={selectedSupplierId || undefined}>
<SelectTrigger className="w-full" disabled={isLoading}>
<SelectValue placeholder="Select a supplier" />
</SelectTrigger>
<SelectContent>
{isLoading ? (
<SelectItem value="" disabled>Loading suppliers...</SelectItem>
) : suppliers.length === 0 ? (
<SelectItem value="" disabled>No suppliers found</SelectItem>
) : (
suppliers.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id}>
{supplier.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
);
}

View file

@ -1,53 +1,55 @@
import React, { useState, useEffect, useCallback } from "react";
import { createColumns, PurchaseOrder } from "@/components/purchase-orders/columns"; // Ensured createColumns is exported
import { PurchaseOrderDataTable } from "@/components/purchase-orders/PurchaseOrderDataTable";
import { useState, useEffect, useCallback } from "react";
import { PurchaseOrderForm } from "@/components/purchase-orders/PurchaseOrderForm";
import { Button } from "@/components/ui/button";
import { PlusCircle } from "lucide-react";
import {
Dialog,
Dialog, // Import Dialog components
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription, // Import DialogDescription
} from "@/components/ui/dialog";
import { PurchaseOrderForm } from "@/components/purchase-orders/PurchaseOrderForm";
import { createColumns, PurchaseOrder } from "@/components/purchase-orders/columns"; // Import createColumns
import { PurchaseOrderDataTable } from "@/components/purchase-orders/PurchaseOrderDataTable";
import pb from "@/lib/pocketbase";
import { showError } from "@/utils/toast";
import { format } from "date-fns"; // Import format for date display
import { jsPDF } from "jspdf"; // Import jsPDF
const PurchaseOrders = () => {
const [addPurchaseOrderDialogOpen, setAddPurchaseOrderDialogOpen] = useState(false);
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([]);
const [purchaseOrderOpen, setPurchaseOrderOpen] = useState(false);
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([]); // Explicitly type purchaseOrders
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [filterValue, setFilterValue] = useState("");
const [viewOrderDialogOpen, setViewOrderDialogOpen] = useState(false); // State for the view order dialog
const [selectedOrderForView, setSelectedOrderForView] = useState<PurchaseOrder | null>(null); // State to hold the order to view
const fetchPurchaseOrders = useCallback(async () => {
try {
const records = await pb.collection('purchase_orders').getList(1, 50, {
sort: '-created',
expand: 'supplier',
sort: '-orderDate',
expand: 'items', // Ensure items are expanded for PDF generation
});
const formattedData: PurchaseOrder[] = records.items.map((item: any) => ({
id: item.id,
supplier: item.expand?.supplier?.name || 'N/A',
supplierId: item.supplier,
orderDate: new Date(item.order_date),
expectedDeliveryDate: new Date(item.expected_delivery_date),
status: item.status || 'Pending',
notes: item.notes || '',
items: item.items || [],
quantities: item.quantities || [],
supplierName: item.expand?.supplier?.name || 'N/A',
const formattedData = records.items.map((record) => ({
id: record.id,
supplierName: record.supplierName,
orderDate: record.orderDate,
status: record.status,
itemCount: record.items?.length || 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
})),
quantities: record.quantities || [], // This array holds the ordered quantities
}));
setPurchaseOrders(formattedData);
} catch (error) {
console.error("Error fetching purchase orders:", error);
showError("Failed to load purchase orders.");
} catch (err) {
console.error("Error fetching purchase orders:", err);
}
}, [refreshTrigger]);
}, [refreshTrigger]); // Include refreshTrigger in dependency array
useEffect(() => {
fetchPurchaseOrders();
@ -57,314 +59,79 @@ const PurchaseOrders = () => {
setRefreshTrigger(prev => prev + 1);
};
const handlePurchaseOrderFormSubmit = (data: any) => {
console.log("Purchase order form submitted with data:", data);
};
const handleFilterChange = (value: string) => {
setFilterValue(value);
};
const generatePDF = useCallback((order: PurchaseOrder) => {
const doc = new jsPDF();
// Set font and colors
doc.setFont("helvetica");
const primaryColor = "#ffc905"; // Your primary color
const textColor = "#333333";
// --- Company Details ---
const companyName = "JEI SA";
const companyAddress = "47 Station St, Oberholzer, Carletonville, 2499";
const companyPhone = "018 786 2897";
// --- Header Section ---
doc.setFontSize(24);
doc.setTextColor(primaryColor);
doc.text("PURCHASE ORDER", 105, 20, { align: "center" });
doc.setFontSize(10);
doc.setTextColor(textColor);
doc.text(companyName, 14, 30); // Company Name
doc.text(companyAddress, 14, 35);
doc.text(companyPhone, 14, 40);
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);
// --- Supplier Details ---
doc.setFontSize(12);
doc.setTextColor(primaryColor);
doc.text("SUPPLIER DETAILS", 14, 65);
doc.setDrawColor(primaryColor);
doc.line(14, 66, 50, 66); // Underline
doc.setFontSize(10);
doc.setTextColor(textColor);
doc.text(`Supplier Name: ${order.supplierName}`, 14, 75); // Use supplierName from expanded data
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);
// --- 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
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;
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
doc.setDrawColor(primaryColor);
doc.line(140, y, 196, y); // Line above subtotal
y += 5;
doc.setFontSize(10);
doc.text("Subtotal:", 140, y);
doc.text(subtotal.toLocaleString("en-ZA", { style: "currency", currency: "ZAR" }), 180, y);
y += 7;
doc.text("Shipping:", 140, y);
doc.text("R0.00", 180, y); // Placeholder for shipping
y += 7;
doc.text("Tax:", 140, y);
doc.text("R0.00", 180, y); // Placeholder for tax
y += 7;
doc.text("Other:", 140, y);
doc.text("R0.00", 180, y); // Placeholder for other charges
y += 10;
doc.setDrawColor(primaryColor);
doc.setLineWidth(0.5);
doc.line(140, y, 196, y); // Thicker line above total
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
doc.save(`purchase_order_${order.id}.pdf`);
// Define the PDF download handler
const handleDownloadPdf = useCallback((order: PurchaseOrder) => {
console.log("Downloading PDF for order:", order.id);
// The actual PDF generation logic is now in PurchaseOrderDataTable,
// so this handler just needs to be passed down.
}, []);
const approveOrder = async (order: PurchaseOrder) => {
try {
await pb.collection('purchase_orders').update(order.id, { status: "Approved" })
handleRefreshData()
} catch (error) {
console.error("Failed to approve order:", error)
showError("Failed to approve order.")
}
}
const rejectOrder = async (order: PurchaseOrder) => {
try {
await pb.collection('purchase_orders').update(order.id, { status: "Rejected" })
handleRefreshData()
} catch (error) {
console.error("Failed to reject order:", error)
showError("Failed to reject order.")
}
}
// Define the handler to open the view order dialog
const handleViewOrder = useCallback((order: PurchaseOrder) => {
setSelectedOrder(order)
setViewDialogOpen(true)
setSelectedOrderForView(order);
setViewOrderDialogOpen(true);
}, []);
const closeViewDialog = () => {
setSelectedOrder(null);
setViewDialogOpen(false);
};
const columnsWithActions = React.useMemo(() => createColumns({ onDownloadPdf: generatePDF, onViewOrder: handleViewOrder }), [generatePDF, handleViewOrder]);
const table = useReactTable<PurchaseOrder>({
data: purchaseOrders, // Use the fetched purchaseOrders state
columns: columnsWithActions,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
})
React.useEffect(() => {
table.getColumn("supplier")?.setFilterValue(filterValue)
}, [filterValue, table])
// Call createColumns to get the columns array, passing the handlers
const columns = createColumns({ onDownloadPdf: handleDownloadPdf, onViewOrder: handleViewOrder });
return (
<>
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter by supplier name..."
value={filterValue}
onChange={(e) => onFilterChange(e.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="ml-auto">
<Columns className="h-4 w-4" />
<span className="sr-only">Toggle columns</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide()
)
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<div>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
<div>
<h1 className="text-3xl font-bold">Purchase Orders</h1>
<p className="text-gray-500">Manage your purchase orders here.</p>
</div>
<Dialog open={purchaseOrderOpen} onOpenChange={setPurchaseOrderOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
New Purchase Order
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>New Purchase Order</DialogTitle>
</DialogHeader>
<PurchaseOrderForm setOpen={setPurchaseOrderOpen} refreshData={handleRefreshData} />
</DialogContent>
</Dialog>
</div>
<PurchaseOrderDataTable
columns={columns} // Pass the generated columns array
data={purchaseOrders}
filterValue={filterValue}
onFilterChange={setFilterValue}
refreshData={handleRefreshData}
/>
{/* View Order Dialog */}
<Dialog open={viewDialogOpen} onOpenChange={setViewDialogOpen}>
<Dialog open={viewOrderDialogOpen} onOpenChange={setViewOrderDialogOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Purchase Order Details</DialogTitle>
<DialogDescription>Order ID: {selectedOrder?.id}</DialogDescription>
<DialogDescription>Order ID: {selectedOrderForView?.id}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<p><strong>Supplier:</strong> {selectedOrder?.supplierName}</p>
<p><strong>Order Date:</strong> {selectedOrder?.orderDate ? format(selectedOrder.orderDate, "PPP") : 'N/A'}</p>
<p><strong>Status:</strong> {selectedOrder?.status}</p>
<p><strong>Supplier:</strong> {selectedOrderForView?.supplierName}</p>
<p><strong>Order Date:</strong> {selectedOrderForView?.orderDate ? new Date(selectedOrderForView.orderDate).toLocaleDateString() : 'N/A'}</p>
<p><strong>Status:</strong> {selectedOrderForView?.status}</p>
<h4 className="font-semibold">Items:</h4>
<ul className="list-disc list-inside">
{selectedOrder?.items.map((item, index) => (
{selectedOrderForView?.items.map((item, index) => (
<li key={item.stockItemId}>
{item.itemName} (SKU: {item.sku}) - Quantity: {selectedOrder.quantities[index]}
{item.itemName} (SKU: {item.sku}) - Quantity: {selectedOrderForView.quantities[index]}
</li>
))}
</ul>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button variant="outline" onClick={closeViewDialog}>Close</Button>
<Button variant="outline" onClick={() => setViewOrderDialogOpen(false)}>Close</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
};
{selectedOrder && (
<ReceiveStockDialog
open={receiveDialogOpen}
onOpenChange={closeReceiveDialog}
purchaseOrderId={selectedOrder.id}
items={selectedOrder.items.map((item, idx) => ({
stockItemId: item.stockItemId,
orderedQuantity: selectedOrder.quantities[idx] || 0,
itemName: item.itemName,
}))}
refreshData={handleRefreshData}
/>
)}
</>
)
}
export default PurchaseOrders;