dstpabuaran.com/resources/js/pages/admin/master/raw-material/create.tsx

553 lines
29 KiB
TypeScript

import { Form, Head, Link, usePage } from '@inertiajs/react';
import {
ArrowLeft,
Check,
ClipboardPaste,
Copy,
Plus,
Trash2,
} from 'lucide-react';
import { useCallback, useRef, useState } from 'react';
import { toast } from 'sonner';
import { ConfirmDialog } from '@/components/dialogs';
import { FileUpload } from '@/components/inputs';
import { InputError } from '@/components/ui';
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 { useRawMaterialDraftSave } from '@/hooks/use-raw-material-draft';
import { UNITS } from '@/lib/constants';
import { loadRawMaterialDraft } from '@/lib/raw-material-draft';
import { getTemporaryUrl } from '@/lib/upload';
import {
index as rawMaterialIndex,
store,
} from '@/routes/admin/master/raw-materials';
type VariantState = {
variant: string;
price: number;
stock: string;
photo: string | null;
photoUrl: string | null;
uploading: boolean;
};
export default function RawMaterialCreate() {
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
const userId = auth.user?.id;
const draft = loadRawMaterialDraft('create', userId);
const [name, setName] = useState(draft?.name ?? '');
const [unit, setUnit] = useState(draft?.unit ?? 'kg');
const [variants, setVariants] = useState<VariantState[]>(() => {
if (draft?.variants && draft.variants.length > 0) {
return draft.variants.map((v) => ({
variant: v.variant,
price: v.price,
stock: String(v.stock),
photo: v.photo ?? null,
photoUrl: v.photo ? getTemporaryUrl(v.photo) : null,
uploading: false,
}));
}
return [
{
variant: '',
price: 0,
stock: '0',
photo: null,
photoUrl: null,
uploading: false,
},
];
});
const variantsRef = useRef(variants);
variantsRef.current = variants;
const draftData = {
name,
unit,
isActive: true,
variants: variants.map((v) => ({
variant: v.variant,
price: v.price,
stock: v.stock,
photo: v.photo ?? undefined,
})),
};
useRawMaterialDraftSave('create', draftData, userId);
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 },
);
});
}, []);
function getPayload() {
return {
name,
unit,
is_active: true,
variants: variantsRef.current.map((v) => ({
variant: v.variant,
price: Number(v.price),
stock: parseFloat(String(v.stock)) || 0,
photo_key: v.photo,
})),
};
}
return (
<>
<Head title="Tambah Bahan Baku" />
<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 Bahan Baku
</h2>
<Button asChild variant="outline">
<Link href={rawMaterialIndex.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 Bahan Baku
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="name">
Nama Bahan Baku{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="name"
name="name"
value={name}
onChange={(e) =>
setName(e.target.value)
}
placeholder="Masukkan nama bahan baku"
/>
<InputError message={errors.name} />
</div>
<div className="grid gap-2">
<Label>
Satuan{' '}
<span className="text-destructive">
*
</span>
</Label>
<RadioGroup
name="unit"
value={unit}
onValueChange={setUnit}
className="flex flex-wrap gap-4"
>
{UNITS.map((u) => (
<div
key={u.value}
className="flex items-center space-x-2"
>
<RadioGroupItem
value={u.value}
id={`unit-${u.value}`}
/>
<Label
htmlFor={`unit-${u.value}`}
className="font-normal"
>
{u.label}
</Label>
</div>
))}
</RadioGroup>
<InputError message={errors.unit} />
</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>
{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-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>
<Input
type="text"
inputMode="decimal"
value={
variant.stock
}
onChange={(
e,
) =>
updateVariant(
variantIndex,
'stock',
e.target.value,
)
}
/>
<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>
</div>
<div className="mt-6 flex items-center gap-4">
<Button
type="submit"
disabled={
processing ||
variants.some((v) => v.uploading)
}
>
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</>
)}
</Form>
<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);
}}
/>
</div>
</>
);
}