diff --git a/.claude.zip b/.claude.zip new file mode 100644 index 0000000..41161fe Binary files /dev/null and b/.claude.zip differ diff --git a/docs.zip b/docs.zip new file mode 100644 index 0000000..bd3f95e Binary files /dev/null and b/docs.zip differ diff --git a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx index 0df7f9f..c580d75 100644 --- a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx +++ b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx @@ -73,17 +73,16 @@ export function PeWorkspaceCreateView({ // Mig 23 — fetch list quy trình duyệt V2 cho User chọn (filter theo // ApplicableType khớp với defaultType: 1=DuyetNcc / 2=DuyetNccPhuongAn). + // Mig 25 — chỉ hiện workflows admin đã ghim "cho user chọn" (IsUserSelectable=true). const approvalWorkflows = useQuery({ queryKey: ['approval-workflows-v2-active', defaultType], queryFn: async () => { - const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>( + const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean; isUserSelectable: boolean }[] }[] }>( '/approval-workflows-v2', { params: { applicableType: defaultType } }, ) - // Trả về tất cả version (active + archived) cho User pick — UAT cần - // flexibility chọn cả version cũ test compare. const typeBucket = res.data.types.find(t => t.applicableType === defaultType) - return typeBucket?.history ?? [] + return (typeBucket?.history ?? []).filter(w => w.isUserSelectable) }, }) diff --git a/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx b/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx index 4db6a7d..922e36e 100644 --- a/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx +++ b/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx @@ -19,7 +19,7 @@ import { useMemo, useState, type FormEvent } from 'react' import { useParams } from 'react-router-dom' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { GitBranch, Plus, Trash2, CheckCircle2, History, Workflow, ChevronUp, ChevronDown } from 'lucide-react' +import { GitBranch, Plus, Trash2, CheckCircle2, History, Workflow, ChevronUp, ChevronDown, Pin, PinOff } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { Button } from '@/components/ui/Button' @@ -59,6 +59,7 @@ type DefinitionDto = { name: string description: string | null isActive: boolean + isUserSelectable: boolean // Mig 25 — admin toggle cho user pick activatedAt: string | null createdAt: string steps: StepDto[] @@ -214,6 +215,19 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi onError: err => toast.error(getErrorMessage(err)), }) + // Mig 25 — toggle "cho user pick lúc create phiếu" (stick/unstick) + const toggleSelectable = useMutation({ + mutationFn: async ({ id, isUserSelectable }: { id: string; isUserSelectable: boolean }) => + api.patch(`/approval-workflows-v2/${id}/user-selectable`, { isUserSelectable }), + onSuccess: (_data, vars) => { + toast.success(vars.isUserSelectable + ? 'Đã ghim — user có thể chọn quy trình này' + : 'Đã bỏ ghim — user không thấy quy trình này') + qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] }) + }, + onError: err => toast.error(getErrorMessage(err)), + }) + return (
{type.active ? ( @@ -221,6 +235,10 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi def={type.active} isActive onClone={d => { setCloneFrom(d); setDesignerOpen(true) }} + onToggleSelectable={() => toggleSelectable.mutate({ + id: type.active!.id, + isUserSelectable: !type.active!.isUserSelectable, + })} onDelete={() => { if (confirm(`Xoá version đang áp dụng "${type.active!.code} v${type.active!.version}"?`)) { del.mutate(type.active!.id) @@ -254,6 +272,10 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi def={d} isActive={false} onClone={dd => { setCloneFrom(dd); setDesignerOpen(true) }} + onToggleSelectable={() => toggleSelectable.mutate({ + id: d.id, + isUserSelectable: !d.isUserSelectable, + })} onDelete={() => { if (confirm(`Xoá version "${d.code} v${d.version}"?`)) del.mutate(d.id) }} @@ -280,11 +302,13 @@ function DefinitionCard({ def, isActive, onClone, + onToggleSelectable, onDelete, }: { def: DefinitionDto isActive: boolean onClone: (d: DefinitionDto) => void + onToggleSelectable: () => void onDelete: () => void }) { return ( @@ -307,6 +331,13 @@ function DefinitionCard({ Archived )} + {/* Mig 25 — badge IsUserSelectable: ghim cho user pick */} + {def.isUserSelectable && ( + + + Cho user chọn + + )}
{def.description &&

{def.description}

} @@ -368,6 +399,16 @@ function DefinitionCard({ Tạo từ bản này + {/* Mig 25 — toggle stick: cho user chọn quy trình này khi tạo phiếu */} +