Standardize adjustment reason categories and descriptions
This commit is contained in:
@ -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");
|
||||||
@ -8,6 +8,13 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AdjustmentReasonCategory {
|
||||||
|
SHRINKAGE
|
||||||
|
DAMAGE
|
||||||
|
REGRADE
|
||||||
|
ADJUSTMENT
|
||||||
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
code String @unique @db.VarChar(50)
|
code String @unique @db.VarChar(50)
|
||||||
@ -486,7 +493,8 @@ model AdjustmentReason {
|
|||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
code String @unique @db.VarChar(50)
|
code String @unique @db.VarChar(50)
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
category String @db.VarChar(50)
|
description String? @db.VarChar(255)
|
||||||
|
category AdjustmentReasonCategory
|
||||||
status String @default("ACTIVE") @db.VarChar(20)
|
status String @default("ACTIVE") @db.VarChar(20)
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export async function PUT(request: Request, context: RouteContext) {
|
|||||||
data: {
|
data: {
|
||||||
code: resolvedCode.code,
|
code: resolvedCode.code,
|
||||||
name: parsed.data.name,
|
name: parsed.data.name,
|
||||||
|
description: parsed.data.description || null,
|
||||||
category: parsed.data.category,
|
category: parsed.data.category,
|
||||||
status: parsed.data.status
|
status: parsed.data.status
|
||||||
}
|
}
|
||||||
@ -83,12 +84,14 @@ export async function PUT(request: Request, context: RouteContext) {
|
|||||||
{
|
{
|
||||||
code: existing.code,
|
code: existing.code,
|
||||||
name: existing.name,
|
name: existing.name,
|
||||||
|
description: existing.description,
|
||||||
category: existing.category,
|
category: existing.category,
|
||||||
status: existing.status
|
status: existing.status
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: reason.code,
|
code: reason.code,
|
||||||
name: reason.name,
|
name: reason.name,
|
||||||
|
description: reason.description,
|
||||||
category: reason.category,
|
category: reason.category,
|
||||||
status: reason.status
|
status: reason.status
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export async function POST(request: Request) {
|
|||||||
data: {
|
data: {
|
||||||
code: resolvedCode.code,
|
code: resolvedCode.code,
|
||||||
name: parsed.data.name,
|
name: parsed.data.name,
|
||||||
|
description: parsed.data.description || null,
|
||||||
category: parsed.data.category,
|
category: parsed.data.category,
|
||||||
status: parsed.data.status
|
status: parsed.data.status
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export async function GET(request: Request) {
|
|||||||
id: reason.id.toString(),
|
id: reason.id.toString(),
|
||||||
code: reason.code,
|
code: reason.code,
|
||||||
name: reason.name,
|
name: reason.name,
|
||||||
|
description: reason.description,
|
||||||
category: reason.category
|
category: reason.category
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,13 +118,13 @@ export async function POST(request: Request) {
|
|||||||
status: after <= 0 ? "DEPLETED" : "ACTIVE"
|
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(
|
lotUpdate.shrinkageQty = new Prisma.Decimal(
|
||||||
Number((lot.shrinkageQty.toNumber() + absChange).toFixed(3))
|
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(
|
lotUpdate.damagedQty = new Prisma.Decimal(
|
||||||
Number((lot.damagedQty.toNumber() + absChange).toFixed(3))
|
Number((lot.damagedQty.toNumber() + absChange).toFixed(3))
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,25 +4,39 @@ import { FormEvent, useEffect, useMemo, useState } from "react";
|
|||||||
|
|
||||||
import { useLocale } from "@/components/providers/locale-provider";
|
import { useLocale } from "@/components/providers/locale-provider";
|
||||||
import { PaginationFooter } from "@/components/shared/pagination-footer";
|
import { PaginationFooter } from "@/components/shared/pagination-footer";
|
||||||
import { useCurrentUser } from "@/hooks/use-current-user";
|
|
||||||
import { usePagination } from "@/hooks/use-pagination";
|
import { usePagination } from "@/hooks/use-pagination";
|
||||||
import type { ApiErrorResponse, DetailResponse } from "@/types/api";
|
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 = {
|
type FormValues = {
|
||||||
code: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
description: string;
|
||||||
|
category: AdjustmentReasonCategory;
|
||||||
status: MasterStatus;
|
status: MasterStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyForm: FormValues = {
|
const emptyForm: FormValues = {
|
||||||
code: "",
|
|
||||||
name: "",
|
name: "",
|
||||||
category: "",
|
description: "",
|
||||||
|
category: "ADJUSTMENT",
|
||||||
status: "ACTIVE"
|
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() {
|
export function AdjustmentReasonsClient() {
|
||||||
const { dict } = useLocale();
|
const { dict } = useLocale();
|
||||||
const [items, setItems] = useState<AdjustmentReasonRecord[]>([]);
|
const [items, setItems] = useState<AdjustmentReasonRecord[]>([]);
|
||||||
@ -32,14 +46,18 @@ export function AdjustmentReasonsClient() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { canEditCode } = useCurrentUser();
|
|
||||||
const filteredItems = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
const keyword = search.trim().toLowerCase();
|
const keyword = search.trim().toLowerCase();
|
||||||
if (!keyword) return items;
|
if (!keyword) return items;
|
||||||
|
|
||||||
return items.filter((item) =>
|
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]);
|
}, [items, search]);
|
||||||
|
|
||||||
const pagination = usePagination(filteredItems, 20);
|
const pagination = usePagination(filteredItems, 20);
|
||||||
|
|
||||||
async function loadItems() {
|
async function loadItems() {
|
||||||
@ -70,8 +88,8 @@ export function AdjustmentReasonsClient() {
|
|||||||
function startEdit(item: AdjustmentReasonRecord) {
|
function startEdit(item: AdjustmentReasonRecord) {
|
||||||
setEditingId(item.id);
|
setEditingId(item.id);
|
||||||
setForm({
|
setForm({
|
||||||
code: item.code,
|
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
description: item.description ?? "",
|
||||||
category: item.category,
|
category: item.category,
|
||||||
status: item.status as MasterStatus
|
status: item.status as MasterStatus
|
||||||
});
|
});
|
||||||
@ -82,17 +100,20 @@ export function AdjustmentReasonsClient() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
editingId ? `/api/v1/adjustment-reasons/${editingId}` : "/api/v1/adjustment-reasons",
|
editingId ? `/api/v1/adjustment-reasons/${editingId}` : "/api/v1/adjustment-reasons",
|
||||||
{
|
{
|
||||||
method: editingId ? "PUT" : "POST",
|
method: editingId ? "PUT" : "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(form)
|
body: JSON.stringify({ ...form, code: "" })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload =
|
const payload =
|
||||||
(await response.json()) as DetailResponse<AdjustmentReasonRecord> | ApiErrorResponse;
|
(await response.json()) as DetailResponse<AdjustmentReasonRecord> | ApiErrorResponse;
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if ("errors" in payload && payload.errors) {
|
if ("errors" in payload && payload.errors) {
|
||||||
const firstError = Object.values(payload.errors)[0]?.[0];
|
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);
|
throw new Error("message" in payload ? payload.message : dict.common.requestFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetForm();
|
resetForm();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -111,6 +133,7 @@ export function AdjustmentReasonsClient() {
|
|||||||
|
|
||||||
async function handleDelete(id: string) {
|
async function handleDelete(id: string) {
|
||||||
if (!window.confirm(`${dict.common.delete} adjustment reason ini?`)) return;
|
if (!window.confirm(`${dict.common.delete} adjustment reason ini?`)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/adjustment-reasons/${id}`, {
|
const response = await fetch(`/api/v1/adjustment-reasons/${id}`, {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
@ -142,29 +165,52 @@ export function AdjustmentReasonsClient() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{editingId ? (
|
{editingId ? (
|
||||||
<button type="button" onClick={resetForm} className="ops-btn-secondary">{dict.common.cancelEdit}</button>
|
<button type="button" onClick={resetForm} className="ops-btn-secondary">
|
||||||
|
{dict.common.cancelEdit}
|
||||||
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Field
|
|
||||||
label={dict.master.code}
|
|
||||||
value={form.code}
|
|
||||||
readOnly={!canEditCode}
|
|
||||||
placeholder={canEditCode ? "ADJ00001" : "Auto"}
|
|
||||||
onChange={(value) => setForm((current) => ({ ...current, code: value }))}
|
|
||||||
/>
|
|
||||||
<Field
|
<Field
|
||||||
label={dict.master.name}
|
label={dict.master.name}
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(value) => setForm((current) => ({ ...current, name: value }))}
|
onChange={(value) => setForm((current) => ({ ...current, name: value }))}
|
||||||
/>
|
/>
|
||||||
<Field
|
<label className="block">
|
||||||
label="Kategori"
|
<span className="ops-label">Kategori</span>
|
||||||
value={form.category}
|
<select
|
||||||
onChange={(value) => setForm((current) => ({ ...current, category: value }))}
|
value={form.category}
|
||||||
/>
|
onChange={(event) =>
|
||||||
|
setForm((current) => ({
|
||||||
|
...current,
|
||||||
|
category: event.target.value as AdjustmentReasonCategory
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="ops-select"
|
||||||
|
>
|
||||||
|
{categoryOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label className="block">
|
||||||
|
<span className="ops-label">{dict.master.description}</span>
|
||||||
|
<textarea
|
||||||
|
value={form.description}
|
||||||
|
onChange={(event) =>
|
||||||
|
setForm((current) => ({ ...current, description: event.target.value }))
|
||||||
|
}
|
||||||
|
rows={4}
|
||||||
|
className="ops-textarea"
|
||||||
|
placeholder="Penjelasan kapan alasan ini dipakai"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="ops-label">{dict.master.status}</span>
|
<span className="ops-label">{dict.master.status}</span>
|
||||||
<select
|
<select
|
||||||
@ -181,11 +227,13 @@ export function AdjustmentReasonsClient() {
|
|||||||
<option value="INACTIVE">{dict.master.inactive}</option>
|
<option value="INACTIVE">{dict.master.inactive}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="rounded border border-ember/30 bg-ember/10 px-4 py-3 text-sm text-ember">
|
<div className="rounded border border-ember/30 bg-ember/10 px-4 py-3 text-sm text-ember">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<button type="submit" disabled={submitting} className="ops-btn-primary">
|
<button type="submit" disabled={submitting} className="ops-btn-primary">
|
||||||
{submitting
|
{submitting
|
||||||
? dict.common.processing
|
? dict.common.processing
|
||||||
@ -195,6 +243,7 @@ export function AdjustmentReasonsClient() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ops-table-shell">
|
<div className="ops-table-shell">
|
||||||
<div className="ops-section-head">
|
<div className="ops-section-head">
|
||||||
<div>
|
<div>
|
||||||
@ -205,6 +254,7 @@ export function AdjustmentReasonsClient() {
|
|||||||
{items.length} {dict.master.dataCount}
|
{items.length} {dict.master.dataCount}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!loading && items.length > 0 ? (
|
{!loading && items.length > 0 ? (
|
||||||
<div className="border-t border-line/70 px-6 py-4">
|
<div className="border-t border-line/70 px-6 py-4">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
@ -212,18 +262,17 @@ export function AdjustmentReasonsClient() {
|
|||||||
<input
|
<input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
placeholder="Cari kode, nama, atau kategori..."
|
placeholder="Cari kode, nama, deskripsi, atau kategori..."
|
||||||
className="ops-input"
|
className="ops-input"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="px-6 py-8 text-sm text-ink/60">{dict.common.loading}</div>
|
<div className="px-6 py-8 text-sm text-ink/60">{dict.common.loading}</div>
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<div className="px-6 py-8 text-sm text-ink/60">
|
<div className="px-6 py-8 text-sm text-ink/60">{dict.master.noData}</div>
|
||||||
{dict.master.noData}
|
|
||||||
</div>
|
|
||||||
) : filteredItems.length === 0 ? (
|
) : filteredItems.length === 0 ? (
|
||||||
<div className="px-6 py-8 text-sm text-ink/60">
|
<div className="px-6 py-8 text-sm text-ink/60">
|
||||||
Tidak ada alasan penyesuaian yang cocok dengan pencarian.
|
Tidak ada alasan penyesuaian yang cocok dengan pencarian.
|
||||||
@ -236,6 +285,7 @@ export function AdjustmentReasonsClient() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{dict.master.code}</th>
|
<th>{dict.master.code}</th>
|
||||||
<th>{dict.master.name}</th>
|
<th>{dict.master.name}</th>
|
||||||
|
<th>{dict.master.description}</th>
|
||||||
<th>Kategori</th>
|
<th>Kategori</th>
|
||||||
<th>{dict.master.status}</th>
|
<th>{dict.master.status}</th>
|
||||||
<th>{dict.common.actions}</th>
|
<th>{dict.common.actions}</th>
|
||||||
@ -246,16 +296,35 @@ export function AdjustmentReasonsClient() {
|
|||||||
<tr key={item.id}>
|
<tr key={item.id}>
|
||||||
<td className="font-semibold text-ink">{item.code}</td>
|
<td className="font-semibold text-ink">{item.code}</td>
|
||||||
<td>{item.name}</td>
|
<td>{item.name}</td>
|
||||||
<td className="text-slate-600">{item.category}</td>
|
<td className="text-slate-600">{item.description || "-"}</td>
|
||||||
|
<td className="text-slate-600">{getCategoryLabel(item.category)}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={item.status === "INACTIVE" ? "ops-chip-danger" : "ops-chip-active"}>
|
<span
|
||||||
{item.status === "INACTIVE" ? dict.master.inactive : dict.master.active}
|
className={
|
||||||
|
item.status === "INACTIVE" ? "ops-chip-danger" : "ops-chip-active"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.status === "INACTIVE"
|
||||||
|
? dict.master.inactive
|
||||||
|
: dict.master.active}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button type="button" onClick={() => startEdit(item)} className="ops-btn-secondary">{dict.common.edit}</button>
|
<button
|
||||||
<button type="button" onClick={() => void handleDelete(item.id)} className="ops-btn-danger">{dict.common.delete}</button>
|
type="button"
|
||||||
|
onClick={() => startEdit(item)}
|
||||||
|
className="ops-btn-secondary"
|
||||||
|
>
|
||||||
|
{dict.common.edit}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleDelete(item.id)}
|
||||||
|
className="ops-btn-danger"
|
||||||
|
>
|
||||||
|
{dict.common.delete}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -271,7 +340,9 @@ export function AdjustmentReasonsClient() {
|
|||||||
totalItems={pagination.totalItems}
|
totalItems={pagination.totalItems}
|
||||||
itemLabel="adjustment reason"
|
itemLabel="adjustment reason"
|
||||||
onPrev={() => pagination.setPage((page) => Math.max(1, page - 1))}
|
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,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
readOnly = false,
|
|
||||||
placeholder
|
placeholder
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
readOnly?: boolean;
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -298,7 +367,6 @@ function Field({
|
|||||||
<span className="ops-label">{label}</span>
|
<span className="ops-label">{label}</span>
|
||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
readOnly={readOnly}
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
className="ops-input"
|
className="ops-input"
|
||||||
|
|||||||
@ -4,7 +4,8 @@ type SerializableAdjustmentReason = {
|
|||||||
id: bigint;
|
id: bigint;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
description: string | null;
|
||||||
|
category: AdjustmentReasonRecord["category"];
|
||||||
status: string;
|
status: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@ -17,6 +18,7 @@ export function serializeAdjustmentReason(
|
|||||||
id: reason.id.toString(),
|
id: reason.id.toString(),
|
||||||
code: reason.code,
|
code: reason.code,
|
||||||
name: reason.name,
|
name: reason.name,
|
||||||
|
description: reason.description,
|
||||||
category: reason.category,
|
category: reason.category,
|
||||||
status: reason.status,
|
status: reason.status,
|
||||||
created_at: reason.createdAt.toISOString(),
|
created_at: reason.createdAt.toISOString(),
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const adjustmentReasonCategorySchema = z.enum([
|
||||||
|
"SHRINKAGE",
|
||||||
|
"DAMAGE",
|
||||||
|
"REGRADE",
|
||||||
|
"ADJUSTMENT"
|
||||||
|
]);
|
||||||
|
|
||||||
export const adjustmentReasonInputSchema = z.object({
|
export const adjustmentReasonInputSchema = z.object({
|
||||||
code: z.string().trim().max(50).optional().or(z.literal("")),
|
code: z.string().trim().max(50).optional().or(z.literal("")),
|
||||||
name: z.string().trim().min(1, "Nama alasan wajib diisi").max(100),
|
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")
|
status: z.enum(["ACTIVE", "INACTIVE"]).default("ACTIVE")
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,6 +26,13 @@ const emptyForm = (): StockAdjustmentForm => ({
|
|||||||
notes: ""
|
notes: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const adjustmentReasonCategoryLabels: Record<AdjustmentReasonRecord["category"], string> = {
|
||||||
|
ADJUSTMENT: "Penyesuaian / Adjustment",
|
||||||
|
SHRINKAGE: "Penyusutan / Shrinkage",
|
||||||
|
DAMAGE: "Kerusakan / Damage",
|
||||||
|
REGRADE: "Regrade / Regrade"
|
||||||
|
};
|
||||||
|
|
||||||
export function StockAdjustmentsClient() {
|
export function StockAdjustmentsClient() {
|
||||||
const { dict, locale } = useLocale();
|
const { dict, locale } = useLocale();
|
||||||
const [items, setItems] = useState<StockAdjustmentListItem[]>([]);
|
const [items, setItems] = useState<StockAdjustmentListItem[]>([]);
|
||||||
@ -167,14 +174,24 @@ export function StockAdjustmentsClient() {
|
|||||||
}
|
}
|
||||||
options={reasons.map((reason) => ({
|
options={reasons.map((reason) => ({
|
||||||
value: reason.id,
|
value: reason.id,
|
||||||
label: `${reason.code} - ${reason.name} (${reason.category})`
|
label: `${reason.code} - ${reason.name} (${adjustmentReasonCategoryLabels[reason.category]})`
|
||||||
}))}
|
}))}
|
||||||
placeholder={dict.stockAdjustments.chooseReason}
|
placeholder={dict.stockAdjustments.chooseReason}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedReason ? (
|
{selectedReason ? (
|
||||||
<div className="rounded border border-line/70 bg-slate-50 px-4 py-4 text-sm text-slate-600">
|
<div className="rounded border border-line/70 bg-slate-50 px-4 py-4 text-sm text-slate-600">
|
||||||
{dict.stockAdjustments.category}: <span className="font-medium text-ink">{selectedReason.category}</span>
|
<div>
|
||||||
|
{dict.stockAdjustments.category}:{" "}
|
||||||
|
<span className="font-medium text-ink">
|
||||||
|
{adjustmentReasonCategoryLabels[selectedReason.category]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{selectedReason.description ? (
|
||||||
|
<div className="mt-2 text-xs leading-5 text-slate-500">
|
||||||
|
{selectedReason.description}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -261,7 +278,14 @@ export function StockAdjustmentsClient() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="text-slate-600">
|
<td className="text-slate-600">
|
||||||
{item.reason.name}
|
{item.reason.name}
|
||||||
<div className="mt-1 text-xs text-slate-500">{item.reason.category}</div>
|
<div className="mt-1 text-xs text-slate-500">
|
||||||
|
{adjustmentReasonCategoryLabels[item.reason.category]}
|
||||||
|
</div>
|
||||||
|
{item.reason.description ? (
|
||||||
|
<div className="mt-1 text-xs leading-5 text-slate-500">
|
||||||
|
{item.reason.description}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td className={item.qty_change < 0 ? "text-ember" : "text-moss"}>
|
<td className={item.qty_change < 0 ? "text-ember" : "text-moss"}>
|
||||||
{item.qty_change > 0 ? "+" : ""}
|
{item.qty_change > 0 ? "+" : ""}
|
||||||
|
|||||||
@ -20,7 +20,8 @@ type SerializableStockAdjustment = {
|
|||||||
id: bigint;
|
id: bigint;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
description: string | null;
|
||||||
|
category: StockAdjustmentListItem["reason"]["category"];
|
||||||
};
|
};
|
||||||
createdBy: {
|
createdBy: {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
@ -46,6 +47,7 @@ export function serializeStockAdjustment(
|
|||||||
id: item.adjustmentReason.id.toString(),
|
id: item.adjustmentReason.id.toString(),
|
||||||
code: item.adjustmentReason.code,
|
code: item.adjustmentReason.code,
|
||||||
name: item.adjustmentReason.name,
|
name: item.adjustmentReason.name,
|
||||||
|
description: item.adjustmentReason.description,
|
||||||
category: item.adjustmentReason.category
|
category: item.adjustmentReason.category
|
||||||
},
|
},
|
||||||
qty_change: item.qtyChange.toNumber(),
|
qty_change: item.qtyChange.toNumber(),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type MasterStatus = "ACTIVE" | "INACTIVE";
|
export type MasterStatus = "ACTIVE" | "INACTIVE";
|
||||||
|
export type AdjustmentReasonCategory = "SHRINKAGE" | "DAMAGE" | "REGRADE" | "ADJUSTMENT";
|
||||||
|
|
||||||
export type GradePriceStandardRecord = {
|
export type GradePriceStandardRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -49,7 +50,8 @@ export type AdjustmentReasonRecord = {
|
|||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
description: string | null;
|
||||||
|
category: AdjustmentReasonCategory;
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { AdjustmentReasonCategory } from "@/types/master-data";
|
||||||
|
|
||||||
export type StockAdjustmentListItem = {
|
export type StockAdjustmentListItem = {
|
||||||
id: string;
|
id: string;
|
||||||
adjustment_no: string;
|
adjustment_no: string;
|
||||||
@ -12,7 +14,8 @@ export type StockAdjustmentListItem = {
|
|||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
description: string | null;
|
||||||
|
category: AdjustmentReasonCategory;
|
||||||
};
|
};
|
||||||
qty_change: number;
|
qty_change: number;
|
||||||
available_qty_before: number;
|
available_qty_before: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user