ifixkart-admin/app/(admin)/inventory/page.tsx

747 lines
31 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from 'react';
import {
Plus,
Search,
RefreshCw,
Package,
TrendingUp,
AlertTriangle,
} from 'lucide-react';
import { toast } from 'sonner';
import { catalogService, PartResponse, SellableSkuRow, StockMovementResponse } from '@/services/api/catalogService';
import { SlideOver } from '@/components/ui/SlideOver';
import { CustomSelect } from '@/components/ui/CustomSelect';
import { TablePagination, TABLE_PAGE_SIZE, useClientPagination } from '@/components/ui/TablePagination';
type LedgerTab = 'skus' | 'parts' | 'movements';
export default function InventoryPage() {
const [activeTab, setActiveTab] = useState<LedgerTab>('skus');
const [parts, setParts] = useState<PartResponse[]>([]);
const [movements, setMovements] = useState<StockMovementResponse[]>([]);
const [skus, setSkus] = useState<SellableSkuRow[]>([]);
const [skuTotal, setSkuTotal] = useState(0);
const [skuPage, setSkuPage] = useState(1);
const skuLimit = TABLE_PAGE_SIZE;
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSkuSearch, setDebouncedSkuSearch] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [showPartModal, setShowPartModal] = useState(false);
const [partSku, setPartSku] = useState('');
const [partName, setPartName] = useState('');
const [partCost, setPartCost] = useState('');
const [partLowStock, setPartLowStock] = useState('3');
const [partSupplier, setPartSupplier] = useState('');
const [partBarcode, setPartBarcode] = useState('');
const [showAdjustModal, setShowAdjustModal] = useState(false);
const [adjustPartId, setAdjustPartId] = useState('');
const [adjustQty, setAdjustQty] = useState('');
const [adjustType, setAdjustType] = useState<'Adjustment' | 'Damage'>('Adjustment');
const [adjustReason, setAdjustReason] = useState('');
const [showSkuAdjustModal, setShowSkuAdjustModal] = useState(false);
const [skuAdjustRow, setSkuAdjustRow] = useState<SellableSkuRow | null>(null);
const [skuAdjustQty, setSkuAdjustQty] = useState('');
const [skuAdjustType, setSkuAdjustType] = useState<'RECEIPT' | 'ADJUSTMENT'>('RECEIPT');
const [skuAdjustNotes, setSkuAdjustNotes] = useState('');
const fetchPartsData = async () => {
setLoading(true);
try {
const [fetchedParts, fetchedMovements] = await Promise.all([
catalogService.getParts(),
catalogService.getStockHistory(),
]);
setParts(fetchedParts);
setMovements(fetchedMovements);
} catch (err: any) {
toast.error(err?.message || 'Failed to load inventory ledger');
} finally {
setLoading(false);
}
};
const fetchSkus = async () => {
setLoading(true);
try {
const result = await catalogService.getSellableSkus({
page: skuPage,
limit: skuLimit,
q: debouncedSkuSearch || undefined,
});
setSkus(result.items);
setSkuTotal(result.total);
} catch (err: any) {
toast.error(err?.message || 'Failed to load sellable SKUs');
} finally {
setLoading(false);
}
};
const fetchData = async () => {
if (activeTab === 'skus') {
await fetchSkus();
return;
}
await fetchPartsData();
};
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSkuSearch(searchQuery);
setSkuPage(1);
}, 400);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
fetchData();
}, [activeTab, skuPage, debouncedSkuSearch]);
const handleCreatePart = async (e: React.FormEvent) => {
e.preventDefault();
if (!partSku.trim() || !partName.trim() || !partCost) {
toast.error('SKU, name, and cost price are required');
return;
}
setIsSubmitting(true);
try {
const created = await catalogService.createPart({
sku: partSku.trim(),
name: partName.trim(),
cost_price: parseFloat(partCost),
low_stock_alert: parseInt(partLowStock) || 3,
supplier: partSupplier.trim() || undefined,
barcode: partBarcode.trim() || undefined,
});
setParts([...parts, created]);
toast.success(`Part "${created.sku}" added to inventory`);
setPartSku('');
setPartName('');
setPartCost('');
setPartLowStock('3');
setPartSupplier('');
setPartBarcode('');
setShowPartModal(false);
} catch (err: any) {
toast.error(err?.message || 'Failed to create inventory part');
} finally {
setIsSubmitting(false);
}
};
const handleAdjustStock = async (e: React.FormEvent) => {
e.preventDefault();
if (!adjustPartId || !adjustQty || !adjustReason.trim()) {
toast.error('Part, quantity, and reason are required');
return;
}
setIsSubmitting(true);
try {
const qtyVal = parseInt(adjustQty);
const payloadQty = adjustType === 'Damage' ? -Math.abs(qtyVal) : qtyVal;
await catalogService.adjustStock({
entity_type: 'part',
entity_id: adjustPartId,
movement_type: adjustType,
quantity: payloadQty,
reference_type: 'ManualAdjustment',
reference_id: adjustReason.trim(),
});
toast.success('Stock movement recorded');
setAdjustPartId('');
setAdjustQty('');
setAdjustReason('');
setShowAdjustModal(false);
fetchData();
} catch (err: any) {
toast.error(err?.message || 'Failed to log stock movement');
} finally {
setIsSubmitting(false);
}
};
const handleSkuAdjust = async (e: React.FormEvent) => {
e.preventDefault();
if (!skuAdjustRow || !skuAdjustQty) {
toast.error('Quantity is required');
return;
}
const qtyVal = parseInt(skuAdjustQty, 10);
if (!Number.isFinite(qtyVal) || qtyVal === 0) {
toast.error('Enter a non-zero quantity');
return;
}
setIsSubmitting(true);
try {
const qty = skuAdjustType === 'ADJUSTMENT' ? -Math.abs(qtyVal) : Math.abs(qtyVal);
await catalogService.adjustSellableStock({
variant_id: skuAdjustRow.variant_id,
event_type: skuAdjustType,
qty,
notes: skuAdjustNotes.trim() || undefined,
});
toast.success(`Stock quantity updated for ${skuAdjustRow.sku}`);
setShowSkuAdjustModal(false);
setSkuAdjustRow(null);
setSkuAdjustQty('');
setSkuAdjustNotes('');
fetchSkus();
} catch (err: any) {
toast.error(err?.message || 'Failed to adjust sellable stock');
} finally {
setIsSubmitting(false);
}
};
const openLogMovement = async () => {
if (parts.length === 0) {
try {
const fetchedParts = await catalogService.getParts();
setParts(fetchedParts);
} catch {
toast.error('Failed to load parts');
}
}
setShowAdjustModal(true);
};
const skuPages = Math.max(1, Math.ceil(skuTotal / skuLimit));
const filteredParts = useMemo(() => {
const q = searchQuery.trim().toLowerCase();
if (!q) return parts;
return parts.filter(
(p) =>
p.sku.toLowerCase().includes(q) ||
p.name.toLowerCase().includes(q) ||
(p.supplier || '').toLowerCase().includes(q) ||
(p.barcode || '').toLowerCase().includes(q)
);
}, [parts, searchQuery]);
const filteredMovements = useMemo(() => {
const q = searchQuery.trim().toLowerCase();
if (!q) return movements;
return movements.filter((m) => {
const partObj = parts.find((p) => p.part_id === m.entity_id);
return (
(partObj?.name || '').toLowerCase().includes(q) ||
(partObj?.sku || '').toLowerCase().includes(q) ||
(m.movement_type || '').toLowerCase().includes(q) ||
(m.reference_id || '').toLowerCase().includes(q)
);
});
}, [movements, parts, searchQuery]);
const partsPager = useClientPagination(filteredParts);
const movementsPager = useClientPagination(filteredMovements);
const countBadge =
activeTab === 'skus' ? skuTotal : activeTab === 'parts' ? parts.length : movements.length;
const dataCardShell =
'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0';
const inputClass =
'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary';
const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5';
const primaryButton =
'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50';
const secondaryButton =
'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50';
const changeTab = (tab: LedgerTab) => {
setActiveTab(tab);
setSearchQuery('');
};
const renderStockBadge = (isLow: boolean, label?: string) => (
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 crm-radius-badge text-[11px] font-semibold ${
isLow ? 'bg-warning text-white' : 'bg-success text-white'
}`}
>
{isLow && <AlertTriangle className="w-3 h-3" />}
{label || (isLow ? 'Low stock' : 'In stock')}
</span>
);
const renderMovementBadge = (type: string) => {
const value = type.toLowerCase();
const cls =
value === 'purchase' || value === 'receipt'
? 'bg-success text-white'
: value === 'damage'
? 'bg-destructive text-white'
: 'bg-muted text-muted-foreground';
return (
<span className={`inline-flex items-center px-2.5 py-1 crm-radius-badge text-[11px] font-semibold ${cls}`}>
{type}
</span>
);
};
return (
<div className="flex flex-col gap-5 min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-foreground">Stock Ledger</h1>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 crm-radius-toggle bg-primary/10 text-primary text-[11px] font-semibold leading-none">
{countBadge}
</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={fetchData}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
title="Reload"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button type="button" onClick={openLogMovement} className={secondaryButton}>
<TrendingUp className="w-3.5 h-3.5" />
Log movement
</button>
<button type="button" onClick={() => setShowPartModal(true)} className={primaryButton}>
<span className="w-[18px] h-[18px] rounded-full bg-white text-primary flex items-center justify-center">
<Plus className="w-3 h-3" strokeWidth={3} />
</span>
Register part
</button>
</div>
</div>
<div className={dataCardShell}>
<div className="px-5 pt-5">
<div className="-mx-5 px-5 flex flex-wrap items-center justify-between gap-3 min-w-0 pb-4 border-b border-border">
<div className="relative flex-1 min-w-[180px] max-w-[480px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<input
type="text"
placeholder="Search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-9 pl-9 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors"
/>
</div>
<div className="crm-radius-toggle inline-flex items-center gap-1 border border-border bg-card p-1 shrink-0">
{([
{ id: 'skus', label: `SKUs (${skuTotal})` },
{ id: 'parts', label: `Parts (${parts.length})` },
{ id: 'movements', label: `History (${movements.length})` },
] as { id: LedgerTab; label: string }[]).map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => changeTab(tab.id)}
className={`h-8 px-3 crm-radius-toggle text-[13px] font-medium cursor-pointer transition-colors ${
activeTab === tab.id ? 'bg-primary text-white' : 'text-foreground hover:bg-muted'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
</div>
{loading ? (
<div className="flex flex-col items-center justify-center py-16 gap-2.5">
<RefreshCw className="w-6 h-6 text-primary animate-spin" />
<span className="text-[13px] text-muted-foreground">Loading stock ledger...</span>
</div>
) : activeTab === 'skus' ? (
<>
<div className="overflow-x-auto">
<table className="crm-data-table min-w-[900px]">
<thead>
<tr className="bg-gray-50 border-y border-gray-200">
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Product</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">SKU</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Price</th>
<th className="px-5 py-3 text-center text-[13px] font-semibold text-gray-700">Pending</th>
<th className="px-5 py-3 text-center text-[13px] font-semibold text-gray-700">Held</th>
<th className="px-5 py-3 text-center text-[13px] font-semibold text-gray-700">Available</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Status</th>
<th className="px-5 py-3 text-center text-[13px] font-semibold text-gray-700">Action</th>
</tr>
</thead>
<tbody className="bg-card">
{skus.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-12 text-[13px] text-muted-foreground">
{searchQuery ? 'No SKUs match your search.' : 'No sellable SKUs found.'}
</td>
</tr>
) : (
skus.map((row) => (
<tr key={row.variant_id} className="border-t border-border hover:bg-muted/10">
<td className="px-5 py-3.5 text-[13px] font-semibold text-foreground">{row.product_name}</td>
<td className="px-5 py-3.5 font-mono text-muted-foreground">
<span className="crm-cell-clip" title={row.sku}>{row.sku}</span>
</td>
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">{row.price}</td>
<td className="px-5 py-3.5 text-center text-[13px] text-info">
{row.pending_confirmation_units || 0}
</td>
<td className="px-5 py-3.5 text-center text-[13px] text-warning">
{row.confirmed_units || 0}
</td>
<td className="px-5 py-3.5 text-center text-[13px] font-semibold text-foreground">
{row.available_stock}
</td>
<td className="px-5 py-3.5">{renderStockBadge(row.is_low)}</td>
<td className="px-5 py-3.5">
<div className="flex justify-center">
<button
type="button"
onClick={() => {
setSkuAdjustRow(row);
setSkuAdjustType('RECEIPT');
setSkuAdjustQty('');
setSkuAdjustNotes('');
setShowSkuAdjustModal(true);
}}
className={secondaryButton.replace('h-9 px-4', 'h-8 px-3') + ' text-[12px]'}
>
Adjust
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<TablePagination
page={skuPage}
totalPages={skuPages}
total={skuTotal}
pageSize={skuLimit}
onPageChange={setSkuPage}
/>
</>
) : activeTab === 'parts' ? (
<>
<div className="overflow-x-auto">
<table className="crm-data-table min-w-[760px]">
<thead>
<tr className="bg-gray-50 border-y border-gray-200">
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Part</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">SKU / Barcode</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Unit cost</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Stock</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Status</th>
</tr>
</thead>
<tbody className="bg-card">
{filteredParts.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-12 text-[13px] text-muted-foreground">
{searchQuery ? 'No parts match your search.' : 'No inventory parts registered.'}
</td>
</tr>
) : (
partsPager.items.map((p) => {
const isLow = p.stock <= p.low_stock_alert;
return (
<tr key={p.part_id} className="border-t border-border hover:bg-muted/10">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 crm-radius-icon bg-primary/10 text-primary flex items-center justify-center shrink-0">
<Package className="w-4 h-4" />
</div>
<div className="min-w-0">
<p className="text-[13px] font-semibold text-foreground truncate">{p.name}</p>
<p className="text-[11px] text-muted-foreground truncate">
{p.supplier || 'No supplier'}
</p>
</div>
</div>
</td>
<td className="px-5 py-3.5 text-[13px] font-mono text-muted-foreground">
{p.sku}
{p.barcode && <span className="block text-[11px]">{p.barcode}</span>}
</td>
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">{p.cost_price}</td>
<td className="px-5 py-3.5 text-[13px] font-semibold text-foreground">{p.stock}</td>
<td className="px-5 py-3.5">{renderStockBadge(isLow, isLow ? 'Reorder' : 'In stock')}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
<TablePagination {...partsPager} />
</>
) : (
<>
<div className="overflow-x-auto">
<table className="crm-data-table min-w-[760px]">
<thead>
<tr className="bg-gray-50 border-y border-gray-200">
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Date</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Item</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Type</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Quantity</th>
<th className="px-5 py-3 text-left text-[13px] font-semibold text-gray-700">Reference</th>
</tr>
</thead>
<tbody className="bg-card">
{filteredMovements.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-12 text-[13px] text-muted-foreground">
{searchQuery ? 'No movements match your search.' : 'No stock movements recorded.'}
</td>
</tr>
) : (
movementsPager.items.map((m) => {
const partObj = parts.find((p) => p.part_id === m.entity_id);
const isNeg = m.quantity < 0;
return (
<tr key={m.movement_id} className="border-t border-border hover:bg-muted/10">
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">
{new Date(m.created_at).toLocaleString()}
</td>
<td className="px-5 py-3.5">
<p className="text-[13px] font-semibold text-foreground">{partObj?.name || 'Deleted part'}</p>
<p className="text-[11px] font-mono text-muted-foreground">{partObj?.sku || '—'}</p>
</td>
<td className="px-5 py-3.5">{renderMovementBadge(m.movement_type)}</td>
<td className="px-5 py-3.5 text-[13px] font-semibold">
<span className={isNeg ? 'text-destructive' : 'text-success'}>
{isNeg ? '' : '+'}
{m.quantity}
</span>
</td>
<td className="px-5 py-3.5 font-mono text-muted-foreground">
<span className="crm-cell-clip" title={m.reference_id || ''}>{m.reference_id}</span>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
<TablePagination {...movementsPager} />
</>
)}
</div>
<SlideOver
open={showSkuAdjustModal && !!skuAdjustRow}
onClose={() => {
if (isSubmitting) return;
setShowSkuAdjustModal(false);
}}
title={skuAdjustRow ? `Adjust ${skuAdjustRow.sku}` : 'Adjust SKU'}
icon={<TrendingUp className="w-4 h-4 text-primary shrink-0" />}
>
{skuAdjustRow && (
<form onSubmit={handleSkuAdjust} className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-4">
<p className="text-[13px] text-muted-foreground">
Current available: <span className="font-semibold text-foreground">{skuAdjustRow.available_stock}</span>
</p>
<div>
<label className={labelClass}>Type</label>
<CustomSelect
value={skuAdjustType}
onChange={(value) => setSkuAdjustType(value as 'RECEIPT' | 'ADJUSTMENT')}
options={[
{ value: 'RECEIPT', label: 'Receive (add)' },
{ value: 'ADJUSTMENT', label: 'Adjustment (remove)' },
]}
/>
</div>
<div>
<label className={labelClass}>Quantity</label>
<input
type="number"
min="1"
value={skuAdjustQty}
onChange={(e) => setSkuAdjustQty(e.target.value)}
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Notes</label>
<input
type="text"
value={skuAdjustNotes}
onChange={(e) => setSkuAdjustNotes(e.target.value)}
className={inputClass}
/>
</div>
</div>
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center justify-end gap-2 bg-card">
<button
type="button"
onClick={() => setShowSkuAdjustModal(false)}
disabled={isSubmitting}
className={secondaryButton}
>
Cancel
</button>
<button type="submit" disabled={isSubmitting} className={primaryButton}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</div>
</form>
)}
</SlideOver>
<SlideOver
open={showPartModal}
onClose={() => {
if (isSubmitting) return;
setShowPartModal(false);
}}
title="Register part"
icon={<Package className="w-4 h-4 text-primary shrink-0" />}
>
<form onSubmit={handleCreatePart} className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-4">
<div>
<label className={labelClass}>
Part SKU <span className="text-primary">*</span>
</label>
<input
type="text"
placeholder="e.g. PART-IP16PM-SCR-ORG"
value={partSku}
onChange={(e) => setPartSku(e.target.value)}
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>
Part name <span className="text-primary">*</span>
</label>
<input
type="text"
placeholder="e.g. iPhone 16 Pro Max Original Screen"
value={partName}
onChange={(e) => setPartName(e.target.value)}
className={inputClass}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>
Cost price () <span className="text-primary">*</span>
</label>
<input type="number" placeholder="12000" value={partCost} onChange={(e) => setPartCost(e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Low stock alert</label>
<input type="number" value={partLowStock} onChange={(e) => setPartLowStock(e.target.value)} className={inputClass} />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Supplier</label>
<input type="text" placeholder="e.g. Apple Inc" value={partSupplier} onChange={(e) => setPartSupplier(e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Barcode</label>
<input type="text" placeholder="e.g. 789123456" value={partBarcode} onChange={(e) => setPartBarcode(e.target.value)} className={inputClass} />
</div>
</div>
</div>
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center justify-end gap-2 bg-card">
<button type="button" onClick={() => setShowPartModal(false)} disabled={isSubmitting} className={secondaryButton}>
Cancel
</button>
<button type="submit" disabled={isSubmitting} className={primaryButton}>
{isSubmitting ? 'Saving...' : 'Register'}
</button>
</div>
</form>
</SlideOver>
<SlideOver
open={showAdjustModal}
onClose={() => {
if (isSubmitting) return;
setShowAdjustModal(false);
}}
title="Log stock movement"
icon={<TrendingUp className="w-4 h-4 text-primary shrink-0" />}
>
<form onSubmit={handleAdjustStock} className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-4">
<div>
<label className={labelClass}>
Part <span className="text-primary">*</span>
</label>
<CustomSelect
value={adjustPartId}
onChange={setAdjustPartId}
options={[
{ value: '', label: 'Select part' },
...parts.map((p) => ({
value: p.part_id,
label: `${p.name} (${p.sku}) — ${p.stock}`,
})),
]}
placeholder="Select part"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Movement type</label>
<CustomSelect
value={adjustType}
onChange={(value) => setAdjustType(value as 'Adjustment' | 'Damage')}
options={[
{ value: 'Adjustment', label: 'Stock adjustment' },
{ value: 'Damage', label: 'Damage (reduces stock)' },
]}
/>
</div>
<div>
<label className={labelClass}>
Quantity <span className="text-primary">*</span>
</label>
<input type="number" placeholder="10" value={adjustQty} onChange={(e) => setAdjustQty(e.target.value)} className={inputClass} />
</div>
</div>
<div>
<label className={labelClass}>
Reason / reference <span className="text-primary">*</span>
</label>
<input
type="text"
placeholder="e.g. Audit adjustment, damaged in transit"
value={adjustReason}
onChange={(e) => setAdjustReason(e.target.value)}
className={inputClass}
/>
</div>
</div>
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center justify-end gap-2 bg-card">
<button type="button" onClick={() => setShowAdjustModal(false)} disabled={isSubmitting} className={secondaryButton}>
Cancel
</button>
<button type="submit" disabled={isSubmitting} className={primaryButton}>
{isSubmitting ? 'Saving...' : 'Apply'}
</button>
</div>
</form>
</SlideOver>
</div>
);
}