refactor: remove unused variant handling and clean up PurchaseEdit component

This commit is contained in:
Yoga Pangestu 2026-08-06 09:16:27 +07:00
parent fb70cca1dc
commit f49cebb35a

View File

@ -3,15 +3,12 @@
import { Form, Head, Link } from '@inertiajs/react'; import { Form, Head, Link } from '@inertiajs/react';
import { import {
ArrowLeft, ArrowLeft,
Check,
ClipboardPaste,
Copy,
Minus, Minus,
Plus, Plus,
ShoppingCart, ShoppingCart,
Trash2, Trash2,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ConfirmDialog } from '@/components/confirm-dialog'; import { ConfirmDialog } from '@/components/confirm-dialog';
import { FileUpload } from '@/components/file-upload'; import { FileUpload } from '@/components/file-upload';
import { ImagePreviewModal } from '@/components/image-preview-modal'; import { ImagePreviewModal } from '@/components/image-preview-modal';
@ -28,7 +25,6 @@ import {
ComboboxItem, ComboboxItem,
ComboboxList, ComboboxList,
} from '@/components/ui/combobox'; } from '@/components/ui/combobox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { import {
Sheet, Sheet,
@ -37,10 +33,8 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from '@/components/ui/sheet'; } from '@/components/ui/sheet';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { formatNumber } from '@/lib/format'; import { formatNumber } from '@/lib/format';
import { getTemporaryUrl } from '@/lib/upload';
import { formatCurrency } from '@/lib/utils'; import { formatCurrency } from '@/lib/utils';
import { import {
index as purchaseIndex, index as purchaseIndex,
@ -48,16 +42,6 @@ import {
} from '@/routes/admin/manage/purchases'; } from '@/routes/admin/manage/purchases';
import type { PurchaseCreateData, PurchaseForEdit } from './columns'; import type { PurchaseCreateData, PurchaseForEdit } from './columns';
type VariantState = {
id?: number;
variant: string;
price: number;
stock: number;
photo: string | null;
photoUrl: string | null;
uploading: boolean;
};
type CartLine = { type CartLine = {
key: string; key: string;
photoUrl: string | null; photoUrl: string | null;
@ -78,19 +62,6 @@ type Props = {
export default function PurchaseEdit({ purchase, data }: Props) { export default function PurchaseEdit({ purchase, data }: Props) {
const { suppliers, rawMaterials } = data; const { suppliers, rawMaterials } = data;
const [name] = useState(purchase.name);
const [variants, setVariants] = useState<VariantState[]>(() =>
purchase.variants.map((v) => ({
id: v.id,
variant: v.variant,
price: v.price,
stock: v.stock,
photo: v.photo_key ?? null,
photoUrl: v.photo_url ?? null,
uploading: false,
})),
);
const [supplierId, setSupplierId] = useState(String(purchase.supplier_id)); const [supplierId, setSupplierId] = useState(String(purchase.supplier_id));
const selectedSupplier = const selectedSupplier =
suppliers.find((s) => String(s.id) === supplierId) ?? null; suppliers.find((s) => String(s.id) === supplierId) ?? null;
@ -101,7 +72,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
const [photoUrl, setPhotoUrl] = useState<string | null>(purchase.photo_url); const [photoUrl, setPhotoUrl] = useState<string | null>(purchase.photo_url);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [mode, setMode] = useState<'new' | 'existing'>(purchase.default_mode);
const [selectedMaterialName, setSelectedMaterialName] = useState( const [selectedMaterialName, setSelectedMaterialName] = useState(
purchase.existing_material_name ?? '', purchase.existing_material_name ?? '',
); );
@ -117,9 +87,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
const [previewKey, setPreviewKey] = useState<string | null>(null); const [previewKey, setPreviewKey] = useState<string | null>(null);
const [cartRemoveKey, setCartRemoveKey] = useState<string | null>(null); const [cartRemoveKey, setCartRemoveKey] = useState<string | null>(null);
const variantsRef = useRef(variants);
variantsRef.current = variants;
const priceMap = useMemo( const priceMap = useMemo(
() => () =>
new Map( new Map(
@ -143,11 +110,7 @@ export default function PurchaseEdit({ purchase, data }: Props) {
[rawMaterials], [rawMaterials],
); );
const newSubtotal = variants.reduce( const subtotal = Object.entries(quantities).reduce(
(sum, v) => sum + Number(v.price) * Number(v.stock),
0,
);
const existingSubtotal = Object.entries(quantities).reduce(
(sum, [priceId, quantity]) => { (sum, [priceId, quantity]) => {
const price = priceMap.get(Number(priceId)); const price = priceMap.get(Number(priceId));
@ -155,93 +118,8 @@ export default function PurchaseEdit({ purchase, data }: Props) {
}, },
0, 0,
); );
const subtotal = mode === 'existing' ? existingSubtotal : newSubtotal;
const total = subtotal - discount + shippingCost; const total = subtotal - discount + shippingCost;
const addVariant = useCallback(() => {
setVariants((prev) => [
...prev,
{
variant: '',
price: 0,
stock: 0,
photo: null,
photoUrl: null,
uploading: false,
},
]);
}, []);
const removeVariant = useCallback((index: number) => {
setVariants((prev) => prev.filter((_, i) => i !== index));
}, []);
const updateVariant = useCallback(
(index: number, field: keyof VariantState, value: unknown) => {
setVariants((prev) => {
const updated = [...prev];
(updated[index] as Record<string, unknown>)[field] = value;
return updated;
});
},
[],
);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleteVariantIndex, setDeleteVariantIndex] = useState<number | null>(
null,
);
const confirmRemoveVariant = useCallback((index: number) => {
setDeleteVariantIndex(index);
setDeleteConfirmOpen(true);
}, []);
const copyPrice = useCallback((variantIndex: number) => {
setVariants((prev) => {
const price = prev[variantIndex].price;
navigator.clipboard.writeText(String(price));
setCopiedIndex(variantIndex);
setTimeout(() => setCopiedIndex(null), 1500);
return prev;
});
}, []);
const pastePrice = useCallback((variantIndex: number) => {
navigator.clipboard.readText().then((text) => {
try {
const price = Number(text);
if (!isNaN(price)) {
setVariants((prev) => {
const updated = [...prev];
updated[variantIndex] = {
...updated[variantIndex],
price,
};
return updated;
});
}
} catch {
// invalid clipboard data
}
});
}, []);
const applyToAll = useCallback((variantIndex: number) => {
setVariants((prev) => {
const sourcePrice = prev[variantIndex].price;
return prev.map((v, i) =>
i === variantIndex ? v : { ...v, price: sourcePrice },
);
});
}, []);
const selectedMaterial = useMemo( const selectedMaterial = useMemo(
() => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null, () => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null,
[rawMaterials, selectedMaterialName], [rawMaterials, selectedMaterialName],
@ -261,20 +139,7 @@ export default function PurchaseEdit({ purchase, data }: Props) {
})); }));
}, []); }, []);
const adjustVariantStock = useCallback((index: number, amount: number) => {
setVariants((prev) => {
const updated = [...prev];
updated[index] = {
...updated[index],
stock: Math.max(0, Number(updated[index].stock) + amount),
};
return updated;
});
}, []);
const cartItems: CartLine[] = (() => { const cartItems: CartLine[] = (() => {
if (mode === 'existing') {
const lines: CartLine[] = []; const lines: CartLine[] = [];
for (const [priceId, quantity] of Object.entries(quantities)) { for (const [priceId, quantity] of Object.entries(quantities)) {
@ -302,21 +167,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
} }
return lines.sort((a, b) => a.title.localeCompare(b.title)); return lines.sort((a, b) => a.title.localeCompare(b.title));
}
return variants
.map((v, index): CartLine => ({
key: `new-${index}`,
photoUrl: v.photoUrl,
title: `${name || 'Bahan Baku Baru'}${v.variant || `Varian ${index + 1}`}`,
subtitle: `${formatCurrency(v.price)} / ${purchase.unit}`,
price: v.price,
quantity: v.stock,
onAdjust: (delta) => adjustVariantStock(index, delta),
onSet: (value) => updateVariant(index, 'stock', value),
onRemove: () => removeVariant(index),
}))
.filter((line) => line.quantity > 0);
})(); })();
function formatQuantity(value: number): string { function formatQuantity(value: number): string {
@ -324,18 +174,13 @@ export default function PurchaseEdit({ purchase, data }: Props) {
} }
function getPayload() { function getPayload() {
const base = { return {
mode, mode: 'existing',
supplier_id: supplierId ? Number(supplierId) : null, supplier_id: supplierId ? Number(supplierId) : null,
discount, discount,
shipping_cost: shippingCost, shipping_cost: shippingCost,
notes: notes || null, notes: notes || null,
photo_key: photo, photo_key: photo,
};
if (mode === 'existing') {
return {
...base,
existing_items: Object.entries(quantities) existing_items: Object.entries(quantities)
.map(([priceId, quantity]) => ({ .map(([priceId, quantity]) => ({
raw_material_price_id: Number(priceId), raw_material_price_id: Number(priceId),
@ -346,19 +191,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
}; };
} }
return {
...base,
name,
variants: variantsRef.current.map((v) => ({
id: v.id,
variant: v.variant,
price: Number(v.price),
stock: Number(v.stock),
photo_key: v.photo,
})),
};
}
return ( return (
<> <>
<Head title="Edit Belanja" /> <Head title="Edit Belanja" />
@ -387,303 +219,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
{({ errors, processing }) => ( {({ errors, processing }) => (
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
<div className="space-y-6 md:col-span-2"> <div className="space-y-6 md:col-span-2">
<Tabs
value={mode}
onValueChange={(value) =>
setMode(value as 'new' | 'existing')
}
>
<TabsList>
<TabsTrigger value="new">
Baru
</TabsTrigger>
<TabsTrigger value="existing">
Lama
</TabsTrigger>
</TabsList>
<TabsContent
value="new"
className="mt-0 space-y-6"
>
<Card>
<CardHeader>
<CardTitle>
Informasi Bahan Baku
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">
Nama Bahan Baku{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="edit-name"
name="name"
value={name}
disabled
placeholder="Masukkan nama bahan baku"
/>
<InputError
message={errors.name}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
Varian Bahan Baku
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{variants.map(
(variant, variantIndex) => (
<div
key={variantIndex}
className="space-y-4 rounded-lg border p-4"
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h4 className="font-medium">
Varian{' '}
{variantIndex +
1}
</h4>
<div className="flex flex-wrap items-center gap-1">
<Button
type="button"
variant="outline"
size="sm"
className="whitespace-nowrap"
onClick={() =>
copyPrice(
variantIndex,
)
}
>
{copiedIndex ===
variantIndex ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
Salin
Harga
</Button>
<Button
type="button"
variant="outline"
size="sm"
className="whitespace-nowrap"
onClick={() =>
pastePrice(
variantIndex,
)
}
>
<ClipboardPaste className="h-4 w-4" />
Tempel
Harga
</Button>
<Button
type="button"
variant="outline"
size="sm"
className="whitespace-nowrap"
onClick={() =>
applyToAll(
variantIndex,
)
}
>
Terapkan
ke Semua
</Button>
{variantIndex >
0 && (
<Button
type="button"
variant="outline"
size="icon"
onClick={() =>
confirmRemoveVariant(
variantIndex,
)
}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="grid gap-2">
<Label>
Nama
Varian{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
value={
variant.variant
}
onChange={(
e,
) =>
updateVariant(
variantIndex,
'variant',
e
.target
.value,
)
}
placeholder="Contoh: Ukuran L, Warna Merah"
/>
<InputError
message={
errors[
`variants.${variantIndex}.variant`
]
}
/>
</div>
<div className="grid gap-2">
<Label>
Harga{' '}
<span className="text-destructive">
*
</span>
</Label>
<RupiahInput
value={
variant.price
}
onValueChange={(
val,
) =>
updateVariant(
variantIndex,
'price',
val,
)
}
/>
<InputError
message={
errors[
`variants.${variantIndex}.price`
]
}
/>
</div>
<div className="grid gap-2">
<Label>
Stok{' '}
<span className="text-destructive">
*
</span>
</Label>
<NumberInput
value={
variant.stock
}
onValueChange={(
val,
) =>
updateVariant(
variantIndex,
'stock',
val,
)
}
/>
<InputError
message={
errors[
`variants.${variantIndex}.stock`
]
}
/>
</div>
</div>
<div className="grid gap-2">
<Label>
Foto Varian{' '}
<span className="text-destructive">
*
</span>
</Label>
<FileUpload
value={
variant.photo
}
onChange={(
key,
) => {
updateVariant(
variantIndex,
'photo',
key,
);
updateVariant(
variantIndex,
'photoUrl',
key
? getTemporaryUrl(
key,
)
: null,
);
}}
folder="raw-material-variant"
existingUrl={
variant.photoUrl
}
onUploadingChange={(
uploading,
) =>
updateVariant(
variantIndex,
'uploading',
uploading,
)
}
/>
<InputError
message={
errors[
`variants.${variantIndex}.photo_key`
]
}
/>
</div>
</div>
),
)}
<Button
type="button"
variant="outline"
onClick={addVariant}
>
<Plus className="h-4 w-4" />
Tambah Varian
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent
value="existing"
className="mt-0 space-y-6"
>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
@ -716,6 +251,7 @@ export default function PurchaseEdit({ purchase, data }: Props) {
<ComboboxInput <ComboboxInput
placeholder="Cari bahan baku..." placeholder="Cari bahan baku..."
className="w-full" className="w-full"
disabled
/> />
<ComboboxContent> <ComboboxContent>
<ComboboxEmpty> <ComboboxEmpty>
@ -868,8 +404,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
)} )}
</CardContent> </CardContent>
</Card> </Card>
</TabsContent>
</Tabs>
</div> </div>
<div className="space-y-6 md:col-span-1"> <div className="space-y-6 md:col-span-1">
@ -1020,15 +554,10 @@ export default function PurchaseEdit({ purchase, data }: Props) {
disabled={ disabled={
processing || processing ||
uploading || uploading ||
variants.some(
(v) => v.uploading,
) ||
!supplierId || !supplierId ||
(mode === 'existing' Object.values(
? Object.values(
quantities, quantities,
).every((q) => q <= 0) ).every((q) => q <= 0)
: !name)
} }
> >
{processing {processing
@ -1184,27 +713,6 @@ export default function PurchaseEdit({ purchase, data }: Props) {
title={cartItems.find((i) => i.key === previewKey)?.title} title={cartItems.find((i) => i.key === previewKey)?.title}
/> />
<ConfirmDialog
open={deleteConfirmOpen}
onOpenChange={(open) => {
if (!open) {
setDeleteConfirmOpen(false);
setDeleteVariantIndex(null);
}
}}
title="Hapus Varian"
description="Apakah Anda yakin ingin menghapus varian ini?"
confirmLabel="Hapus"
onConfirm={() => {
if (deleteVariantIndex !== null) {
removeVariant(deleteVariantIndex);
}
setDeleteConfirmOpen(false);
setDeleteVariantIndex(null);
}}
/>
<ConfirmDialog <ConfirmDialog
open={cartRemoveKey !== null} open={cartRemoveKey !== null}
onOpenChange={(open) => { onOpenChange={(open) => {