892 lines
50 KiB
TypeScript
892 lines
50 KiB
TypeScript
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
|
import {
|
|
ArrowLeft,
|
|
Copy,
|
|
ClipboardPaste,
|
|
Check,
|
|
Plus,
|
|
Trash2,
|
|
} from 'lucide-react';
|
|
import { useCallback, useRef, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { ConfirmDialog } from '@/components/dialogs';
|
|
import { FileUploadMultiple } from '@/components/inputs';
|
|
import { InputError } from '@/components/ui';
|
|
import { NumberInput } from '@/components/inputs';
|
|
import { RupiahInput } from '@/components/inputs';
|
|
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 { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useProductDraftSave } from '@/hooks/use-product-draft';
|
|
import { loadProductDraft } from '@/lib/product-draft';
|
|
import { getTemporaryUrl } from '@/lib/upload';
|
|
import { index as productIndex, store } from '@/routes/admin/master/products';
|
|
|
|
type Category = {
|
|
id: number;
|
|
name: string;
|
|
};
|
|
|
|
type Props = {
|
|
categories: Category[];
|
|
};
|
|
|
|
const PRICE_TYPES = [
|
|
{ key: 'distributor', label: 'Distributor' },
|
|
{ key: 'agent', label: 'Agen' },
|
|
{ key: 'sub_agent', label: 'Sub Agen' },
|
|
{ key: 'wholesale', label: 'Grosir' },
|
|
{ key: 'retail', label: 'Ecer' },
|
|
{ key: 'tiktok', label: 'TikTok' },
|
|
{ key: 'shopee', label: 'Shopee' },
|
|
{ key: 'capital', label: 'Modal' },
|
|
{ key: 'reject_capital', label: 'Reject Modal' },
|
|
{ key: 'reject_selling', label: 'Reject Jual' },
|
|
];
|
|
|
|
function createEmptyPrices(): Array<{ type: string; price: number }> {
|
|
return PRICE_TYPES.map((pt) => ({ type: pt.key, price: 0 }));
|
|
}
|
|
|
|
type VariantState = {
|
|
name: string;
|
|
stock: number;
|
|
reject_stock: number;
|
|
retail_stock: number;
|
|
photos: Array<{ key: string; url: string | null }>;
|
|
uploading: boolean;
|
|
prices: Array<{ type: string; price: number }>;
|
|
};
|
|
|
|
export default function ProductCreate({ categories }: Props) {
|
|
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
|
const userId = auth.user?.id;
|
|
|
|
const draft = loadProductDraft('create', userId);
|
|
|
|
const [productName, setProductName] = useState(draft?.productName ?? '');
|
|
const [status, setStatus] = useState(draft?.status ?? 'active');
|
|
const [description, setDescription] = useState(draft?.description ?? '');
|
|
const [categoryIds, setCategoryIds] = useState<number[]>(
|
|
draft?.categoryIds ?? [],
|
|
);
|
|
const [useSamePrice, setUseSamePrice] = useState(
|
|
draft?.useSamePrice ?? true,
|
|
);
|
|
const [sharedPrices, setSharedPrices] = useState<
|
|
Array<{ type: string; price: number }>
|
|
>(draft?.sharedPrices ?? createEmptyPrices());
|
|
const [variants, setVariants] = useState<VariantState[]>(() => {
|
|
if (draft?.variants && draft.variants.length > 0) {
|
|
return draft.variants.map((v) => {
|
|
const photos = Array.isArray(v.photos)
|
|
? v.photos.map((p) => ({ key: p.key, url: getTemporaryUrl(p.key) }))
|
|
: typeof v.photo === 'string' && v.photo
|
|
? [{ key: v.photo, url: getTemporaryUrl(v.photo) }]
|
|
: [];
|
|
|
|
return {
|
|
...v,
|
|
photos,
|
|
uploading: false,
|
|
};
|
|
});
|
|
}
|
|
|
|
return [
|
|
{
|
|
name: '',
|
|
stock: 0,
|
|
reject_stock: 0,
|
|
retail_stock: 0,
|
|
photos: [],
|
|
uploading: false,
|
|
prices: createEmptyPrices(),
|
|
},
|
|
];
|
|
});
|
|
|
|
const variantsRef = useRef(variants);
|
|
variantsRef.current = variants;
|
|
|
|
const draftData = {
|
|
productName,
|
|
status,
|
|
description,
|
|
categoryIds,
|
|
useSamePrice,
|
|
sharedPrices,
|
|
variants: variants.map((v) => ({
|
|
name: v.name,
|
|
stock: v.stock,
|
|
reject_stock: v.reject_stock,
|
|
retail_stock: v.retail_stock,
|
|
photos: v.photos.map((p) => ({ key: p.key })),
|
|
prices: v.prices,
|
|
})),
|
|
};
|
|
|
|
useProductDraftSave('create', draftData, userId);
|
|
|
|
const addVariant = useCallback(() => {
|
|
setVariants((prev) => [
|
|
...prev,
|
|
{
|
|
name: '',
|
|
stock: 0,
|
|
reject_stock: 0,
|
|
retail_stock: 0,
|
|
photos: [],
|
|
uploading: false,
|
|
prices: createEmptyPrices(),
|
|
},
|
|
]);
|
|
}, []);
|
|
|
|
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 updateVariantPrice = useCallback(
|
|
(variantIndex: number, priceIndex: number, value: number) => {
|
|
setVariants((prev) => {
|
|
const updated = [...prev];
|
|
updated[variantIndex] = {
|
|
...updated[variantIndex],
|
|
prices: updated[variantIndex].prices.map((p, i) =>
|
|
i === priceIndex ? { ...p, price: value } : p,
|
|
),
|
|
};
|
|
|
|
return updated;
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const updateSharedPrice = useCallback(
|
|
(priceIndex: number, value: number) => {
|
|
setSharedPrices((prev) => {
|
|
const updated = [...prev];
|
|
updated[priceIndex] = { ...updated[priceIndex], price: 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 copyPrices = useCallback((variantIndex: number) => {
|
|
setVariants((prev) => {
|
|
const prices = prev[variantIndex].prices;
|
|
navigator.clipboard.writeText(JSON.stringify(prices));
|
|
setCopiedIndex(variantIndex);
|
|
setTimeout(() => setCopiedIndex(null), 1500);
|
|
|
|
return prev;
|
|
});
|
|
}, []);
|
|
|
|
const pastePrices = useCallback((variantIndex: number) => {
|
|
navigator.clipboard.readText().then((text) => {
|
|
try {
|
|
const prices = JSON.parse(text) as Array<{
|
|
type: string;
|
|
price: number;
|
|
}>;
|
|
setVariants((prev) => {
|
|
const updated = [...prev];
|
|
updated[variantIndex] = {
|
|
...updated[variantIndex],
|
|
prices,
|
|
};
|
|
|
|
return updated;
|
|
});
|
|
} catch {
|
|
// invalid clipboard data
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
const applyToAll = useCallback((variantIndex: number) => {
|
|
setVariants((prev) => {
|
|
const sourcePrices = prev[variantIndex].prices;
|
|
|
|
return prev.map((v, i) =>
|
|
i === variantIndex ? v : { ...v, prices: [...sourcePrices] },
|
|
);
|
|
});
|
|
}, []);
|
|
|
|
function getPayload() {
|
|
return {
|
|
name: productName,
|
|
status,
|
|
description,
|
|
category_ids: categoryIds,
|
|
use_same_price: useSamePrice,
|
|
shared_prices: useSamePrice
|
|
? sharedPrices.map((p) => ({
|
|
type: p.type,
|
|
price: Number(p.price),
|
|
}))
|
|
: [],
|
|
variants: variantsRef.current.map((v) => ({
|
|
name: v.name,
|
|
stock: Number(v.stock),
|
|
reject_stock: Number(v.reject_stock),
|
|
retail_stock: Number(v.retail_stock),
|
|
photo_keys: v.photos.map((p) => p.key),
|
|
prices: useSamePrice
|
|
? []
|
|
: v.prices.map((p) => ({
|
|
type: p.type,
|
|
price: Number(p.price),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head title="Tambah Produk" />
|
|
|
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-2xl font-semibold tracking-tight">
|
|
Tambah Produk
|
|
</h2>
|
|
<Button asChild variant="outline">
|
|
<Link href={productIndex.url()}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Kembali
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<Form
|
|
action={store()}
|
|
transform={(data) => ({
|
|
...data,
|
|
...getPayload(),
|
|
})}
|
|
onError={() => {
|
|
toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.');
|
|
}}
|
|
>
|
|
{({ errors, processing }) => (
|
|
<>
|
|
<div className="grid gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Informasi Produk</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="name">
|
|
Nama Produk{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<Input
|
|
id="name"
|
|
name="name"
|
|
value={productName}
|
|
onChange={(e) =>
|
|
setProductName(
|
|
e.target.value,
|
|
)
|
|
}
|
|
placeholder="Masukkan nama produk"
|
|
/>
|
|
<InputError message={errors.name} />
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Status{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<RadioGroup
|
|
name="status"
|
|
value={status}
|
|
onValueChange={setStatus}
|
|
className="flex gap-4"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
value="active"
|
|
id="status-active"
|
|
/>
|
|
<Label
|
|
htmlFor="status-active"
|
|
className="font-normal"
|
|
>
|
|
Aktif
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
value="inactive"
|
|
id="status-inactive"
|
|
/>
|
|
<Label
|
|
htmlFor="status-inactive"
|
|
className="font-normal"
|
|
>
|
|
Non Aktif
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
value="draft"
|
|
id="status-draft"
|
|
/>
|
|
<Label
|
|
htmlFor="status-draft"
|
|
className="font-normal"
|
|
>
|
|
Draft
|
|
</Label>
|
|
</div>
|
|
</RadioGroup>
|
|
<InputError
|
|
message={errors.status}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2 md:col-span-2">
|
|
<Label>
|
|
Kategori{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{categories.map((category) => (
|
|
<label
|
|
key={category.id}
|
|
className="flex items-center space-x-2 rounded-md border px-3 py-2 text-sm"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
name="category_ids[]"
|
|
value={category.id}
|
|
checked={categoryIds.includes(
|
|
category.id,
|
|
)}
|
|
onChange={(e) => {
|
|
setCategoryIds(
|
|
(prev) =>
|
|
e.target
|
|
.checked
|
|
? [
|
|
...prev,
|
|
category.id,
|
|
]
|
|
: prev.filter(
|
|
(
|
|
id,
|
|
) =>
|
|
id !==
|
|
category.id,
|
|
),
|
|
);
|
|
}}
|
|
className="rounded"
|
|
/>
|
|
<span>
|
|
{category.name}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<InputError
|
|
message={errors.category_ids}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2 md:col-span-2">
|
|
<Label htmlFor="description">
|
|
Deskripsi
|
|
</Label>
|
|
<Textarea
|
|
id="description"
|
|
name="description"
|
|
value={description}
|
|
onChange={(e) =>
|
|
setDescription(
|
|
e.target.value,
|
|
)
|
|
}
|
|
placeholder="Masukkan deskripsi produk"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Harga</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<RadioGroup
|
|
name="use_same_price"
|
|
value={useSamePrice ? '1' : '0'}
|
|
onValueChange={(val) =>
|
|
setUseSamePrice(val === '1')
|
|
}
|
|
className="flex gap-6"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
value="1"
|
|
id="price-same"
|
|
/>
|
|
<Label
|
|
htmlFor="price-same"
|
|
className="font-normal"
|
|
>
|
|
Semua varian sama
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
value="0"
|
|
id="price-different"
|
|
/>
|
|
<Label
|
|
htmlFor="price-different"
|
|
className="font-normal"
|
|
>
|
|
Harga per varian
|
|
</Label>
|
|
</div>
|
|
</RadioGroup>
|
|
|
|
{useSamePrice && (
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
|
{PRICE_TYPES.map(
|
|
(priceType, priceIndex) => (
|
|
<div
|
|
key={priceType.key}
|
|
className="grid gap-2"
|
|
>
|
|
<Label>
|
|
Harga{' '}
|
|
{
|
|
priceType.label
|
|
}{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<RupiahInput
|
|
value={
|
|
sharedPrices[
|
|
priceIndex
|
|
]?.price ??
|
|
0
|
|
}
|
|
onValueChange={(
|
|
val,
|
|
) =>
|
|
updateSharedPrice(
|
|
priceIndex,
|
|
val,
|
|
)
|
|
}
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`shared_prices.${priceIndex}.price`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
),
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Varian Produk</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">
|
|
{!useSamePrice && (
|
|
<>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="whitespace-nowrap"
|
|
onClick={() =>
|
|
copyPrices(
|
|
variantIndex,
|
|
)
|
|
}
|
|
>
|
|
{copiedIndex ===
|
|
variantIndex ? (
|
|
<Check className="h-4 w-4 text-green-600" />
|
|
) : (
|
|
<Copy className="h-4 w-4" />
|
|
)}
|
|
Salin
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="whitespace-nowrap"
|
|
onClick={() =>
|
|
pastePrices(
|
|
variantIndex,
|
|
)
|
|
}
|
|
>
|
|
<ClipboardPaste className="h-4 w-4" />
|
|
Tempel
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="whitespace-nowrap"
|
|
onClick={() =>
|
|
applyToAll(
|
|
variantIndex,
|
|
)
|
|
}
|
|
>
|
|
Terapkan
|
|
ke Semua
|
|
</Button>
|
|
</>
|
|
)}
|
|
{variants.length >
|
|
1 && (
|
|
<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-4">
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Nama Varian{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<Input
|
|
value={
|
|
variant.name
|
|
}
|
|
onChange={(e) =>
|
|
updateVariant(
|
|
variantIndex,
|
|
'name',
|
|
e.target
|
|
.value,
|
|
)
|
|
}
|
|
placeholder="Contoh: Ukuran L, Warna Merah"
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`variants.${variantIndex}.name`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Stok Bagus{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<NumberInput
|
|
value={
|
|
variant.stock
|
|
}
|
|
onValueChange={(
|
|
val,
|
|
) =>
|
|
updateVariant(
|
|
variantIndex,
|
|
'stock',
|
|
val,
|
|
)
|
|
}
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`variants.${variantIndex}.stock`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Stok Reject{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<NumberInput
|
|
value={
|
|
variant.reject_stock
|
|
}
|
|
onValueChange={(
|
|
val,
|
|
) =>
|
|
updateVariant(
|
|
variantIndex,
|
|
'reject_stock',
|
|
val,
|
|
)
|
|
}
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`variants.${variantIndex}.reject_stock`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Stok Ecer{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<NumberInput
|
|
value={
|
|
variant.retail_stock
|
|
}
|
|
onValueChange={(
|
|
val,
|
|
) =>
|
|
updateVariant(
|
|
variantIndex,
|
|
'retail_stock',
|
|
val,
|
|
)
|
|
}
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`variants.${variantIndex}.retail_stock`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Foto Varian{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<FileUploadMultiple
|
|
value={
|
|
variant.photos
|
|
}
|
|
onChange={(
|
|
photos,
|
|
) =>
|
|
updateVariant(
|
|
variantIndex,
|
|
'photos',
|
|
photos,
|
|
)
|
|
}
|
|
folder="product-variant"
|
|
maxItems={5}
|
|
onUploadingChange={(
|
|
uploading,
|
|
) =>
|
|
updateVariant(
|
|
variantIndex,
|
|
'uploading',
|
|
uploading,
|
|
)
|
|
}
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`variants.${variantIndex}.photo_keys`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
{!useSamePrice && (
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
{PRICE_TYPES.map(
|
|
(
|
|
priceType,
|
|
priceIndex,
|
|
) => (
|
|
<div
|
|
key={
|
|
priceType.key
|
|
}
|
|
className="grid gap-2"
|
|
>
|
|
<Label>
|
|
Harga{' '}
|
|
{
|
|
priceType.label
|
|
}{' '}
|
|
<span className="text-destructive">
|
|
*
|
|
</span>
|
|
</Label>
|
|
<RupiahInput
|
|
value={
|
|
variant
|
|
.prices[
|
|
priceIndex
|
|
]
|
|
?.price ??
|
|
0
|
|
}
|
|
onValueChange={(
|
|
val,
|
|
) =>
|
|
updateVariantPrice(
|
|
variantIndex,
|
|
priceIndex,
|
|
val,
|
|
)
|
|
}
|
|
/>
|
|
<InputError
|
|
message={
|
|
errors[
|
|
`variants.${variantIndex}.prices.${priceIndex}.price`
|
|
]
|
|
}
|
|
/>
|
|
</div>
|
|
),
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
),
|
|
)}
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={addVariant}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Tambah Varian
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="mt-6 flex items-center gap-4">
|
|
<Button
|
|
type="submit"
|
|
disabled={
|
|
processing ||
|
|
variants.some((v) => v.uploading)
|
|
}
|
|
>
|
|
{processing
|
|
? 'Menyimpan...'
|
|
: variants.some((v) => v.uploading)
|
|
? 'Mengunggah...'
|
|
: 'Simpan'}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Form>
|
|
</div>
|
|
|
|
<ConfirmDialog
|
|
open={deleteConfirmOpen}
|
|
onOpenChange={setDeleteConfirmOpen}
|
|
title="Hapus Varian"
|
|
description="Apakah Anda yakin ingin menghapus varian ini?"
|
|
confirmLabel="Hapus"
|
|
onConfirm={() => {
|
|
if (deleteVariantIndex !== null) {
|
|
removeVariant(deleteVariantIndex);
|
|
}
|
|
|
|
setDeleteConfirmOpen(false);
|
|
setDeleteVariantIndex(null);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|