'use no memo';
import { NumberInput } from '@/components/inputs';
import { InputError } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '@/components/ui/combobox';
import { Label } from '@/components/ui/label';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Textarea } from '@/components/ui/textarea';
import { useStokOpnameDraftSave } from '@/hooks/use-stok-opname-draft';
import { formatNumber } from '@/lib/format';
import { loadStokOpnameDraft } from '@/lib/stok-opname-draft';
import { index as stokOpnameIndex, store } from '@/routes/admin/manage/stok-opnames';
import { Form, Head, Link, usePage } from '@inertiajs/react';
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'sonner';
import type { ProductForStokOpname, StokOpnameCreateData } from './columns';
type StockType = 'good' | 'reject' | 'retail';
type CartLine = {
key: string;
photoUrl: string | null;
title: string;
subtitle: string;
quantity: number;
onAdjust: (delta: number) => void;
onSet: (value: number) => void;
onRemove: () => void;
};
type Props = {
products: StokOpnameCreateData['products'];
};
export default function StokOpnameCreate({ products }: Props) {
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
const userId = auth.user?.id;
const draft = loadStokOpnameDraft('create', userId);
const [selectedProductId, setSelectedProductId] = useState(
draft?.selectedProductId ?? '',
);
const [physicalStocks, setPhysicalStocks] = useState>>(() => {
if (draft?.physicalStocks) {
return draft.physicalStocks;
}
return {};
});
const [notes, setNotes] = useState(draft?.notes ?? '');
const [cartOpen, setCartOpen] = useState(false);
const draftData = useMemo(
() => ({
selectedProductId,
physicalStocks,
notes,
}),
[selectedProductId, physicalStocks, notes],
);
useStokOpnameDraftSave('create', draftData, userId);
const physicalStocksRef = useRef(physicalStocks);
useEffect(() => {
physicalStocksRef.current = physicalStocks;
}, [physicalStocks]);
const selectedProduct = useMemo(
() => products.find((p) => String(p.id) === selectedProductId) ?? null,
[products, selectedProductId],
);
const variantById = useMemo(
() =>
new Map(
products.flatMap((p: ProductForStokOpname) =>
p.product_variants.map((v) => [v.id, v]),
),
),
[products],
);
const updatePhysicalStock = useCallback((variantId: number, stockType: StockType, value: number) => {
setPhysicalStocks((prev) => ({
...prev,
[variantId]: {
...prev[variantId],
[stockType]: Math.max(0, value),
},
}));
}, []);
const hasAnyStock = useCallback((variantId: number) => {
const stocks = physicalStocks[variantId];
if (!stocks) return false;
return stocks.good > 0 || stocks.reject > 0 || stocks.retail > 0;
}, [physicalStocks]);
const cartItems: CartLine[] = (() => {
const lines: CartLine[] = [];
for (const [variantId, stocks] of Object.entries(physicalStocks)) {
const id = Number(variantId);
const variant = variantById.get(id);
if (!variant) continue;
for (const [stockType, quantity] of Object.entries(stocks)) {
if (quantity <= 0) continue;
const type = stockType as StockType;
const typeLabel = type === 'good' ? 'Bagus' : type === 'reject' ? 'Reject' : 'Ecer';
lines.push({
key: `variant-${id}-${type}`,
photoUrl: variant.photo_url,
title: variant.name,
subtitle: `${typeLabel}: ${formatNumber(quantity)}`,
quantity,
onAdjust: (delta) => updatePhysicalStock(id, type, quantity + delta),
onSet: (value) => updatePhysicalStock(id, type, value),
onRemove: () => updatePhysicalStock(id, type, 0),
});
}
}
return lines.sort((a, b) => a.title.localeCompare(b.title));
})();
function formatQuantity(value: number): string {
return formatNumber(value, { maximumFractionDigits: 4 });
}
function getPayload() {
const items: Array<{ product_variant_id: number; stock_quality: StockType; physical_stock: number }> = [];
for (const [variantId, stocks] of Object.entries(physicalStocksRef.current)) {
const variant = variantById.get(Number(variantId));
if (!variant) continue;
for (const [stockType, quantity] of Object.entries(stocks)) {
if (quantity <= 0) continue;
items.push({
product_variant_id: Number(variantId),
stock_quality: stockType as StockType,
physical_stock: quantity,
});
}
}
return {
opname_date: new Date().toISOString().split('T')[0],
items,
notes: notes || null,
};
}
return (
<>
Tambah Stok Opname
Keranjang Stok Opname
{cartItems.length === 0 ? (
Keranjang kosong.
) : (
(() => {
const groupedByVariant: Record
= {};
for (const item of cartItems) {
const variantKey = item.key.split('-').slice(0, 2).join('-');
if (!groupedByVariant[variantKey]) {
groupedByVariant[variantKey] = {
title: item.title,
photoUrl: item.photoUrl,
items: [],
};
}
groupedByVariant[variantKey].items.push(item);
}
return Object.entries(groupedByVariant).map(([variantKey, group]) => (
{group.photoUrl ? (
) : (
N/A
)}
{group.title}
{group.items.map((item) => {
const stockType = item.key.split('-').pop();
const typeLabel = stockType === 'good' ? 'Bagus' : stockType === 'reject' ? 'Reject' : 'Ecer';
return (
{typeLabel}
{formatQuantity(item.quantity)} pcs
);
})}
));
})()
)}
Total Item
{cartItems.length}
>
);
}