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}
+
@@ -205,6 +254,7 @@ export function AdjustmentReasonsClient() {
{items.length} {dict.master.dataCount}
+
{!loading && items.length > 0 ? (
) : null}
+
{loading ? (
{dict.common.loading}
) : items.length === 0 ? (
-
- {dict.master.noData}
-
+
{dict.master.noData}
) : filteredItems.length === 0 ? (
Tidak ada alasan penyesuaian yang cocok dengan pencarian.
@@ -236,6 +285,7 @@ export function AdjustmentReasonsClient() {
| {dict.master.code} |
{dict.master.name} |
+ {dict.master.description} |
Kategori |
{dict.master.status} |
{dict.common.actions} |
@@ -246,16 +296,35 @@ export function AdjustmentReasonsClient() {
| {item.code} |
{item.name} |
- {item.category} |
+ {item.description || "-"} |
+ {getCategoryLabel(item.category)} |
-
- {item.status === "INACTIVE" ? dict.master.inactive : dict.master.active}
+
+ {item.status === "INACTIVE"
+ ? dict.master.inactive
+ : dict.master.active}
|
-
-
+
+
|
@@ -271,7 +340,9 @@ export function AdjustmentReasonsClient() {
totalItems={pagination.totalItems}
itemLabel="adjustment reason"
onPrev={() => pagination.setPage((page) => Math.max(1, page - 1))}
- onNext={() => pagination.setPage((page) => Math.min(pagination.totalPages, page + 1))}
+ onNext={() =>
+ pagination.setPage((page) => Math.min(pagination.totalPages, page + 1))
+ }
/>
>
)}
@@ -284,13 +355,11 @@ function Field({
label,
value,
onChange,
- readOnly = false,
placeholder
}: {
label: string;
value: string;
onChange: (value: string) => void;
- readOnly?: boolean;
placeholder?: string;
}) {
return (
@@ -298,7 +367,6 @@ function Field({
{label}
onChange(event.target.value)}
className="ops-input"
diff --git a/src/features/adjustment-reasons/lib/serialize-adjustment-reason.ts b/src/features/adjustment-reasons/lib/serialize-adjustment-reason.ts
index 72bdb58..c9ba42e 100644
--- a/src/features/adjustment-reasons/lib/serialize-adjustment-reason.ts
+++ b/src/features/adjustment-reasons/lib/serialize-adjustment-reason.ts
@@ -4,7 +4,8 @@ type SerializableAdjustmentReason = {
id: bigint;
code: string;
name: string;
- category: string;
+ description: string | null;
+ category: AdjustmentReasonRecord["category"];
status: string;
createdAt: Date;
updatedAt: Date;
@@ -17,6 +18,7 @@ export function serializeAdjustmentReason(
id: reason.id.toString(),
code: reason.code,
name: reason.name,
+ description: reason.description,
category: reason.category,
status: reason.status,
created_at: reason.createdAt.toISOString(),
diff --git a/src/features/adjustment-reasons/schemas/adjustment-reason.schema.ts b/src/features/adjustment-reasons/schemas/adjustment-reason.schema.ts
index a05e9c3..d68070c 100644
--- a/src/features/adjustment-reasons/schemas/adjustment-reason.schema.ts
+++ b/src/features/adjustment-reasons/schemas/adjustment-reason.schema.ts
@@ -1,8 +1,16 @@
import { z } from "zod";
+const adjustmentReasonCategorySchema = z.enum([
+ "SHRINKAGE",
+ "DAMAGE",
+ "REGRADE",
+ "ADJUSTMENT"
+]);
+
export const adjustmentReasonInputSchema = z.object({
code: z.string().trim().max(50).optional().or(z.literal("")),
name: z.string().trim().min(1, "Nama alasan wajib diisi").max(100),
- category: z.string().trim().min(1, "Category wajib diisi").max(50),
+ description: z.string().trim().max(255).optional().or(z.literal("")),
+ category: adjustmentReasonCategorySchema,
status: z.enum(["ACTIVE", "INACTIVE"]).default("ACTIVE")
});
diff --git a/src/features/stock-adjustments/components/stock-adjustments-client.tsx b/src/features/stock-adjustments/components/stock-adjustments-client.tsx
index d3b6664..c6701d1 100644
--- a/src/features/stock-adjustments/components/stock-adjustments-client.tsx
+++ b/src/features/stock-adjustments/components/stock-adjustments-client.tsx
@@ -26,6 +26,13 @@ const emptyForm = (): StockAdjustmentForm => ({
notes: ""
});
+const adjustmentReasonCategoryLabels: Record
= {
+ ADJUSTMENT: "Penyesuaian / Adjustment",
+ SHRINKAGE: "Penyusutan / Shrinkage",
+ DAMAGE: "Kerusakan / Damage",
+ REGRADE: "Regrade / Regrade"
+};
+
export function StockAdjustmentsClient() {
const { dict, locale } = useLocale();
const [items, setItems] = useState([]);
@@ -167,14 +174,24 @@ export function StockAdjustmentsClient() {
}
options={reasons.map((reason) => ({
value: reason.id,
- label: `${reason.code} - ${reason.name} (${reason.category})`
+ label: `${reason.code} - ${reason.name} (${adjustmentReasonCategoryLabels[reason.category]})`
}))}
placeholder={dict.stockAdjustments.chooseReason}
/>
{selectedReason ? (
- {dict.stockAdjustments.category}:
{selectedReason.category}
+
+ {dict.stockAdjustments.category}:{" "}
+
+ {adjustmentReasonCategoryLabels[selectedReason.category]}
+
+
+ {selectedReason.description ? (
+
+ {selectedReason.description}
+
+ ) : null}
) : null}
@@ -261,7 +278,14 @@ export function StockAdjustmentsClient() {
{item.reason.name}
- {item.reason.category}
+
+ {adjustmentReasonCategoryLabels[item.reason.category]}
+
+ {item.reason.description ? (
+
+ {item.reason.description}
+
+ ) : null}
|
{item.qty_change > 0 ? "+" : ""}
diff --git a/src/features/stock-adjustments/lib/serialize-stock-adjustment.ts b/src/features/stock-adjustments/lib/serialize-stock-adjustment.ts
index 0532402..5448a4b 100644
--- a/src/features/stock-adjustments/lib/serialize-stock-adjustment.ts
+++ b/src/features/stock-adjustments/lib/serialize-stock-adjustment.ts
@@ -20,7 +20,8 @@ type SerializableStockAdjustment = {
id: bigint;
code: string;
name: string;
- category: string;
+ description: string | null;
+ category: StockAdjustmentListItem["reason"]["category"];
};
createdBy: {
id: bigint;
@@ -46,6 +47,7 @@ export function serializeStockAdjustment(
id: item.adjustmentReason.id.toString(),
code: item.adjustmentReason.code,
name: item.adjustmentReason.name,
+ description: item.adjustmentReason.description,
category: item.adjustmentReason.category
},
qty_change: item.qtyChange.toNumber(),
diff --git a/src/types/master-data.ts b/src/types/master-data.ts
index 4bec04a..2c0f322 100644
--- a/src/types/master-data.ts
+++ b/src/types/master-data.ts
@@ -1,4 +1,5 @@
export type MasterStatus = "ACTIVE" | "INACTIVE";
+export type AdjustmentReasonCategory = "SHRINKAGE" | "DAMAGE" | "REGRADE" | "ADJUSTMENT";
export type GradePriceStandardRecord = {
id: string;
@@ -49,7 +50,8 @@ export type AdjustmentReasonRecord = {
id: string;
code: string;
name: string;
- category: string;
+ description: string | null;
+ category: AdjustmentReasonCategory;
status: string;
created_at: string;
updated_at: string;
diff --git a/src/types/stock-adjustment.ts b/src/types/stock-adjustment.ts
index 2e6ff01..7d7be82 100644
--- a/src/types/stock-adjustment.ts
+++ b/src/types/stock-adjustment.ts
@@ -1,3 +1,5 @@
+import type { AdjustmentReasonCategory } from "@/types/master-data";
+
export type StockAdjustmentListItem = {
id: string;
adjustment_no: string;
@@ -12,7 +14,8 @@ export type StockAdjustmentListItem = {
id: string;
code: string;
name: string;
- category: string;
+ description: string | null;
+ category: AdjustmentReasonCategory;
};
qty_change: number;
available_qty_before: number;
|