Files
GSB-Construction/Modules/MaterialLogistics/resources/js/Pages/Transfers/Create.tsx

336 lines
21 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useForm, Link } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Card, CardContent, CardHeader, CardTitle, CardFooter, CardDescription } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
import { Textarea } from '@/Components/ui/textarea';
import { Trash2, ArrowRight, PackageOpen } from 'lucide-react';
import { Badge } from '@/Components/ui/badge';
interface InventoryItem {
id: number;
material_id: number;
material_name: string;
material_sku: string;
material_unit: string;
available_qty: number;
}
export default function Create({ projects }: { projects: any[] }) {
const [availableInventory, setAvailableInventory] = useState<InventoryItem[]>([]);
const [isLoadingInventory, setIsLoadingInventory] = useState(false);
// Local state for the inputs in the left container
const [transferInputs, setTransferInputs] = useState<Record<number, string>>({});
const projectItems = projects.map(p => ({ value: p.id.toString(), label: p.name }));
const { data, setData, post, processing, errors } = useForm({
from_project_id: '',
to_project_id: '',
notes: '',
items: [] as any[]
});
useEffect(() => {
if (data.from_project_id) {
setIsLoadingInventory(true);
fetch(`/material-transfers/project-inventory/${data.from_project_id}`)
.then(res => res.json())
.then(json => {
setAvailableInventory(json);
setData('items', []);
setTransferInputs({});
})
.finally(() => setIsLoadingInventory(false));
} else {
setAvailableInventory([]);
setData('items', []);
}
}, [data.from_project_id]);
const handleAddItem = (inv: InventoryItem) => {
const qtyStr = transferInputs[inv.id];
const qty = parseFloat(qtyStr);
if (!qty || qty <= 0 || qty > inv.available_qty) return;
const existingIndex = data.items.findIndex(i => i.material_id === inv.material_id);
if (existingIndex >= 0) {
const newItems = [...data.items];
newItems[existingIndex].requested_quantity = qty;
setData('items', newItems);
} else {
setData('items', [
...data.items,
{
material_id: inv.material_id,
material_name: inv.material_name,
material_sku: inv.material_sku,
material_unit: inv.material_unit,
requested_quantity: qty,
notes: ''
}
]);
}
// Clear input after adding
setTransferInputs(prev => ({ ...prev, [inv.id]: '' }));
};
const handleRemoveItem = (index: number) => {
setData('items', data.items.filter((_, i) => i !== index));
};
const submit = (e: React.FormEvent) => {
e.preventDefault();
post(route('material-transfers.store'));
};
return (
<AuthenticatedLayout
header={
<div className="flex justify-between items-center">
<h2 className="font-semibold text-xl text-slate-800 dark:text-slate-200 leading-tight">
Create Material Transfer
</h2>
<Link href={route('material-transfers.index')}>
<Button variant="outline">Cancel</Button>
</Link>
</div>
}
>
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<form onSubmit={submit} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Route Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>From Project (Source)</Label>
<Select
value={data.from_project_id}
onValueChange={(val: string | null) => setData('from_project_id', val || '')}
/* @ts-ignore custom items prop for this project's select */
items={projectItems}
>
<SelectTrigger>
<SelectValue placeholder="Select origin project" />
</SelectTrigger>
<SelectContent>
{projects.map((p) => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
{errors.from_project_id && <p className="text-sm text-red-500">{errors.from_project_id}</p>}
</div>
<div className="space-y-2">
<Label>To Project (Destination)</Label>
<Select
value={data.to_project_id}
onValueChange={(val: string | null) => setData('to_project_id', val || '')}
/* @ts-ignore custom items prop for this project's select */
items={projectItems}
>
<SelectTrigger>
<SelectValue placeholder="Select destination project" />
</SelectTrigger>
<SelectContent>
{projects.map((p) => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
{errors.to_project_id && <p className="text-sm text-red-500">{errors.to_project_id}</p>}
</div>
</div>
<div className="space-y-2">
<Label>Notes / Reason for Transfer</Label>
<Textarea
value={data.notes}
onChange={e => setData('notes', e.target.value)}
placeholder="Optional instructions or reason"
/>
{errors.notes && <p className="text-sm text-red-500">{errors.notes}</p>}
</div>
</CardContent>
</Card>
{/* Dual Container Interface */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* LEFT CONTAINER: SOURCE INVENTORY */}
<Card className="shadow-sm border-indigo-100">
<CardHeader className="bg-slate-50 dark:bg-slate-800/50 border-b">
<CardTitle className="text-lg flex items-center gap-2">
<PackageOpen className="h-5 w-5 text-indigo-500" />
Source Inventory
</CardTitle>
<CardDescription>
Available items in the selected origin project.
</CardDescription>
</CardHeader>
<CardContent className="p-0">
{!data.from_project_id ? (
<div className="p-8 text-center text-slate-500 text-sm">
Select a "From Project" to view its inventory.
</div>
) : isLoadingInventory ? (
<div className="p-8 text-center text-slate-500 text-sm">
Loading inventory...
</div>
) : availableInventory.length === 0 ? (
<div className="p-8 text-center text-slate-500 text-sm">
No available inventory found for this project.
</div>
) : (
<div className="max-h-[500px] overflow-y-auto">
<table className="w-full text-sm text-left">
<thead className="sticky top-0 bg-white dark:bg-slate-900 border-b shadow-sm z-10">
<tr>
<th className="p-3 font-medium text-slate-500">Material</th>
<th className="p-3 font-medium text-slate-500 text-right">Available</th>
<th className="p-3 font-medium text-slate-500 text-right w-32">Transfer Qty</th>
<th className="p-3 w-16"></th>
</tr>
</thead>
<tbody className="divide-y">
{availableInventory.map((inv) => {
const inTransferList = data.items.find(i => i.material_id === inv.material_id);
const currentInput = transferInputs[inv.id] || '';
return (
<tr key={inv.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
<td className="p-3">
<div className="font-medium">{inv.material_name}</div>
<div className="text-xs text-slate-500">{inv.material_sku}</div>
</td>
<td className="p-3 text-right">
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 hover:bg-emerald-50">
{inv.available_qty} {inv.material_unit}
</Badge>
</td>
<td className="p-3 text-right">
<Input
type="number"
min="0.01"
max={inv.available_qty}
step="0.01"
className="h-8 text-right"
placeholder={inTransferList ? String(inTransferList.requested_quantity) : '0'}
value={currentInput}
onChange={e => setTransferInputs(prev => ({ ...prev, [inv.id]: e.target.value }))}
/>
</td>
<td className="p-3 text-right">
<Button
type="button"
size="sm"
variant="ghost"
className="h-8 w-8 p-0 text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50"
disabled={!currentInput || parseFloat(currentInput) <= 0 || parseFloat(currentInput) > inv.available_qty}
onClick={() => handleAddItem(inv)}
>
<ArrowRight className="h-4 w-4" />
</Button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
{/* RIGHT CONTAINER: TRANSFER LIST */}
<Card className="shadow-sm border-blue-100 flex flex-col h-full">
<CardHeader className="bg-blue-50/50 dark:bg-blue-900/10 border-b">
<div className="flex justify-between items-center">
<div>
<CardTitle className="text-lg flex items-center gap-2">
Transfer List
</CardTitle>
<CardDescription>
Items staged for transfer to the destination.
</CardDescription>
</div>
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
{data.items.length} Items
</Badge>
</div>
</CardHeader>
<CardContent className="p-0 flex-1 relative">
{data.items.length === 0 ? (
<div className="p-12 text-center flex flex-col items-center justify-center text-slate-500">
<div className="h-16 w-16 bg-slate-100 rounded-full flex items-center justify-center mb-4">
<ArrowRight className="h-8 w-8 text-slate-300" />
</div>
<p className="text-sm">Select items from the source inventory to transfer them.</p>
</div>
) : (
<div className="max-h-[500px] overflow-y-auto">
<table className="w-full text-sm text-left">
<thead className="sticky top-0 bg-white dark:bg-slate-900 border-b shadow-sm z-10">
<tr>
<th className="p-3 font-medium text-slate-500">Material</th>
<th className="p-3 font-medium text-slate-500 text-right">Transferring</th>
<th className="p-3 w-16"></th>
</tr>
</thead>
<tbody className="divide-y">
{data.items.map((item, index) => (
<tr key={index} className="bg-blue-50/20">
<td className="p-3">
<div className="font-medium text-blue-900 dark:text-blue-200">{item.material_name}</div>
<div className="text-xs text-slate-500">{item.material_sku}</div>
</td>
<td className="p-3 text-right">
<div className="font-semibold text-lg">{item.requested_quantity}</div>
<div className="text-xs text-slate-500">{item.material_unit}</div>
</td>
<td className="p-3 text-right">
<Button
type="button"
size="sm"
variant="ghost"
className="h-8 w-8 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={() => handleRemoveItem(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
<CardFooter className="bg-slate-50 dark:bg-slate-800/50 border-t p-4 flex justify-between">
<div className="text-sm text-slate-500">
{/* @ts-ignore */}
{errors.items && <span className="text-red-500 font-medium">Please add at least one valid item.</span>}
</div>
<Button type="submit" disabled={processing || data.items.length === 0} className="w-full sm:w-auto">
Create Draft Transfer
</Button>
</CardFooter>
</Card>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
);
}