- Integrated toast notifications to display error messages when form submissions fail across various components, enhancing user feedback and experience. - Updated components including DeleteUser, FormDialog, ManageTwoFactor, TwoFactorRecoveryCodes, TwoFactorSetupModal, PayrollPeriodShow, EmployeeCreate, EmployeeEdit, CuttingCreate, CuttingEdit, PurchaseCreate, PurchaseEdit, RestockCreate, RestockEdit, TransactionCreate, TransactionEdit, Category management, Product management, Raw Material management, Role management, Settings, and Authentication pages.
570 lines
29 KiB
TypeScript
570 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/confirm-dialog';
|
|
import { FileUpload } from '@/components/file-upload';
|
|
import InputError from '@/components/input-error';
|
|
import { NumberInput } from '@/components/number-input';
|
|
import { RupiahInput } from '@/components/rupiah-input';
|
|
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 { clearRawMaterialDraft } from '@/lib/raw-material-draft';
|
|
import { getTemporaryUrl } from '@/lib/upload';
|
|
import {
|
|
index as rawMaterialIndex,
|
|
update,
|
|
} from '@/routes/admin/master/raw-materials';
|
|
import type { RawMaterialForEdit, RawMaterialVariantForEdit } from './columns';
|
|
|
|
type Props = {
|
|
rawMaterial: RawMaterialForEdit;
|
|
};
|
|
|
|
type VariantState = {
|
|
id: number | null;
|
|
variant: string;
|
|
price: number;
|
|
stock: number;
|
|
photo: string | null;
|
|
photoUrl: string | null;
|
|
uploading: boolean;
|
|
};
|
|
|
|
export default function RawMaterialEdit({ rawMaterial }: Props) {
|
|
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
|
const userId = auth.user?.id;
|
|
|
|
clearRawMaterialDraft('edit', userId, rawMaterial.id);
|
|
|
|
const [name, setName] = useState(rawMaterial.name);
|
|
const [unit, setUnit] = useState(rawMaterial.unit);
|
|
const [isActive, setIsActive] = useState(rawMaterial.is_active);
|
|
|
|
const serverVariants: VariantState[] = rawMaterial.raw_material_prices.map(
|
|
(v: RawMaterialVariantForEdit) => ({
|
|
id: v.id,
|
|
variant: v.variant,
|
|
price: v.price,
|
|
stock: v.stock,
|
|
photo: v.photo_key,
|
|
photoUrl: v.photo_url,
|
|
uploading: false,
|
|
}),
|
|
);
|
|
|
|
const [variants, setVariants] = useState<VariantState[]>(() => {
|
|
return serverVariants.length > 0
|
|
? serverVariants
|
|
: [
|
|
{
|
|
id: null,
|
|
variant: '',
|
|
price: 0,
|
|
stock: 0,
|
|
photo: null,
|
|
photoUrl: null,
|
|
uploading: false,
|
|
},
|
|
];
|
|
});
|
|
|
|
const variantsRef = useRef(variants);
|
|
variantsRef.current = variants;
|
|
|
|
const draftData = {
|
|
name,
|
|
unit,
|
|
isActive,
|
|
variants: variants.map((v) => ({
|
|
id: v.id,
|
|
variant: v.variant,
|
|
price: v.price,
|
|
stock: v.stock,
|
|
photo: v.photo ?? undefined,
|
|
})),
|
|
};
|
|
|
|
useRawMaterialDraftSave('edit', draftData, userId, rawMaterial.id);
|
|
|
|
const addVariant = useCallback(() => {
|
|
setVariants((prev) => [
|
|
...prev,
|
|
{
|
|
id: null,
|
|
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: isActive,
|
|
variants: variantsRef.current.map((v) => ({
|
|
id: v.id,
|
|
variant: v.variant,
|
|
price: Number(v.price),
|
|
stock: Number(v.stock),
|
|
photo_key: v.photo,
|
|
})),
|
|
};
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head title="Edit 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">
|
|
Edit Bahan Baku
|
|
</h2>
|
|
<Button asChild variant="outline">
|
|
<Link href={rawMaterialIndex.url()}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Kembali
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<Form
|
|
action={update(rawMaterial.id)}
|
|
method="put"
|
|
transform={(data) => ({
|
|
...data,
|
|
...getPayload(),
|
|
})}
|
|
onError={() => {
|
|
toast.error('Terjadi kesalahan saat menyimpan data. 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>
|
|
<NumberInput
|
|
value={
|
|
Number(
|
|
variant.stock,
|
|
) || 0
|
|
}
|
|
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>
|
|
</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>
|
|
</>
|
|
);
|
|
}
|