feat: update validation rules in CuttingRequest and enhance error handling in CuttingCreate and CuttingEdit components
This commit is contained in:
parent
b45052dc55
commit
aaafbb92a2
@ -16,16 +16,16 @@ public function rules(): array
|
|||||||
return [
|
return [
|
||||||
'description' => ['nullable', 'string', 'max:100'],
|
'description' => ['nullable', 'string', 'max:100'],
|
||||||
'product_name' => ['required', 'string', 'max:255'],
|
'product_name' => ['required', 'string', 'max:255'],
|
||||||
'sample' => ['required', 'integer', 'min:0'],
|
'sample' => ['required', 'integer'],
|
||||||
'original_outside_sample' => ['required', 'integer', 'min:0'],
|
'original_outside_sample' => ['required', 'integer'],
|
||||||
'cutting_result' => ['required', 'integer', 'min:0'],
|
'cutting_result' => ['required', 'integer', 'min:1'],
|
||||||
'materials' => ['required', 'array', 'min:1'],
|
'materials' => ['required', 'array', 'min:1'],
|
||||||
'materials.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
'materials.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
||||||
'materials.*.material_usage' => ['required', 'integer', 'min:0'],
|
'materials.*.material_usage' => ['required', 'integer', 'min:1'],
|
||||||
'materials.*.material_result' => ['required', 'integer', 'min:0'],
|
'materials.*.material_result' => ['required', 'integer', 'min:1'],
|
||||||
'materials.*.combination_index' => ['nullable', 'integer', 'min:0'],
|
'materials.*.combination_index' => ['nullable', 'integer', 'min:1'],
|
||||||
'combinations' => ['nullable', 'array'],
|
'combinations' => ['nullable', 'array'],
|
||||||
'combinations.*.material_result' => ['nullable', 'integer', 'min:0'],
|
'combinations.*.material_result' => ['nullable', 'integer', 'min:1'],
|
||||||
'photo_key' => ['nullable', 'string', 'max:500'],
|
'photo_key' => ['nullable', 'string', 'max:500'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
|
||||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
|
||||||
import { useCallback, useMemo, useRef, 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';
|
||||||
@ -22,6 +19,9 @@ import { formatNumber } from '@/lib/format';
|
|||||||
import { getTemporaryUrl } from '@/lib/upload';
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings';
|
import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings';
|
||||||
|
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import type { CuttingCreateData } from './columns';
|
import type { CuttingCreateData } from './columns';
|
||||||
|
|
||||||
type MaterialState = {
|
type MaterialState = {
|
||||||
@ -45,7 +45,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function CuttingCreate({ data }: Props) {
|
export default function CuttingCreate({ data }: Props) {
|
||||||
const { rawMaterials } = data;
|
const { rawMaterials } = data;
|
||||||
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
const { auth, errors } = usePage().props as { auth: { user?: { id?: number } }; errors: Record<string, string> };
|
||||||
const userId = auth.user?.id;
|
const userId = auth.user?.id;
|
||||||
|
|
||||||
const draft = loadCuttingDraft('create', userId);
|
const draft = loadCuttingDraft('create', userId);
|
||||||
@ -152,19 +152,19 @@ export default function CuttingCreate({ data }: Props) {
|
|||||||
const addVariant = useCallback(
|
const addVariant = useCallback(
|
||||||
(priceId: number) => {
|
(priceId: number) => {
|
||||||
if (!selectedMaterial) {
|
if (!selectedMaterial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||||
|
|
||||||
if (!price) {
|
if (!price) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMaterials((prev) => {
|
setMaterials((prev) => {
|
||||||
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...prev,
|
...prev,
|
||||||
@ -199,8 +199,8 @@ return prev;
|
|||||||
|
|
||||||
const confirmCombo = useCallback(() => {
|
const confirmCombo = useCallback(() => {
|
||||||
if (comboSelectedPriceIds.length < 2) {
|
if (comboSelectedPriceIds.length < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const comboIndex = combinations.length;
|
const comboIndex = combinations.length;
|
||||||
|
|
||||||
@ -284,9 +284,9 @@ return;
|
|||||||
return {
|
return {
|
||||||
description: notes || null,
|
description: notes || null,
|
||||||
product_name: productName || null,
|
product_name: productName || null,
|
||||||
sample: sample || null,
|
sample: sample,
|
||||||
original_outside_sample: originalOutsideSample || null,
|
original_outside_sample: originalOutsideSample,
|
||||||
cutting_result: cuttingResult || null,
|
cutting_result: cuttingResult,
|
||||||
materials: materialsRef.current.map((m) => ({
|
materials: materialsRef.current.map((m) => ({
|
||||||
raw_material_price_id: m.raw_material_price_id,
|
raw_material_price_id: m.raw_material_price_id,
|
||||||
material_usage: m.material_usage,
|
material_usage: m.material_usage,
|
||||||
@ -316,8 +316,8 @@ return;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
||||||
submittingRef.current = true;
|
submittingRef.current = true;
|
||||||
}}>
|
}}>
|
||||||
{({ 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">
|
||||||
@ -390,8 +390,6 @@ return;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -457,12 +455,12 @@ return;
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Foto</Label>
|
<Label>Foto</Label>
|
||||||
<FileUpload value={photo} onChange={(key) => {
|
<FileUpload value={photo} onChange={(key) => {
|
||||||
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
||||||
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
||||||
<InputError message={errors.photo_key} />
|
<InputError message={errors.photo_key} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={processing || submittingRef.current || uploading || materials.length === 0 || !productName || !sample}>
|
<Button type="submit" className="w-full" disabled={processing}>
|
||||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -498,8 +496,8 @@ return;
|
|||||||
materials.forEach((m, i) => {
|
materials.forEach((m, i) => {
|
||||||
if (m.combination_id !== null) {
|
if (m.combination_id !== null) {
|
||||||
if (!comboMap.has(m.combination_id)) {
|
if (!comboMap.has(m.combination_id)) {
|
||||||
comboMap.set(m.combination_id, []);
|
comboMap.set(m.combination_id, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
comboMap.get(m.combination_id)!.push({ m, index: i });
|
comboMap.get(m.combination_id)!.push({ m, index: i });
|
||||||
} else {
|
} else {
|
||||||
@ -513,24 +511,27 @@ comboMap.set(m.combination_id, []);
|
|||||||
return groups.map((group, gi) => (
|
return groups.map((group, gi) => (
|
||||||
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
||||||
{group.comboIndex !== null && (
|
{group.comboIndex !== null && (
|
||||||
<div className="flex items-center justify-between border-b pb-2">
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground">·</span>
|
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
||||||
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
|
<span className="text-xs text-muted-foreground">·</span>
|
||||||
<NumberInput
|
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
|
||||||
className="w-20"
|
<NumberInput
|
||||||
value={combinations[group.comboIndex]?.material_result ?? 0}
|
className="w-20"
|
||||||
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
|
value={combinations[group.comboIndex]?.material_result ?? 0}
|
||||||
/>
|
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
|
||||||
|
setComboDeleteIndex(group.comboIndex);
|
||||||
|
setComboDeleteConfirmOpen(true);
|
||||||
|
}}>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
|
<InputError message={errors[`combinations.${group.comboIndex}.material_result` as keyof typeof errors]} />
|
||||||
setComboDeleteIndex(group.comboIndex);
|
</>
|
||||||
setComboDeleteConfirmOpen(true);
|
|
||||||
}}>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{group.items.map(({ m, index }) => {
|
{group.items.map(({ m, index }) => {
|
||||||
const price = priceMap.get(m.raw_material_price_id);
|
const price = priceMap.get(m.raw_material_price_id);
|
||||||
@ -569,6 +570,7 @@ comboMap.set(m.combination_id, []);
|
|||||||
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<InputError message={errors[`materials.${index}.material_usage` as keyof typeof errors]} />
|
||||||
{group.comboIndex === null && (
|
{group.comboIndex === null && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
||||||
@ -579,6 +581,7 @@ comboMap.set(m.combination_id, []);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{group.comboIndex === null && <InputError message={errors[`materials.${index}.material_result` as keyof typeof errors]} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -599,10 +602,10 @@ comboMap.set(m.combination_id, []);
|
|||||||
<ImagePreviewModal
|
<ImagePreviewModal
|
||||||
open={previewKey !== null}
|
open={previewKey !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setPreviewKey(null);
|
setPreviewKey(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
||||||
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
||||||
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
||||||
@ -725,40 +728,40 @@ setPreviewKey(null);
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
||||||
}
|
}
|
||||||
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
||||||
if (deleteMaterialIndex !== null) {
|
if (deleteMaterialIndex !== null) {
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
||||||
}
|
}
|
||||||
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
||||||
if (cartDeleteIndex !== null) {
|
if (cartDeleteIndex !== null) {
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
||||||
}
|
}
|
||||||
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
||||||
if (comboDeleteIndex !== null) {
|
if (comboDeleteIndex !== null) {
|
||||||
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
import { Form, Head, Link } from '@inertiajs/react';
|
|
||||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
|
||||||
import { useCallback, useMemo, useRef, 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';
|
||||||
@ -20,6 +17,9 @@ import { formatNumber } from '@/lib/format';
|
|||||||
import { getTemporaryUrl } from '@/lib/upload';
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { index as cuttingIndex, update } from '@/routes/admin/manage/cuttings';
|
import { index as cuttingIndex, update } from '@/routes/admin/manage/cuttings';
|
||||||
|
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import type { CuttingCreateData, CuttingForEdit } from './columns';
|
import type { CuttingCreateData, CuttingForEdit } from './columns';
|
||||||
|
|
||||||
type MaterialState = {
|
type MaterialState = {
|
||||||
@ -46,6 +46,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function CuttingEdit({ cutting, data }: Props) {
|
export default function CuttingEdit({ cutting, data }: Props) {
|
||||||
const { rawMaterials } = data;
|
const { rawMaterials } = data;
|
||||||
|
const { errors } = usePage().props as { errors: Record<string, string> };
|
||||||
|
|
||||||
const [materials, setMaterials] = useState<MaterialState[]>(() => {
|
const [materials, setMaterials] = useState<MaterialState[]>(() => {
|
||||||
const comboIdToIndex = new Map<number, number>();
|
const comboIdToIndex = new Map<number, number>();
|
||||||
@ -132,19 +133,19 @@ export default function CuttingEdit({ cutting, data }: Props) {
|
|||||||
const addVariant = useCallback(
|
const addVariant = useCallback(
|
||||||
(priceId: number) => {
|
(priceId: number) => {
|
||||||
if (!selectedMaterial) {
|
if (!selectedMaterial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||||
|
|
||||||
if (!price) {
|
if (!price) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMaterials((prev) => {
|
setMaterials((prev) => {
|
||||||
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...prev,
|
...prev,
|
||||||
@ -179,8 +180,8 @@ return prev;
|
|||||||
|
|
||||||
const confirmCombo = useCallback(() => {
|
const confirmCombo = useCallback(() => {
|
||||||
if (comboSelectedPriceIds.length < 2) {
|
if (comboSelectedPriceIds.length < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const comboIndex = combinations.length;
|
const comboIndex = combinations.length;
|
||||||
|
|
||||||
@ -259,9 +260,9 @@ return;
|
|||||||
return {
|
return {
|
||||||
description: notes || null,
|
description: notes || null,
|
||||||
product_name: productName || null,
|
product_name: productName || null,
|
||||||
sample: sample || null,
|
sample: sample,
|
||||||
original_outside_sample: originalOutsideSample || null,
|
original_outside_sample: originalOutsideSample,
|
||||||
cutting_result: cuttingResult || null,
|
cutting_result: cuttingResult,
|
||||||
materials: materialsRef.current.map((m) => ({
|
materials: materialsRef.current.map((m) => ({
|
||||||
raw_material_price_id: m.raw_material_price_id,
|
raw_material_price_id: m.raw_material_price_id,
|
||||||
material_usage: m.material_usage,
|
material_usage: m.material_usage,
|
||||||
@ -291,8 +292,8 @@ return;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
||||||
submittingRef.current = true;
|
submittingRef.current = true;
|
||||||
}}>
|
}}>
|
||||||
{({ 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">
|
||||||
@ -430,12 +431,12 @@ return;
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Foto</Label>
|
<Label>Foto</Label>
|
||||||
<FileUpload value={photo} onChange={(key) => {
|
<FileUpload value={photo} onChange={(key) => {
|
||||||
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
||||||
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
||||||
<InputError message={errors.photo_key} />
|
<InputError message={errors.photo_key} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={processing || submittingRef.current || uploading || materials.length === 0 || !productName || !sample}>
|
<Button type="submit" className="w-full" disabled={processing}>
|
||||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -471,8 +472,8 @@ return;
|
|||||||
materials.forEach((m, i) => {
|
materials.forEach((m, i) => {
|
||||||
if (m.combination_id !== null) {
|
if (m.combination_id !== null) {
|
||||||
if (!comboMap.has(m.combination_id)) {
|
if (!comboMap.has(m.combination_id)) {
|
||||||
comboMap.set(m.combination_id, []);
|
comboMap.set(m.combination_id, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
comboMap.get(m.combination_id)!.push({ m, index: i });
|
comboMap.get(m.combination_id)!.push({ m, index: i });
|
||||||
} else {
|
} else {
|
||||||
@ -486,24 +487,27 @@ comboMap.set(m.combination_id, []);
|
|||||||
return groups.map((group, gi) => (
|
return groups.map((group, gi) => (
|
||||||
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
||||||
{group.comboIndex !== null && (
|
{group.comboIndex !== null && (
|
||||||
<div className="flex items-center justify-between border-b pb-2">
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground">·</span>
|
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
||||||
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
|
<span className="text-xs text-muted-foreground">·</span>
|
||||||
<NumberInput
|
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
|
||||||
className="w-20"
|
<NumberInput
|
||||||
value={combinations[group.comboIndex]?.material_result ?? 0}
|
className="w-20"
|
||||||
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
|
value={combinations[group.comboIndex]?.material_result ?? 0}
|
||||||
/>
|
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
|
||||||
|
setComboDeleteIndex(group.comboIndex);
|
||||||
|
setComboDeleteConfirmOpen(true);
|
||||||
|
}}>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
|
<InputError message={errors[`combinations.${group.comboIndex}.material_result` as keyof typeof errors]} />
|
||||||
setComboDeleteIndex(group.comboIndex);
|
</>
|
||||||
setComboDeleteConfirmOpen(true);
|
|
||||||
}}>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{group.items.map(({ m, index }) => {
|
{group.items.map(({ m, index }) => {
|
||||||
const price = priceMap.get(m.raw_material_price_id);
|
const price = priceMap.get(m.raw_material_price_id);
|
||||||
@ -535,23 +539,25 @@ comboMap.set(m.combination_id, []);
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Pemakaian <span className="text-destructive">*</span></span>
|
<span className="text-xs text-muted-foreground whitespace-nowrap">Pemakaian <span className="text-destructive">*</span></span>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
value={m.material_usage}
|
value={m.material_usage}
|
||||||
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{group.comboIndex === null && (
|
<InputError message={errors[`materials.${index}.material_usage` as keyof typeof errors]} />
|
||||||
<div className="flex items-center gap-2">
|
{group.comboIndex === null && (
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
<div className="flex items-center gap-2">
|
||||||
<NumberInput
|
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
||||||
className="flex-1"
|
<NumberInput
|
||||||
value={m.material_result}
|
className="flex-1"
|
||||||
onValueChange={(val) => updateMaterial(index, 'material_result', val)}
|
value={m.material_result}
|
||||||
/>
|
onValueChange={(val) => updateMaterial(index, 'material_result', val)}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{group.comboIndex === null && <InputError message={errors[`materials.${index}.material_result` as keyof typeof errors]} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -572,10 +578,10 @@ comboMap.set(m.combination_id, []);
|
|||||||
<ImagePreviewModal
|
<ImagePreviewModal
|
||||||
open={previewKey !== null}
|
open={previewKey !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setPreviewKey(null);
|
setPreviewKey(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
||||||
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
||||||
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
||||||
@ -698,40 +704,40 @@ setPreviewKey(null);
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
||||||
}
|
}
|
||||||
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
||||||
if (deleteMaterialIndex !== null) {
|
if (deleteMaterialIndex !== null) {
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
||||||
}
|
}
|
||||||
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
||||||
if (cartDeleteIndex !== null) {
|
if (cartDeleteIndex !== null) {
|
||||||
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
||||||
}
|
}
|
||||||
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
||||||
if (comboDeleteIndex !== null) {
|
if (comboDeleteIndex !== null) {
|
||||||
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user