Reverted all changes back to version 276be2d4b3c75c348a414629733163508a479a4f

This commit is contained in:
[dyad] 2025-09-12 16:12:47 +02:00
parent 4e6f5a2fea
commit ecbe200e1d
3 changed files with 163 additions and 255 deletions

View file

@ -4,7 +4,6 @@ import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { useState, useEffect, useCallback } from "react"
import { Button } from "@/components/ui/button"
import {
@ -16,247 +15,160 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { showSuccess, showError } from "@/utils/toast"
import { CustomerCombobox } from "./CustomerCombobox"
import { StockItemCombobox } from "./StockItemCombobox" // Import the new combobox
import pb from "@/lib/pocketbase"
import { StockItemCombobox } from "@/components/book-out/StockItemCombobox" // Assuming this path is correct
// Define the schema for a single item in the book out form
const bookOutItemSchema = z.object({
stockItemId: z.string().min(1, { message: "Stock item ID is required." }),
itemName: z.string().min(1, { message: "Item name is required." }),
quantity: z.number().int().positive(),
sku: z.string(),
})
// Define the type for the items array
type BookOutItemSchemaType = z.infer<typeof bookOutItemSchema>;
// Define the schema for the book out form
const bookOutFormSchema = z.object({
bookOutDate: z.date({ required_error: "Book out date is required." }),
items: z.array(bookOutItemSchema).min(1, { message: "At least one item is required." }),
const formSchema = z.object({
customer: z.string({ required_error: "Please select a customer." }),
stockItemId: z.string({ required_error: "Please select an item." }), // Changed from 'item' to 'stockItemId'
quantity: z.coerce.number().int().positive({ message: "Quantity must be a positive number." }),
})
interface BookOutFormProps {
setOpen: (open: boolean) => void
refreshData: () => void
}
// Define the type for a stock item
type StockItem = {
id: string
name: string
sku: string
costPrice: number // Although not used in BookOut, keeping for consistency if needed later
setOpen: (open: boolean) => void;
refreshData: () => void; // Added refreshData prop
}
export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) {
const [stockItems, setStockItems] = useState<StockItem[]>([]) // State to hold stock items
const [customerOptions, setCustomerOptions] = React.useState<{ value: string; label: string }[]>([]);
const [stockItemOptions, setStockItemOptions] = React.useState<{ value: string; label: string; quantity: number }[]>([]);
// Fetch stock items when the component mounts
const fetchStockItems = useCallback(async () => {
const fetchCustomers = React.useCallback(async () => {
try {
const records = await pb.collection('customers').getList(1, 50);
const options = records.items.map((customer: any) => ({
value: customer.id,
label: customer.comapny_name,
}));
setCustomerOptions(options);
} catch (error) {
console.error("Error fetching customers for combobox:", error);
showError("Failed to load customers.");
}
}, []);
const fetchStockItems = React.useCallback(async () => {
try {
const records = await pb.collection('stock_items').getList(1, 50, {
sort: '-created',
filter: 'quantity > 0', // Only show items with quantity > 0
});
// Ensure the fetched data conforms to the StockItem type
const formattedStockItems: StockItem[] = records.items.map((item: any) => ({
id: item.id,
name: item.name,
sku: item.sku,
costPrice: item.cost_price, // Assuming cost_price is the field in stock_items
const options = records.items.map((item: any) => ({
value: item.id,
label: item.name,
quantity: item.quantity,
}));
setStockItems(formattedStockItems);
setStockItemOptions(options);
} catch (error) {
console.error("Error fetching stock items:", error);
console.error("Error fetching stock items for combobox:", error);
showError("Failed to load stock items.");
}
}, []);
useEffect(() => {
React.useEffect(() => {
fetchCustomers();
fetchStockItems();
}, [fetchStockItems]);
}, [fetchCustomers, fetchStockItems]);
const form = useForm<z.infer<typeof bookOutFormSchema>>({
resolver: zodResolver(bookOutFormSchema),
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
bookOutDate: new Date(),
items: [{
stockItemId: "",
itemName: "",
quantity: 1,
sku: "",
}],
quantity: 1,
},
})
// Function to add a new item row
const addItem = () => {
const currentItems = form.getValues("items");
form.setValue("items", [...currentItems, {
stockItemId: "",
itemName: "",
quantity: 1,
sku: "",
}]);
}
// Function to remove an item row
const removeItem = (index: number) => {
const currentItems = form.getValues("items");
const newItems = currentItems.filter((_, i) => i !== index);
form.setValue("items", newItems);
}
// Function to handle changes in item fields
const handleItemChange = (index: number, field: keyof BookOutItemSchemaType, value: any) => {
const currentItems = form.getValues("items");
const newItems = [...currentItems];
const currentItem = newItems[index] as BookOutItemSchemaType;
if (field === "quantity") {
currentItem[field] = parseInt(value, 10) || 0;
} else {
currentItem[field] = value;
}
newItems[index] = currentItem;
form.setValue("items", newItems);
}
// Function to handle stock item selection from dropdown
const handleStockItemSelect = (index: number, selectedItemName: string) => {
const selectedStockItem = stockItems.find(item => item.name === selectedItemName);
if (selectedStockItem) {
// Update the form fields with the selected stock item's details
handleItemChange(index, "stockItemId", selectedStockItem.id);
handleItemChange(index, "itemName", selectedStockItem.name);
handleItemChange(index, "sku", selectedStockItem.sku);
// Quantity remains as is or defaults to 1 if not set
if (form.getValues("items")[index].quantity === 0) {
handleItemChange(index, "quantity", 1);
}
}
}
async function onSubmit(values: z.infer<typeof bookOutFormSchema>) {
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
// Prepare data for PocketBase
const bookOutData = {
bookOutDate: values.bookOutDate.toISOString(),
// PocketBase expects an array of item objects for the 'items' field
items: values.items.map(item => ({
stockItemId: item.stockItemId,
itemName: item.itemName,
quantity: item.quantity,
sku: item.sku,
})),
};
// Fetch the latest stock item data to prevent race conditions
const selectedStockItem = await pb.collection('stock_items').getOne(values.stockItemId);
await pb.collection('book_out_records').create(bookOutData);
showSuccess("Stock booked out successfully!");
if (!selectedStockItem) {
showError("Selected item not found.");
return;
}
const currentQuantity = selectedStockItem.quantity;
const bookedOutQuantity = values.quantity;
if (bookedOutQuantity > currentQuantity) {
showError(`Not enough stock. Only ${currentQuantity} available.`);
return;
}
const newQuantity = currentQuantity - bookedOutQuantity;
// Update the stock item quantity in PocketBase
await pb.collection('stock_items').update(values.stockItemId, {
quantity: newQuantity,
});
// Create a new record in the 'book_out_records' collection
await pb.collection('book_out_records').create({
customer: values.customer,
items: values.stockItemId, // 'items' is a relation field to 'stock_items'
quantity: bookedOutQuantity,
date_booked_out: new Date().toISOString(), // Record the current date/time
});
showSuccess(`Successfully booked out ${bookedOutQuantity} of ${selectedStockItem.name}!`);
form.reset();
setOpen(false);
refreshData();
fetchStockItems(); // Re-fetch stock items to update quantities in the combobox
refreshData(); // Trigger refresh in parent component if needed
} catch (error) {
console.error("Error booking out stock:", error);
showError("Failed to book out stock. Please check the form and try again.");
console.error("Error booking out item:", error);
showError("Failed to book out item. Please try again.");
}
}
// Filter stock items based on search term (if search functionality is added to Combobox)
// For now, we pass all stockItems and let the Combobox handle filtering internally.
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="bookOutDate"
name="customer"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Book Out Date</FormLabel>
<FormLabel>Customer</FormLabel>
<CustomerCombobox
customers={customerOptions}
value={field.value}
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="stockItemId" // Changed name to stockItemId
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Item</FormLabel>
<StockItemCombobox
stockItems={stockItemOptions}
value={field.value}
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel>Quantity</FormLabel>
<FormControl>
<Input
type="date"
{...field}
onChange={(e) => field.onChange(new Date(e.target.value))}
value={field.value instanceof Date ? field.value.toISOString().split('T')[0] : ''}
/>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Items Section */}
<div className="border p-4 rounded-md">
<h3 className="text-lg font-semibold mb-4">Items to Book Out</h3>
{form.getValues("items").map((item, index) => (
<div key={index} className="grid grid-cols-4 gap-4 mb-4 items-center">
<FormField
control={form.control}
name={`items.${index}.itemName`}
render={({ field }) => (
<FormItem className="col-span-2"> {/* Make item name take more space */}
<FormLabel>Item Name</FormLabel>
<StockItemCombobox
stockItems={stockItems} // Pass the fetched stock items here
selectedItemName={field.value}
onSelectItem={(selectedItemName) => handleStockItemSelect(index, selectedItemName)}
placeholder="Search items..."
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.sku`}
render={({ field }) => (
<FormItem>
<FormLabel>SKU</FormLabel>
<FormControl>
<Input {...field} readOnly placeholder="SKU" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.quantity`}
render={({ field }) => (
<FormItem>
<FormLabel>Quantity</FormLabel>
<FormControl>
<Input type="number" {...field} onChange={(e) => handleItemChange(index, "quantity", e.target.value)} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center justify-center mt-6"> {/* Adjust margin-top for alignment */}
{form.getValues("items").length > 1 && (
<Button type="button" variant="outline" size="icon" onClick={() => removeItem(index)}>
-
</Button>
)}
</div>
</div>
))}
<Button type="button" variant="outline" onClick={addItem} className="mt-2">
Add Item
</Button>
</div>
<Button type="submit" className="w-full">Book Out Stock</Button>
<Button type="submit" className="w-full">Book Out Item</Button>
</form>
</Form>
)

View file

@ -11,7 +11,6 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList, // Import CommandList for scrolling
} from "@/components/ui/command"
import {
Popover,
@ -19,24 +18,20 @@ import {
PopoverTrigger,
} from "@/components/ui/popover"
// Define the type for a stock item
type StockItem = {
id: string
name: string
sku: string
costPrice: number
interface StockItemOption {
value: string; // PocketBase ID of the stock item
label: string; // Item name
quantity: number; // Current quantity
}
interface StockItemComboboxProps {
stockItems: StockItem[];
selectedItemName: string;
onSelectItem: (itemName: string) => void;
placeholder: string;
stockItems: StockItemOption[];
value: string;
onChange: (value: string) => void;
}
export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, placeholder }: StockItemComboboxProps) {
export function StockItemCombobox({ stockItems, value, onChange }: StockItemComboboxProps) {
const [open, setOpen] = React.useState(false)
const [search, setSearch] = React.useState("")
return (
<Popover open={open} onOpenChange={setOpen}>
@ -47,48 +42,36 @@ export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem,
aria-expanded={open}
className="w-full justify-between"
>
{selectedItemName ? stockItems.find(item => item.name === selectedItemName)?.name : placeholder}
{value
? stockItems.find((item) => item.value === value)?.label
: "Select an item..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0"> {/* Adjust width as needed */}
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput
placeholder={placeholder}
value={search}
onValueChange={setSearch}
/>
<CommandList> {/* Use CommandList for scrolling */}
<CommandEmpty>No items found.</CommandEmpty>
<CommandGroup>
{stockItems
.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.sku.toLowerCase().includes(search.toLowerCase())
)
.map((item) => (
<CommandItem
key={item.id}
value={item.name} // Use item name for value
onSelect={(value) => {
onSelectItem(value);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedItemName === item.name ? "opacity-100" : "opacity-0"
)}
/>
<div>
<div>{item.name}</div>
<div className="text-xs text-muted-foreground">{item.sku}</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
<CommandInput placeholder="Search item..." />
<CommandEmpty>No item found.</CommandEmpty>
<CommandGroup>
{stockItems.map((item) => (
<CommandItem
key={item.value}
value={item.label} // Use label for search, but value for selection
onSelect={() => {
onChange(item.value === value ? "" : item.value);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === item.value ? "opacity-100" : "opacity-0"
)}
/>
{item.label} (Qty: {item.quantity})
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>

View file

@ -25,7 +25,6 @@ import {
} from "@/components/ui/select" // Import Select components
import { showSuccess, showError } from "@/utils/toast"
import pb from "@/lib/pocketbase"
import { StockItemCombobox } from "@/components/book-out/StockItemCombobox" // Import the new Combobox component
// Define the schema for a single item in the purchase order
const itemSchema = z.object({
@ -192,6 +191,8 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
supplier: selectedSupplier.id, // This will be the relation to the suppliers collection
orderDate: values.orderDate.toISOString(),
status: "Pending Approval",
// Ensure the structure matches what your PocketBase 'purchase_orders' collection expects for 'items'
// Based on your schema, it seems 'embedded_items' is used for storing item details directly.
embedded_items: values.items.map(item => ({
stockItemId: item.stockItemId,
itemName: item.itemName,
@ -199,7 +200,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
sku: item.sku,
costPrice: item.costPrice,
})),
quantities: values.items.map(item => item.quantity),
quantities: values.items.map(item => item.quantity), // Keep quantities array as well if needed elsewhere
};
await pb.collection('purchase_orders').create(purchaseOrderData);
@ -273,12 +274,23 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
render={({ field }) => (
<FormItem>
<FormLabel>Item Name</FormLabel>
<StockItemCombobox
stockItems={stockItems}
selectedItemName={field.value}
onSelectItem={(selectedItemName) => handleStockItemSelect(index, selectedItemName)}
placeholder="Search items..."
/>
<Select
onValueChange={(selectedValue) => handleStockItemSelect(index, selectedValue)}
value={field.value} // Set the current value of the item name
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select item" />
</SelectTrigger>
</FormControl>
<SelectContent>
{stockItems.map((stockItem) => (
<SelectItem key={stockItem.id} value={stockItem.name}>
{stockItem.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
@ -290,6 +302,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
<FormItem>
<FormLabel>SKU</FormLabel>
<FormControl>
{/* Display SKU, but it's managed by handleStockItemSelect */}
<Input {...field} readOnly placeholder="SKU" />
</FormControl>
<FormMessage />