Files
GSB-Construction/Modules/MaterialLogistics/resources/js/Pages/Requisitions/Form.tsx

246 lines
13 KiB
TypeScript

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router, useForm } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/Components/ui/select';
import { PageProps } from '@/types';
import {
ChevronLeft, ClipboardList, Loader2, Plus, Save, X, Search
} from 'lucide-react';
import { FormEvent, useMemo, useState } from 'react';
import { MaterialCatalogModal, MaterialOption, MaterialGroupOption } from './MaterialCatalogModal';
interface ProjectOption {
id: number; ulid: string; name: string; code: string;
}
interface ItemEntry { material_ulid: string; quantity: string; unit_cost: string; }
interface Props extends PageProps {
requisition?: any; // If editing
materials: MaterialOption[];
materialGroups: MaterialGroupOption[];
}
export default function Form({ requisition, materials, materialGroups }: Props) {
const isEditing = !!requisition;
const [catalogModalOpen, setCatalogModalOpen] = useState(false);
const [activeRowIndex, setActiveRowIndex] = useState<number | null>(null);
const { data, setData, post, put, processing, errors } = useForm({
notes: requisition?.notes || '',
items: (requisition?.items || [{ material_ulid: '', quantity: '', unit_cost: '' }]).map((i: any) => ({
material_ulid: i.material?.ulid || i.material_ulid || '',
quantity: i.quantity || '',
unit_cost: i.unit_cost || '',
})) as ItemEntry[],
});
const materialSelectItems = useMemo(() => materials.map(m => ({ value: m.ulid, label: `${m.name} (${m.unit})` })), [materials]);
const addItem = () => setData('items', [...data.items, { material_ulid: '', quantity: '', unit_cost: '' }]);
const removeItem = (idx: number) => setData('items', data.items.filter((_, i) => i !== idx));
const updateItem = (idx: number, field: keyof ItemEntry, value: string) => {
setData('items', data.items.map((item, i) => i === idx ? { ...item, [field]: value } : item));
};
const openCatalogModal = (idx: number | null = null) => {
setActiveRowIndex(idx);
setCatalogModalOpen(true);
};
const handleCatalogMultipleSelect = (selectedMaterials: MaterialOption[]) => {
if (selectedMaterials.length === 0) return;
const newItems: ItemEntry[] = selectedMaterials.map(m => ({
material_ulid: m.ulid,
quantity: '1',
unit_cost: m.unit_cost || ''
}));
const updatedItems = [...data.items];
if (activeRowIndex !== null) {
// Replace the specific row with the first selected item, then append the rest
const existingRow = updatedItems[activeRowIndex];
if (!existingRow.material_ulid && !existingRow.quantity && selectedMaterials.length === 1) {
updatedItems[activeRowIndex] = newItems[0];
} else if (!existingRow.material_ulid && !existingRow.quantity) {
updatedItems.splice(activeRowIndex, 1, ...newItems);
} else {
updatedItems[activeRowIndex] = { ...existingRow, material_ulid: newItems[0].material_ulid, unit_cost: newItems[0].unit_cost };
if (newItems.length > 1) {
updatedItems.push(...newItems.slice(1));
}
}
} else {
// Check if there's a single empty item at the end to reuse
const lastItem = updatedItems[updatedItems.length - 1];
if (updatedItems.length === 1 && !lastItem.material_ulid && !lastItem.quantity) {
updatedItems.splice(0, 1, ...newItems);
} else {
updatedItems.push(...newItems);
}
}
setData('items', updatedItems);
};
const handleCatalogKitSelect = (group: MaterialGroupOption, quantityMultiplier: number) => {
// Group is now the Kit itself! We add it as a single line item to retain the 'container' context
const newItems: ItemEntry[] = [{
material_ulid: group.ulid,
quantity: String(quantityMultiplier),
unit_cost: '0' // Unit cost will be read from the item if it has one, or user can input
}];
const updatedItems = [...data.items];
if (activeRowIndex !== null) {
// If the active row is completely empty, replace it, then append the rest
if (!updatedItems[activeRowIndex].material_ulid && !updatedItems[activeRowIndex].quantity) {
updatedItems.splice(activeRowIndex, 1, ...newItems);
} else {
// Otherwise just append to the end of the array
updatedItems.push(...newItems);
}
} else {
updatedItems.push(...newItems);
}
setData('items', updatedItems);
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
// filter out empty items
const payloadItems = data.items.filter(i => i.material_ulid && i.quantity);
setData('items', payloadItems);
if (isEditing) {
put(route('requisitions.update', requisition.ulid));
} else {
post(route('requisitions.store'));
}
};
return (
<AuthenticatedLayout
header={
<div className="flex items-center gap-4">
<Link href={route('requisitions.index')}>
<Button variant="ghost" size="icon"><ChevronLeft className="h-5 w-5" /></Button>
</Link>
<ClipboardList className="h-5 w-5" />
<h2 className="text-xl font-semibold leading-tight text-gray-800">
{isEditing ? `Edit Requisition: ${requisition.document_number}` : 'Create Material Requisition'}
</h2>
</div>
}
>
<Head title={isEditing ? 'Edit Requisition' : 'New Requisition'} />
<div className="py-6"><div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle>Requisition Details</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
<div>
<Label>Optional Notes</Label>
<Input value={data.notes || ''} onChange={e => setData('notes', e.target.value)} placeholder="E.g. required immediately..." />
</div>
</div>
<div className="pt-4 border-t">
<div className="flex items-center justify-between mb-4">
<Label className="text-sm font-semibold">Materials / Items</Label>
<div className="flex gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => openCatalogModal(null)} className="text-indigo-600 border-indigo-200 hover:bg-indigo-50">
<Search className="mr-1 h-4 w-4" /> Browse Catalog
</Button>
<Button type="button" variant="outline" size="sm" onClick={addItem}>
<Plus className="mr-1 h-4 w-4" /> Add Empty Row
</Button>
</div>
</div>
{errors.items && <p className="mb-4 text-sm text-red-500">{errors.items}</p>}
<div className="space-y-3">
{data.items.map((item, idx) => (
<div key={idx} className="flex flex-wrap md:flex-nowrap items-end gap-2 rounded-md border p-4 bg-gray-50/50">
<div className="flex-1 w-full md:w-auto min-w-[200px]">
<Label className="text-xs text-gray-500">Material *</Label>
{item.material_ulid ? (
<div className="flex items-center gap-2 mt-1">
<div className="flex-1 border rounded-md px-3 py-2 bg-white text-sm">
{materials.find(m => m.ulid === item.material_ulid)?.name || 'Unknown Material'}
</div>
<Button type="button" variant="outline" size="sm" onClick={() => openCatalogModal(idx)}>
Change
</Button>
</div>
) : (
<Button
type="button"
variant="outline"
className="w-full justify-start mt-1 text-gray-500"
onClick={() => openCatalogModal(idx)}
>
<Search className="h-4 w-4 mr-2" />
Select Material...
</Button>
)}
</div>
<div className="w-24">
<Label className="text-xs text-gray-500">Qty *</Label>
<Input type="number" step="0.01" min="0.01" value={item.quantity}
onChange={e => updateItem(idx, 'quantity', e.target.value)} />
</div>
<div className="w-32">
<Label className="text-xs text-gray-500">Est. Unit Cost</Label>
<Input type="number" step="0.01" value={item.unit_cost}
onChange={e => updateItem(idx, 'unit_cost', e.target.value)} />
</div>
{data.items.length > 1 && (
<div className="w-10 flex justify-end">
<Button type="button" variant="ghost" size="icon" onClick={() => removeItem(idx)}>
<X className="h-5 w-5 text-red-500" />
</Button>
</div>
)}
</div>
))}
</div>
</div>
<div className="flex justify-end pt-6 border-t gap-2">
<Link href={route('requisitions.index')}>
<Button type="button" variant="ghost">Cancel</Button>
</Link>
<Button type="submit" disabled={processing || data.items.every(i => !i.material_ulid)}>
{processing ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Saving...</>
: <><Save className="mr-2 h-4 w-4" /> Save Requisition</>}
</Button>
</div>
</CardContent>
</Card>
</form>
</div></div>
<MaterialCatalogModal
open={catalogModalOpen}
onOpenChange={setCatalogModalOpen}
materials={materials}
materialGroups={materialGroups}
onSelectMaterials={handleCatalogMultipleSelect}
onAddKit={handleCatalogKitSelect}
/>
</AuthenticatedLayout>
);
}