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

708 lines
40 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 { Badge } from '@/Components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/Components/ui/dialog';
import { PageProps } from '@/types';
import {
ChevronLeft, ClipboardList, Loader2, Plus, Save, X, Search, ChevronDown, CheckCircle2,
Lock, Package, Sparkles, AlertCircle, Info, Layers
} from 'lucide-react';
import { FormEvent, useMemo, useState, useEffect } from 'react';
import { MaterialCatalogModal, MaterialOption, MaterialGroupOption } from './MaterialCatalogModal';
interface RemainingEstimate {
material_ulid: string;
material_name: string;
unit: string;
quantity: number;
unit_cost: number;
estimated_qty?: number;
remaining_qty?: number;
}
interface ProjectOption {
id: number;
ulid: string;
name: string;
code: string;
client_name?: string | null;
location?: string | null;
status?: string | null;
start_date?: string | null;
target_end_date?: string | null;
contract_value?: string | number | null;
contractor?: { id: number; ulid: string; company_name: string } | null;
has_remaining_estimates?: boolean;
remaining_estimates?: RemainingEstimate[];
}
interface ItemEntry {
material_ulid: string;
quantity: string;
unit_cost: string;
is_unestimated?: boolean;
}
interface Props extends PageProps {
requisition?: any; // If editing
materials: MaterialOption[];
materialGroups: MaterialGroupOption[];
projects: ProjectOption[];
selectedProject?: ProjectOption | null;
prefilledItems?: RemainingEstimate[];
hasRemainingEstimates?: boolean;
}
interface ProjectLookupProps {
projects: ProjectOption[];
selectedId: string;
onSelect: (ulid: string) => void;
}
function ProjectLookup({ projects, selectedId, onSelect }: ProjectLookupProps) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const [tempSelectedId, setTempSelectedId] = useState(selectedId);
const filteredProjects = useMemo(() => {
if (!search) return projects;
const lowerSearch = search.toLowerCase();
return projects.filter(p =>
p.name.toLowerCase().includes(lowerSearch) ||
p.code.toLowerCase().includes(lowerSearch)
);
}, [projects, search]);
const handleConfirm = () => {
onSelect(tempSelectedId);
setOpen(false);
};
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (newOpen) {
setTempSelectedId(selectedId);
setSearch('');
}
};
const selectedProject = projects.find(p => p.ulid === selectedId);
return (
<>
<Button
variant="outline"
role="combobox"
onClick={() => handleOpenChange(true)}
className={`w-full justify-between font-normal mt-1 border-slate-250 ${!selectedId ? 'text-muted-foreground' : ''}`}
type="button"
>
{selectedProject ? (
<span className="truncate font-medium text-slate-800">{selectedProject.code} - {selectedProject.name}</span>
) : (
"Select Project"
)}
<ChevronDown className="h-4 w-4 shrink-0 opacity-50 ml-2" />
</Button>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-5xl lg:max-w-6xl w-full max-h-[85vh] flex flex-col p-0 gap-0">
<DialogHeader className="px-6 py-5 border-b">
<DialogTitle>Select Project</DialogTitle>
</DialogHeader>
<div className="px-6 py-5 border-b bg-slate-50/50">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="Search by project name or code..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9 bg-white border-slate-200"
autoFocus
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 max-h-[50vh]">
{filteredProjects.length === 0 ? (
<div className="text-center py-12 text-slate-500">
No projects found matching your search.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredProjects.map((proj) => {
const isSelected = tempSelectedId === proj.ulid;
return (
<div
key={proj.id}
onClick={() => setTempSelectedId(proj.ulid)}
className={`relative flex flex-col justify-between p-4 rounded-xl border-2 transition-all cursor-pointer select-none text-left ${
isSelected
? 'border-indigo-600 bg-indigo-50/30 shadow-sm ring-1 ring-indigo-600/20'
: 'border-slate-100 bg-white hover:border-slate-300 hover:shadow-sm'
}`}
>
<div>
<div className="flex items-start justify-between gap-2 mb-2">
<Badge variant="outline" className="font-mono text-[10px] tracking-wider text-slate-500 border-slate-200">
{proj.code}
</Badge>
{isSelected && (
<CheckCircle2 className="h-4 w-4 text-indigo-600 shrink-0" />
)}
</div>
<h4 className="font-semibold text-slate-900 text-sm line-clamp-1 mb-1" title={proj.name}>
{proj.name}
</h4>
<p className="text-xs text-slate-500 line-clamp-1 mb-3">
Client: {proj.client_name || 'N/A'}
</p>
<div className="flex items-center gap-1.5 flex-wrap">
{proj.has_remaining_estimates ? (
<Badge variant="secondary" className="text-[10px] bg-emerald-50 text-emerald-700 border-emerald-200">
{proj.remaining_estimates?.length ?? 0} Est. Items Available
</Badge>
) : (
<Badge variant="secondary" className="text-[10px] bg-slate-100 text-slate-600 border-slate-200">
Estimates Exhausted
</Badge>
)}
</div>
</div>
{proj.contract_value && (
<div className="mt-4 pt-3 border-t border-slate-100 flex items-center justify-between">
<span className="text-[10px] text-slate-400 font-medium uppercase tracking-wider whitespace-nowrap">Contract Value</span>
<span className="text-xs font-semibold text-slate-800 font-mono whitespace-nowrap">
{new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(proj.contract_value))}
</span>
</div>
)}
</div>
);
})}
</div>
)}
</div>
<DialogFooter className="mx-0 mb-0 px-6 py-4 border-t bg-slate-50/30 flex justify-end gap-2">
<Button variant="outline" type="button" onClick={() => setOpen(false)}>Cancel</Button>
<Button type="button" onClick={handleConfirm} disabled={!tempSelectedId} className="bg-indigo-600 hover:bg-indigo-700 text-white">Confirm Selection</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default function Form({
requisition,
materials,
materialGroups,
projects = [],
selectedProject,
prefilledItems = [],
hasRemainingEstimates = false
}: Props) {
const isEditing = !!requisition;
const [catalogModalOpen, setCatalogModalOpen] = useState(false);
const [activeRowIndex, setActiveRowIndex] = useState<number | null>(null);
const initialProjectUlid = requisition?.project?.ulid || selectedProject?.ulid || '';
const activeProject = projects.find(p => p.ulid === initialProjectUlid);
const activeHasRemaining = activeProject?.has_remaining_estimates ?? hasRemainingEstimates ?? (prefilledItems && prefilledItems.length > 0);
const activeRemainingEstimates = activeProject?.remaining_estimates ?? prefilledItems ?? [];
const initialRequisitionType = requisition?.requisition_type || (activeHasRemaining ? 'estimated' : 'unestimated');
const { data, setData, post, put, processing, errors } = useForm({
project_ulid: initialProjectUlid,
requisition_type: initialRequisitionType,
notes: requisition?.notes || '',
items: (requisition?.items || (
initialRequisitionType === 'estimated' && activeRemainingEstimates.length > 0
? activeRemainingEstimates.map(i => ({
material_ulid: i.material_ulid,
quantity: String(i.quantity),
unit_cost: String(i.unit_cost),
is_unestimated: false,
}))
: [{ material_ulid: '', quantity: '', unit_cost: '', is_unestimated: initialRequisitionType === 'unestimated' }]
)).map((i: any) => ({
material_ulid: i.material?.ulid || i.material_ulid || '',
quantity: i.quantity || '',
unit_cost: i.unit_cost || '',
is_unestimated: i.is_unestimated ?? (initialRequisitionType === 'unestimated'),
})) as ItemEntry[],
});
const addItem = () => setData('items', [...data.items, { material_ulid: '', quantity: '', unit_cost: '', is_unestimated: data.requisition_type === 'unestimated' }]);
const removeItem = (idx: number) => setData('items', data.items.filter((_, i) => i !== idx));
const updateItem = (idx: number, field: keyof ItemEntry, value: any) => {
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 || '',
is_unestimated: data.requisition_type === 'unestimated',
}));
const updatedItems = [...data.items];
if (activeRowIndex !== null) {
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 {
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) => {
const newItems: ItemEntry[] = [{
material_ulid: group.ulid,
quantity: String(quantityMultiplier),
unit_cost: '0',
is_unestimated: data.requisition_type === 'unestimated',
}];
const updatedItems = [...data.items];
if (activeRowIndex !== null) {
if (!updatedItems[activeRowIndex].material_ulid && !updatedItems[activeRowIndex].quantity) {
updatedItems.splice(activeRowIndex, 1, ...newItems);
} else {
updatedItems.push(...newItems);
}
} else {
updatedItems.push(...newItems);
}
setData('items', updatedItems);
};
const handleProjectChange = (ulid: string) => {
const proj = projects.find(p => p.ulid === ulid);
const hasRem = proj?.has_remaining_estimates ?? false;
const newType = hasRem ? 'estimated' : 'unestimated';
setData(prev => ({
...prev,
project_ulid: ulid,
requisition_type: newType,
items: hasRem && proj?.remaining_estimates && proj.remaining_estimates.length > 0
? proj.remaining_estimates.map(i => ({
material_ulid: i.material_ulid,
quantity: String(i.quantity),
unit_cost: String(i.unit_cost),
is_unestimated: false,
}))
: [{ material_ulid: '', quantity: '', unit_cost: '', is_unestimated: true }]
}));
router.get(route('requisitions.create'), { project_ulid: ulid }, {
preserveState: true,
replace: true
});
};
const handleModeSelect = (mode: 'estimated' | 'unestimated') => {
if (mode === 'estimated' && !activeHasRemaining) {
return; // Locked
}
if (mode === 'estimated') {
const itemsToPopulate = activeRemainingEstimates.map(i => ({
material_ulid: i.material_ulid,
quantity: String(i.quantity),
unit_cost: String(i.unit_cost),
is_unestimated: false,
}));
setData(prev => ({
...prev,
requisition_type: 'estimated',
items: itemsToPopulate.length > 0 ? itemsToPopulate : [{ material_ulid: '', quantity: '', unit_cost: '', is_unestimated: false }]
}));
} else {
setData(prev => ({
...prev,
requisition_type: 'unestimated',
items: [{ material_ulid: '', quantity: '', unit_cost: '', is_unestimated: true }]
}));
}
};
// Calculate total cost and unestimated cost in real time
const totalCost = useMemo(() => {
return data.items.reduce((sum, item) => {
const q = parseFloat(item.quantity) || 0;
const c = parseFloat(item.unit_cost) || 0;
return sum + (q * c);
}, 0);
}, [data.items]);
const unestimatedCost = useMemo(() => {
return data.items.reduce((sum, item) => {
if (data.requisition_type === 'unestimated' || item.is_unestimated) {
const q = parseFloat(item.quantity) || 0;
const c = parseFloat(item.unit_cost) || 0;
return sum + (q * c);
}
return sum;
}, 0);
}, [data.items, data.requisition_type]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const payloadItems = data.items
.filter(i => i.material_ulid && i.quantity)
.map(i => ({
material_ulid: i.material_ulid,
quantity: parseFloat(i.quantity),
unit_cost: parseFloat(i.unit_cost) || 0,
is_unestimated: data.requisition_type === 'unestimated' || !!i.is_unestimated,
}));
const payload = {
project_ulid: data.project_ulid,
requisition_type: data.requisition_type,
notes: data.notes,
items: payloadItems as any,
};
if (isEditing) {
router.put(route('requisitions.update', requisition.ulid), payload);
} else {
router.post(route('requisitions.store'), payload);
}
};
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 className="shadow-sm border-slate-200">
<CardHeader className="bg-slate-50/50 border-b pb-4">
<CardTitle className="text-base text-slate-800 flex items-center gap-2">
<Layers className="h-5 w-5 text-indigo-600" /> Requisition Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-6 pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="project_ulid" className="font-semibold text-slate-700">Project <span className="text-red-500">*</span></Label>
{isEditing ? (
<div className="mt-1 border border-slate-200 rounded-md px-3 py-2 bg-slate-50 text-slate-700 text-sm font-medium">
{requisition?.project?.code} - {requisition?.project?.name}
</div>
) : (
<ProjectLookup
projects={projects}
selectedId={data.project_ulid}
onSelect={handleProjectChange}
/>
)}
{errors.project_ulid && <p className="mt-1 text-sm text-red-500">{errors.project_ulid}</p>}
</div>
<div>
<Label htmlFor="notes" className="font-semibold text-slate-700">Optional Notes</Label>
<Input id="notes" className="mt-1 border-slate-200" value={data.notes || ''} onChange={e => setData('notes', e.target.value)} placeholder="E.g. urgent delivery for concrete foundation..." />
</div>
</div>
{/* Mode Selection Cards (Estimated vs Unestimated Materials) */}
{data.project_ulid && (
<div className="space-y-2 pt-2">
<Label className="text-sm font-semibold text-slate-800">Select Requisition Mode <span className="text-red-500">*</span></Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-1">
{/* Option 1: Estimated Materials */}
<div
onClick={() => handleModeSelect('estimated')}
className={`relative p-4 rounded-xl border-2 transition-all select-none ${
data.requisition_type === 'estimated'
? 'border-indigo-600 bg-indigo-50/40 shadow-sm ring-1 ring-indigo-600/30'
: activeHasRemaining
? 'border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50/50 cursor-pointer'
: 'border-slate-200 bg-slate-100/70 opacity-80 cursor-not-allowed'
}`}
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className={`h-10 w-10 rounded-lg flex items-center justify-center ${
data.requisition_type === 'estimated' ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600'
}`}>
<Package className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-slate-900 text-sm flex items-center gap-2">
Estimated Materials
{data.requisition_type === 'estimated' && (
<CheckCircle2 className="h-4 w-4 text-indigo-600 inline" />
)}
</h4>
<p className="text-xs text-slate-500 mt-0.5">
Requisition from the project's baseline estimation
</p>
</div>
</div>
{activeHasRemaining ? (
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200 font-medium text-[11px]">
{activeRemainingEstimates.length} items remaining
</Badge>
) : (
<Badge variant="outline" className="bg-slate-200 text-slate-600 border-slate-300 font-medium text-[11px] flex items-center gap-1">
<Lock className="h-3 w-3" /> Locked
</Badge>
)}
</div>
{!activeHasRemaining && (
<div className="mt-3 pt-2.5 border-t border-slate-200/80 flex items-start gap-1.5 text-xs text-amber-700 font-medium">
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5 text-amber-600" />
<span>All estimated materials have already been requested on prior MRs. Please select <strong>Unestimated Materials</strong> to request additional items.</span>
</div>
)}
</div>
{/* Option 2: Unestimated Materials */}
<div
onClick={() => handleModeSelect('unestimated')}
className={`relative p-4 rounded-xl border-2 transition-all cursor-pointer select-none ${
data.requisition_type === 'unestimated'
? 'border-indigo-600 bg-indigo-50/40 shadow-sm ring-1 ring-indigo-600/30'
: 'border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50/50'
}`}
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className={`h-10 w-10 rounded-lg flex items-center justify-center ${
data.requisition_type === 'unestimated' ? 'bg-indigo-600 text-white' : 'bg-purple-50 text-purple-600'
}`}>
<Sparkles className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-slate-900 text-sm flex items-center gap-2">
Unestimated Materials
{data.requisition_type === 'unestimated' && (
<CheckCircle2 className="h-4 w-4 text-indigo-600 inline" />
)}
</h4>
<p className="text-xs text-slate-500 mt-0.5">
Request missed, supplemental, or urgent items
</p>
</div>
</div>
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-200 font-medium text-[11px]">
Supplemental
</Badge>
</div>
<div className="mt-3 pt-2.5 border-t border-slate-100 flex items-center gap-1.5 text-xs text-indigo-700 font-medium">
<Info className="h-4 w-4 shrink-0 text-indigo-600" />
<span>Prices will be automatically added to the Project Capitalization Cap.</span>
</div>
</div>
</div>
</div>
)}
{/* Materials Table Section */}
<div className="pt-4 border-t space-y-3">
<div className="flex items-center justify-between">
<div>
<Label className="text-sm font-semibold text-slate-800">
{data.requisition_type === 'estimated' ? 'Estimated Items to Requisition' : 'Unestimated Materials Request List'}
</Label>
<p className="text-xs text-slate-500 mt-0.5">
{data.requisition_type === 'estimated'
? 'Populated with remaining unrequisitioned project balances.'
: 'Add materials from catalog that are required for project execution.'}
</p>
</div>
<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.5 h-4 w-4" /> Browse Catalog
</Button>
<Button type="button" variant="outline" size="sm" onClick={addItem}>
<Plus className="mr-1.5 h-4 w-4" /> Add Row
</Button>
</div>
</div>
{errors.items && <p className="text-sm text-red-500">{errors.items}</p>}
<div className="space-y-3">
{data.items.map((item, idx) => {
const selectedMat = materials.find(m => m.ulid === item.material_ulid);
return (
<div key={idx} className="flex flex-wrap md:flex-nowrap items-end gap-2 rounded-xl border border-slate-200 p-4 bg-slate-50/50 hover:bg-slate-50 transition-colors">
<div className="flex-1 w-full md:w-auto min-w-[220px]">
<Label className="text-xs font-medium text-slate-600">Material <span className="text-red-500">*</span></Label>
{item.material_ulid ? (
<div className="flex items-center gap-2 mt-1">
<div className="flex-1 border border-slate-200 rounded-lg px-3 py-2 bg-white text-sm font-medium text-slate-800 flex items-center justify-between">
<span className="truncate">{selectedMat?.name || 'Unknown Material'}</span>
{selectedMat?.unit && (
<span className="text-xs text-slate-400 font-normal ml-2">({selectedMat.unit})</span>
)}
</div>
<Button type="button" variant="outline" size="sm" onClick={() => openCatalogModal(idx)} className="h-9 px-3">
Change
</Button>
</div>
) : (
<Button
type="button"
variant="outline"
className="w-full justify-start mt-1 text-slate-500 h-9 bg-white border-slate-200"
onClick={() => openCatalogModal(idx)}
>
<Search className="h-4 w-4 mr-2 text-slate-400" />
Select Material from Catalog...
</Button>
)}
</div>
<div className="w-28">
<Label className="text-xs font-medium text-slate-600">Quantity <span className="text-red-500">*</span></Label>
<Input
type="number"
step="0.01"
min="0.01"
value={item.quantity}
onChange={e => updateItem(idx, 'quantity', e.target.value)}
className="mt-1 bg-white border-slate-200"
placeholder="0.00"
/>
</div>
<div className="w-36">
<Label className="text-xs font-medium text-slate-600">Unit Cost (PHP)</Label>
<Input
type="number"
step="0.01"
min="0"
value={item.unit_cost}
onChange={e => updateItem(idx, 'unit_cost', e.target.value)}
className="mt-1 bg-white border-slate-200 font-mono text-sm"
placeholder="0.00"
/>
</div>
<div className="w-32 hidden md:block text-right pr-2">
<Label className="text-xs text-slate-400 block mb-1">Line Total</Label>
<div className="text-sm font-semibold font-mono text-slate-800 py-1.5">
{new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(
(parseFloat(item.quantity) || 0) * (parseFloat(item.unit_cost) || 0)
)}
</div>
</div>
{data.items.length > 1 && (
<div className="w-10 flex justify-end pb-0.5">
<Button type="button" variant="ghost" size="icon" onClick={() => removeItem(idx)} className="text-slate-400 hover:text-red-600">
<X className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
})}
</div>
{/* Financial Summary Footer */}
<div className="mt-4 p-4 rounded-xl bg-slate-100/80 border border-slate-200 flex flex-col md:flex-row items-start md:items-center justify-between gap-3">
<div>
<span className="text-xs text-slate-500 font-medium">Requisition Total Cost:</span>
<div className="text-lg font-bold font-mono text-slate-900">
{new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(totalCost)}
</div>
</div>
{unestimatedCost > 0 && (
<div className="text-right">
<span className="text-xs text-purple-700 font-medium flex items-center gap-1">
<Sparkles className="h-3.5 w-3.5" /> Added to Project Capitalization:
</span>
<div className="text-sm font-bold font-mono text-purple-900">
+{new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(unestimatedCost)}
</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)} className="bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm">
{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>
);
}