From f0ceebbbc8193440d7128fd88010cf3934bd7c9c Mon Sep 17 00:00:00 2001 From: Wira Irawan Date: Thu, 21 May 2026 10:37:11 +0700 Subject: [PATCH] Standardize adjustment reason categories and descriptions --- .../migration.sql | 8 + prisma/schema.prisma | 10 +- .../api/v1/adjustment-reasons/[id]/route.ts | 3 + src/app/api/v1/adjustment-reasons/route.ts | 1 + .../stock-adjustments/bootstrap/route.ts | 1 + src/app/api/v1/stock-adjustments/route.ts | 4 +- .../master-data/adjustment-reasons-client.tsx | 140 +++++++++++++----- .../lib/serialize-adjustment-reason.ts | 4 +- .../schemas/adjustment-reason.schema.ts | 10 +- .../components/stock-adjustments-client.tsx | 30 +++- .../lib/serialize-stock-adjustment.ts | 4 +- src/types/master-data.ts | 4 +- src/types/stock-adjustment.ts | 5 +- 13 files changed, 177 insertions(+), 47 deletions(-) create mode 100644 prisma/migrations/20260521_adjustment_reason_category_enum_description/migration.sql diff --git a/prisma/migrations/20260521_adjustment_reason_category_enum_description/migration.sql b/prisma/migrations/20260521_adjustment_reason_category_enum_description/migration.sql new file mode 100644 index 0000000..3080c05 --- /dev/null +++ b/prisma/migrations/20260521_adjustment_reason_category_enum_description/migration.sql @@ -0,0 +1,8 @@ +CREATE TYPE "AdjustmentReasonCategory" AS ENUM ('SHRINKAGE', 'DAMAGE', 'REGRADE', 'ADJUSTMENT'); + +ALTER TABLE "adjustment_reasons" +ADD COLUMN "description" VARCHAR(255); + +ALTER TABLE "adjustment_reasons" +ALTER COLUMN "category" TYPE "AdjustmentReasonCategory" +USING ("category"::"AdjustmentReasonCategory"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f4ee44a..a240c47 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,6 +8,13 @@ datasource db { url = env("DATABASE_URL") } +enum AdjustmentReasonCategory { + SHRINKAGE + DAMAGE + REGRADE + ADJUSTMENT +} + model Role { id BigInt @id @default(autoincrement()) code String @unique @db.VarChar(50) @@ -486,7 +493,8 @@ model AdjustmentReason { id BigInt @id @default(autoincrement()) code String @unique @db.VarChar(50) name String @db.VarChar(100) - category String @db.VarChar(50) + description String? @db.VarChar(255) + category AdjustmentReasonCategory status String @default("ACTIVE") @db.VarChar(20) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/src/app/api/v1/adjustment-reasons/[id]/route.ts b/src/app/api/v1/adjustment-reasons/[id]/route.ts index 64cab5a..99ea487 100644 --- a/src/app/api/v1/adjustment-reasons/[id]/route.ts +++ b/src/app/api/v1/adjustment-reasons/[id]/route.ts @@ -66,6 +66,7 @@ export async function PUT(request: Request, context: RouteContext) { data: { code: resolvedCode.code, name: parsed.data.name, + description: parsed.data.description || null, category: parsed.data.category, status: parsed.data.status } @@ -83,12 +84,14 @@ export async function PUT(request: Request, context: RouteContext) { { code: existing.code, name: existing.name, + description: existing.description, category: existing.category, status: existing.status }, { code: reason.code, name: reason.name, + description: reason.description, category: reason.category, status: reason.status } diff --git a/src/app/api/v1/adjustment-reasons/route.ts b/src/app/api/v1/adjustment-reasons/route.ts index e199b72..a50e323 100644 --- a/src/app/api/v1/adjustment-reasons/route.ts +++ b/src/app/api/v1/adjustment-reasons/route.ts @@ -45,6 +45,7 @@ export async function POST(request: Request) { data: { code: resolvedCode.code, name: parsed.data.name, + description: parsed.data.description || null, category: parsed.data.category, status: parsed.data.status } diff --git a/src/app/api/v1/mobile/stock-adjustments/bootstrap/route.ts b/src/app/api/v1/mobile/stock-adjustments/bootstrap/route.ts index 7565372..14d8741 100644 --- a/src/app/api/v1/mobile/stock-adjustments/bootstrap/route.ts +++ b/src/app/api/v1/mobile/stock-adjustments/bootstrap/route.ts @@ -18,6 +18,7 @@ export async function GET(request: Request) { id: reason.id.toString(), code: reason.code, name: reason.name, + description: reason.description, category: reason.category })) } diff --git a/src/app/api/v1/stock-adjustments/route.ts b/src/app/api/v1/stock-adjustments/route.ts index 27380ab..0680358 100644 --- a/src/app/api/v1/stock-adjustments/route.ts +++ b/src/app/api/v1/stock-adjustments/route.ts @@ -118,13 +118,13 @@ export async function POST(request: Request) { status: after <= 0 ? "DEPLETED" : "ACTIVE" }; - if (parsed.data.qty_change < 0 && reason.category.toUpperCase() === "SHRINKAGE") { + if (parsed.data.qty_change < 0 && reason.category === "SHRINKAGE") { lotUpdate.shrinkageQty = new Prisma.Decimal( Number((lot.shrinkageQty.toNumber() + absChange).toFixed(3)) ); } - if (parsed.data.qty_change < 0 && reason.category.toUpperCase() === "DAMAGE") { + if (parsed.data.qty_change < 0 && reason.category === "DAMAGE") { lotUpdate.damagedQty = new Prisma.Decimal( Number((lot.damagedQty.toNumber() + absChange).toFixed(3)) ); diff --git a/src/components/master-data/adjustment-reasons-client.tsx b/src/components/master-data/adjustment-reasons-client.tsx index e9e8173..e871636 100644 --- a/src/components/master-data/adjustment-reasons-client.tsx +++ b/src/components/master-data/adjustment-reasons-client.tsx @@ -4,25 +4,39 @@ import { FormEvent, useEffect, useMemo, useState } from "react"; import { useLocale } from "@/components/providers/locale-provider"; import { PaginationFooter } from "@/components/shared/pagination-footer"; -import { useCurrentUser } from "@/hooks/use-current-user"; import { usePagination } from "@/hooks/use-pagination"; import type { ApiErrorResponse, DetailResponse } from "@/types/api"; -import type { AdjustmentReasonRecord, MasterStatus } from "@/types/master-data"; +import type { + AdjustmentReasonCategory, + AdjustmentReasonRecord, + MasterStatus +} from "@/types/master-data"; type FormValues = { - code: string; name: string; - category: string; + description: string; + category: AdjustmentReasonCategory; status: MasterStatus; }; const emptyForm: FormValues = { - code: "", name: "", - category: "", + description: "", + category: "ADJUSTMENT", status: "ACTIVE" }; +const categoryOptions: Array<{ value: AdjustmentReasonCategory; label: string }> = [ + { value: "ADJUSTMENT", label: "Penyesuaian / Adjustment" }, + { value: "SHRINKAGE", label: "Penyusutan / Shrinkage" }, + { value: "DAMAGE", label: "Kerusakan / Damage" }, + { value: "REGRADE", label: "Regrade / Regrade" } +]; + +function getCategoryLabel(category: AdjustmentReasonCategory) { + return categoryOptions.find((option) => option.value === category)?.label ?? category; +} + export function AdjustmentReasonsClient() { const { dict } = useLocale(); const [items, setItems] = useState([]); @@ -32,14 +46,18 @@ export function AdjustmentReasonsClient() { const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - const { canEditCode } = useCurrentUser(); + const filteredItems = useMemo(() => { const keyword = search.trim().toLowerCase(); if (!keyword) return items; + return items.filter((item) => - [item.code, item.name, item.category].some((value) => value.toLowerCase().includes(keyword)) + [item.code, item.name, item.description ?? "", item.category].some((value) => + value.toLowerCase().includes(keyword) + ) ); }, [items, search]); + const pagination = usePagination(filteredItems, 20); async function loadItems() { @@ -70,8 +88,8 @@ export function AdjustmentReasonsClient() { function startEdit(item: AdjustmentReasonRecord) { setEditingId(item.id); setForm({ - code: item.code, name: item.name, + description: item.description ?? "", category: item.category, status: item.status as MasterStatus }); @@ -82,17 +100,20 @@ export function AdjustmentReasonsClient() { event.preventDefault(); setSubmitting(true); setError(null); + try { const response = await fetch( editingId ? `/api/v1/adjustment-reasons/${editingId}` : "/api/v1/adjustment-reasons", { method: editingId ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(form) + body: JSON.stringify({ ...form, code: "" }) } ); + 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]; @@ -100,6 +121,7 @@ export function AdjustmentReasonsClient() { } throw new Error("message" in payload ? payload.message : dict.common.requestFailed); } + resetForm(); await loadItems(); } catch (err) { @@ -111,6 +133,7 @@ export function AdjustmentReasonsClient() { async function handleDelete(id: string) { if (!window.confirm(`${dict.common.delete} adjustment reason ini?`)) return; + try { const response = await fetch(`/api/v1/adjustment-reasons/${id}`, { method: "DELETE" @@ -142,29 +165,52 @@ export function AdjustmentReasonsClient() {

{editingId ? ( - + ) : null}
- setForm((current) => ({ ...current, code: value }))} - /> setForm((current) => ({ ...current, name: value }))} /> - setForm((current) => ({ ...current, category: value }))} - /> +
+ +