"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useLocale } from "@/components/providers/locale-provider"; import { SearchableSelectField } from "@/components/shared/searchable-select-field"; import { formatCurrencyAmount, formatDecimal, formatKilogram } from "@/lib/formatters"; import type { ApiErrorResponse, DetailResponse } from "@/types/api"; import type { GradeRecord, WarehouseLocationRecord, WarehouseRecord } from "@/types/master-data"; import type { LotListItem } from "@/types/lot"; import type { LotTransformationDetail, LotTransformationListItem } from "@/types/lot-transformation"; type TransformationInputForm = { source_lot_id: string; qty_used: string; notes: string; }; type TransformationOutputForm = { grade_id: string; warehouse_id: string; warehouse_location_id: string; qty_produced: string; notes: string; }; type TransformationForm = { transformation_type: "MIX" | "REGRADE"; transformation_date: string; remainder_mode: "KEEP_SOURCE_GRADE" | "SHRINKAGE" | ""; processing_loss_mode: "SHRINKAGE" | ""; notes: string; inputs: TransformationInputForm[]; outputs: TransformationOutputForm[]; }; const createEmptyInput = (): TransformationInputForm => ({ source_lot_id: "", qty_used: "", notes: "" }); const createEmptyOutput = (): TransformationOutputForm => ({ grade_id: "", warehouse_id: "", warehouse_location_id: "", qty_produced: "", notes: "" }); const createEmptyForm = (): TransformationForm => ({ transformation_type: "MIX", transformation_date: new Date().toISOString().slice(0, 10), remainder_mode: "", processing_loss_mode: "", notes: "", inputs: [createEmptyInput(), createEmptyInput()], outputs: [createEmptyOutput()] }); export function LotMixingClient({ currencyCode }: { currencyCode: string }) { const { locale } = useLocale(); const [lots, setLots] = useState([]); const [transformations, setTransformations] = useState([]); const [grades, setGrades] = useState([]); const [warehouses, setWarehouses] = useState([]); const [locations, setLocations] = useState([]); const [selectedTransformation, setSelectedTransformation] = useState(null); const [form, setForm] = useState(createEmptyForm); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); async function loadAll() { setLoading(true); setError(null); try { const [lotsRes, transformationsRes, gradesRes, warehousesRes, locationsRes] = await Promise.all([ fetch("/api/v1/lots", { cache: "no-store" }), fetch("/api/v1/lot-transformations", { cache: "no-store" }), fetch("/api/v1/grades", { cache: "no-store" }), fetch("/api/v1/warehouses", { cache: "no-store" }), fetch("/api/v1/warehouse-locations", { cache: "no-store" }) ]); const lotsPayload = (await lotsRes.json()) as { data: LotListItem[] }; const transformationsPayload = (await transformationsRes.json()) as { data: LotTransformationListItem[]; }; const gradesPayload = (await gradesRes.json()) as { data: GradeRecord[] }; const warehousesPayload = (await warehousesRes.json()) as { data: WarehouseRecord[] }; const locationsPayload = (await locationsRes.json()) as { data: WarehouseLocationRecord[] }; if ( !lotsRes.ok || !transformationsRes.ok || !gradesRes.ok || !warehousesRes.ok || !locationsRes.ok ) { throw new Error(locale === "id" ? "Gagal memuat data mixing lot" : "Failed to load lot mixing data"); } setLots(lotsPayload.data); setTransformations(transformationsPayload.data); setGrades(gradesPayload.data); setWarehouses(warehousesPayload.data); setLocations(locationsPayload.data); } catch (err) { setError(err instanceof Error ? err.message : locale === "id" ? "Gagal memuat data mixing lot" : "Failed to load lot mixing data"); } finally { setLoading(false); } } useEffect(() => { void loadAll(); }, []); useEffect(() => { setForm((current) => { if (current.transformation_type === "REGRADE") { return { ...current, inputs: current.inputs.length > 0 ? [current.inputs[0]] : [createEmptyInput()] }; } if (current.inputs.length >= 2) { return current; } return { ...current, inputs: [...current.inputs, createEmptyInput()] }; }); }, [form.transformation_type]); async function openTransformation(id: string) { setError(null); try { const response = await fetch(`/api/v1/lot-transformations/${id}`, { cache: "no-store" }); const payload = (await response.json()) as DetailResponse | ApiErrorResponse; if (!response.ok || !("data" in payload)) { throw new Error( "message" in payload ? payload.message : locale === "id" ? "Gagal memuat detail transformasi" : "Failed to load transformation details" ); } setSelectedTransformation(payload.data); } catch (err) { setError( err instanceof Error ? err.message : locale === "id" ? "Gagal memuat detail transformasi" : "Failed to load transformation details" ); } } function resetForm() { setForm(createEmptyForm()); setError(null); } function updateInput(index: number, patch: Partial) { setForm((current) => ({ ...current, inputs: current.inputs.map((line, lineIndex) => lineIndex === index ? { ...line, ...patch } : line ) })); } function updateOutput(index: number, patch: Partial) { setForm((current) => ({ ...current, outputs: current.outputs.map((line, lineIndex) => lineIndex === index ? { ...line, ...patch } : line ) })); } function addInput() { if (form.transformation_type === "REGRADE") return; setForm((current) => ({ ...current, inputs: [...current.inputs, createEmptyInput()] })); } function removeInput(index: number) { setForm((current) => ({ ...current, inputs: current.inputs.filter((_, lineIndex) => lineIndex !== index) })); } function addOutput() { setForm((current) => ({ ...current, outputs: [...current.outputs, createEmptyOutput()] })); } function removeOutput(index: number) { setForm((current) => ({ ...current, outputs: current.outputs.filter((_, lineIndex) => lineIndex !== index) })); } async function createTransformation() { setSubmitting(true); setError(null); try { const response = await fetch("/api/v1/lot-transformations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transformation_type: form.transformation_type, transformation_date: form.transformation_date, remainder_mode: form.remainder_mode || null, processing_loss_mode: form.processing_loss_mode || null, notes: form.notes, inputs: form.inputs.map((line) => ({ source_lot_id: line.source_lot_id, qty_used: Number(line.qty_used), notes: line.notes || null })), outputs: form.outputs.map((line) => ({ grade_id: line.grade_id, warehouse_id: line.warehouse_id, warehouse_location_id: line.warehouse_location_id || null, qty_produced: Number(line.qty_produced), notes: line.notes || null })) }) }); const payload = (await response.json()) as DetailResponse | ApiErrorResponse; if (!response.ok) { if ("errors" in payload && payload.errors) { const firstError = Object.values(payload.errors)[0]?.[0]; throw new Error( firstError ?? payload.message ?? (locale === "id" ? "Validasi gagal" : "Validation failed") ); } throw new Error( "message" in payload ? payload.message : locale === "id" ? "Gagal membuat transformasi" : "Failed to create transformation" ); } if (!("data" in payload)) { throw new Error( locale === "id" ? "Respons transformasi tidak valid" : "Invalid transformation response" ); } resetForm(); setSelectedTransformation(payload.data); await loadAll(); } catch (err) { setError( err instanceof Error ? err.message : locale === "id" ? "Gagal membuat transformasi" : "Failed to create transformation" ); } finally { setSubmitting(false); } } const selectableLots = useMemo( () => lots.filter( (lot) => lot.status !== "DEPLETED" && lot.available_qty > 0 ), [lots] ); const inputTotal = useMemo( () => form.inputs.reduce((sum, line) => sum + (Number(line.qty_used) || 0), 0), [form.inputs] ); const outputTotal = useMemo( () => form.outputs.reduce((sum, line) => sum + (Number(line.qty_produced) || 0), 0), [form.outputs] ); const selectedRegradeSourceLot = form.transformation_type === "REGRADE" ? selectableLots.find((lot) => lot.id === form.inputs[0]?.source_lot_id) ?? null : null; const unusedSourceRemainderQty = form.transformation_type === "REGRADE" && selectedRegradeSourceLot ? Number( ( selectedRegradeSourceLot.available_qty - (Number(form.inputs[0]?.qty_used) || 0) ).toFixed(3) ) : 0; const outputLossQty = Number((inputTotal - outputTotal).toFixed(3)); useEffect(() => { if (form.transformation_type !== "REGRADE") return; if (unusedSourceRemainderQty > 0) return; setForm((current) => current.remainder_mode ? { ...current, remainder_mode: "" } : current ); }, [form.transformation_type, unusedSourceRemainderQty]); useEffect(() => { if (form.transformation_type !== "REGRADE") return; if (outputLossQty > 0) return; setForm((current) => current.processing_loss_mode ? { ...current, processing_loss_mode: "" } : current ); }, [form.transformation_type, outputLossQty]); return (

Transformasi QC

Mixing Lot / Ubah Grade

Pilih beberapa lot aktif di gudang, tentukan qty yang dipakai, lalu hasilkan satu atau lebih lot baru dengan grade baru.

setForm((current) => ({ ...current, transformation_type: value as "MIX" | "REGRADE", remainder_mode: value === "REGRADE" ? current.remainder_mode : "" })) } options={[ { value: "MIX", label: "MIX" }, { value: "REGRADE", label: "REGRADE" } ]} /> setForm((current) => ({ ...current, transformation_date: value })) } />