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 */}
+