ignore folder

This commit is contained in:
2026-04-21 06:30:48 +07:00
commit ca00b36f19
70 changed files with 3871 additions and 0 deletions

View File

@ -0,0 +1,197 @@
"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import {
approveWorkflowRequest,
getWorkflowRequests,
rejectWorkflowRequest,
WorkflowFilters
} from "@/services/workflow";
import { useApiStore } from "@/store/uiStore";
import { useLocaleStore } from "@/store/uiStore";
import { t } from "@/lib/locale";
import DataTable from "@/components/ui/Table";
import StatusBadge from "@/components/workflow/StatusBadge";
import ApprovalActionModal from "@/components/workflow/ApprovalActionModal";
import PageHeader from "@/components/ui/PageHeader";
import EmptyState from "@/components/ui/EmptyState";
import { WorkflowRequestItem, WorkflowStatus } from "@/types/api";
const DEFAULT_FILTERS: WorkflowFilters = {
status: "",
resourceType: "",
makerUsername: "",
limit: 50
};
export default function WorkflowPage() {
const [filters, setFilters] = useState<WorkflowFilters>(DEFAULT_FILTERS);
const [requests, setRequests] = useState<WorkflowRequestItem[]>([]);
const [loading, setLoading] = useState(false);
const [approvalRequest, setApprovalRequest] = useState<WorkflowRequestItem | null>(null);
const [actionType, setActionType] = useState<"approve" | "reject">("approve");
const [isOpen, setIsOpen] = useState(false);
const addToast = useApiStore((s) => s.addToast);
const locale = useLocaleStore((s) => s.locale);
const filtered = useMemo(
() =>
requests.filter((item) => {
const matchesStatus = !filters.status || item.status === filters.status;
const matchesType = !filters.resourceType || item.resourceType === filters.resourceType;
const matchesMaker = !filters.makerUsername || item.makerUsername.includes(filters.makerUsername);
return matchesStatus && matchesType && matchesMaker;
}),
[filters, requests]
);
const load = async () => {
setLoading(true);
try {
const response = await getWorkflowRequests(filters);
setRequests(response);
} catch (error) {
addToast((error as { message?: string })?.message || t("loadFailed", locale), "error");
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, [filters]);
const onSubmitAction = async (notes?: string, checkerRole?: string) => {
if (!approvalRequest) return;
try {
if (actionType === "approve") {
await approveWorkflowRequest(approvalRequest.id, { notes, checkerRole });
addToast(t("requestApproved", locale), "success");
} else {
await rejectWorkflowRequest(approvalRequest.id, { notes, checkerRole });
addToast(t("requestRejected", locale), "success");
}
setIsOpen(false);
load();
} catch (error) {
addToast((error as { message?: string })?.message || t("actionFailed", locale), "error");
}
};
const submitFilters = (e: FormEvent) => {
e.preventDefault();
load();
};
return (
<main className="vstack gap-3">
<PageHeader
title={t("workflow", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("workflow", locale), href: "/dashboard/workflow" }
]}
/>
<form className="card page-card card-body" onSubmit={submitFilters}>
<div className="form-grid">
<select
className="form-select"
value={filters.status ?? ""}
onChange={(e) => setFilters((prev) => ({ ...prev, status: e.target.value as WorkflowStatus | "" }))}
>
<option value="">{t("allStatuses", locale)}</option>
<option value="DRAFT">DRAFT</option>
<option value="PENDING">PENDING</option>
<option value="APPROVED">APPROVED</option>
<option value="REJECTED">REJECTED</option>
</select>
<input
className="form-control"
placeholder="Resource type"
value={filters.resourceType ?? ""}
onChange={(e) => setFilters((prev) => ({ ...prev, resourceType: e.target.value }))}
/>
<input
className="form-control"
placeholder="Maker username"
value={filters.makerUsername ?? ""}
onChange={(e) => setFilters((prev) => ({ ...prev, makerUsername: e.target.value }))}
/>
<input
className="form-control"
type="number"
min={1}
max={200}
value={filters.limit ?? 50}
onChange={(e) => setFilters((prev) => ({ ...prev, limit: Number(e.target.value) }))}
/>
<button className="btn btn-outline-primary">Apply</button>
</div>
</form>
<section className="card page-card">
<div className="card-body">
{filtered.length === 0 && !loading && (
<EmptyState
title={t("noData", locale)}
description={t("noData", locale)}
/>
)}
<DataTable
loading={loading}
columns={[
{ key: "id", header: "Request Id" },
{ key: "resourceType", header: "Resource Type" },
{ key: "resourceId", header: "Resource Id" },
{ key: "makerUsername", header: "Maker" },
{
key: "status",
header: "Status",
render: (row) => <StatusBadge status={row.status} />
},
{ key: "currentStep", header: "Current Step" },
{ key: "requiredSteps", header: "Required Steps" },
{ key: "updatedAt", header: "Updated At" },
{
key: "actions",
header: "Actions",
render: (row) => (
<div className="d-flex gap-2">
<button
className="btn btn-success btn-sm"
onClick={() => {
setApprovalRequest(row);
setActionType("approve");
setIsOpen(true);
}}
>
{t("approve", locale)}
</button>
<button
className="btn btn-danger btn-sm"
onClick={() => {
setApprovalRequest(row);
setActionType("reject");
setIsOpen(true);
}}
>
{t("reject", locale)}
</button>
</div>
)
}
]}
data={filtered}
/>
</div>
</section>
<ApprovalActionModal
isOpen={isOpen}
mode={actionType}
onSubmit={onSubmitAction}
onClose={() => setIsOpen(false)}
title={actionType === "approve" ? t("approveRequest", locale) : t("rejectRequest", locale)}
/>
</main>
);
}