336 lines
21 KiB
TypeScript
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>
|
|
);
|
|
}
|