[dyad] Added stock item selection and quantity deduction to Book Out form - wrote 3 file(s)
This commit is contained in:
parent
10b34db897
commit
837c8354ba
3 changed files with 161 additions and 28 deletions
|
|
@ -17,54 +17,101 @@ import {
|
|||
import { Input } from "@/components/ui/input"
|
||||
import { showSuccess, showError } from "@/utils/toast"
|
||||
import { CustomerCombobox } from "./CustomerCombobox"
|
||||
import { StockItemCombobox } from "./StockItemCombobox" // Import the new combobox
|
||||
import pb from "@/lib/pocketbase"
|
||||
|
||||
const formSchema = z.object({
|
||||
customer: z.string({ required_error: "Please select a customer." }),
|
||||
item: z.string().min(2, { message: "Item name must be at least 2 characters." }),
|
||||
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; // Added refreshData prop
|
||||
}
|
||||
|
||||
export function BookOutForm({ setOpen }: BookOutFormProps) {
|
||||
export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) {
|
||||
const [customerOptions, setCustomerOptions] = React.useState<{ value: string; label: string }[]>([]);
|
||||
const [stockItemOptions, setStockItemOptions] = React.useState<{ value: string; label: string; quantity: number }[]>([]);
|
||||
|
||||
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, {
|
||||
filter: 'quantity > 0', // Only show items with quantity > 0
|
||||
});
|
||||
const options = records.items.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
quantity: item.quantity,
|
||||
}));
|
||||
setStockItemOptions(options);
|
||||
} catch (error) {
|
||||
console.error("Error fetching stock items for combobox:", error);
|
||||
showError("Failed to load stock items.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchCustomers = async () => {
|
||||
try {
|
||||
const records = await pb.collection('customers').getList(1, 50);
|
||||
const options = records.items.map((customer: any) => ({
|
||||
value: customer.id, // Use ID as value for uniqueness
|
||||
label: customer.comapny_name, // Display company name
|
||||
}));
|
||||
setCustomerOptions(options);
|
||||
} catch (error) {
|
||||
console.error("Error fetching customers for combobox:", error);
|
||||
showError("Failed to load customers.");
|
||||
}
|
||||
};
|
||||
fetchCustomers();
|
||||
}, []);
|
||||
fetchStockItems();
|
||||
}, [fetchCustomers, fetchStockItems]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
item: "",
|
||||
quantity: 1,
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
try {
|
||||
// Here you would typically save the book-out record to PocketBase
|
||||
// For now, we'll just log it and show a success message.
|
||||
console.log("Book-out data:", values);
|
||||
showSuccess("Item booked out successfully!");
|
||||
// Fetch the latest stock item data to prevent race conditions
|
||||
const selectedStockItem = await pb.collection('stock_items').getOne(values.stockItemId);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
// Optionally, you could create a 'book_out_records' collection here
|
||||
// to log each book-out transaction with customer, item, quantity, date, etc.
|
||||
// For now, we'll just update the stock.
|
||||
|
||||
showSuccess(`Successfully booked out ${bookedOutQuantity} of ${selectedStockItem.name}!`);
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
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 item:", error);
|
||||
showError("Failed to book out item. Please try again.");
|
||||
|
|
@ -91,13 +138,15 @@ export function BookOutForm({ setOpen }: BookOutFormProps) {
|
|||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="item"
|
||||
name="stockItemId" // Changed name to stockItemId
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Item Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g., Wireless Mouse" {...field} />
|
||||
</FormControl>
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Item</FormLabel>
|
||||
<StockItemCombobox
|
||||
stockItems={stockItemOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
|
|||
79
src/components/book-out/StockItemCombobox.tsx
Normal file
79
src/components/book-out/StockItemCombobox.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"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,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
interface StockItemOption {
|
||||
value: string; // PocketBase ID of the stock item
|
||||
label: string; // Item name
|
||||
quantity: number; // Current quantity
|
||||
}
|
||||
|
||||
interface StockItemComboboxProps {
|
||||
stockItems: StockItemOption[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function StockItemCombobox({ stockItems, value, onChange }: StockItemComboboxProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{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-[--radix-popover-trigger-width] p-0">
|
||||
<Command>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,6 +13,11 @@ import {
|
|||
|
||||
const BookOut = () => {
|
||||
const [bookOutOpen, setBookOutOpen] = useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0); // State to trigger data refresh
|
||||
|
||||
const handleRefreshData = () => {
|
||||
setRefreshTrigger(prev => prev + 1); // Increment to trigger any future data fetching
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -35,7 +40,7 @@ const BookOut = () => {
|
|||
Fill in the details below to record an item being booked out.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<BookOutForm setOpen={setBookOutOpen} />
|
||||
<BookOutForm setOpen={setBookOutOpen} refreshData={handleRefreshData} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue