[CLAUDE] FE-Admin: chọn "Phòng cha" trong quản lý phòng ban — dựng cây tổ chức
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m4s

Anh chốt self-service: admin gán phòng cha để dựng sơ đồ org cho trang Hồ sơ Nhân sự.
DepartmentsPage: +Select "Phòng cha (Thuộc khối/phòng)" (option "— Không có (cấp gốc) —"
= null) + query departments-all (pageSize 200) + cột "Thuộc" hiện tên phòng cha + gửi
parentId trong Create/Update + pre-select khi sửa + loại-trừ-chính-nó khỏi dropdown
(cycle sâu hơn = BE 409 ConflictException -> toast). +parentId vào type Department.
Build PASS fe-admin (0 TS error). Mirror fe-user defer (quản lý phòng ban = admin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-16 10:41:00 +07:00
parent 0f44d9754d
commit 8c8179cda0
2 changed files with 33 additions and 4 deletions

View File

@ -8,6 +8,7 @@ import { PermissionGuard } from '@/components/PermissionGuard'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea'
import { Dialog } from '@/components/ui/Dialog'
import { api } from '@/lib/api'
@ -15,8 +16,8 @@ import { getErrorMessage } from '@/lib/apiError'
import { MenuKeys } from '@/lib/menuKeys'
import type { Department, Paged } from '@/types/master'
type FormState = { id?: string; code: string; name: string; note: string }
const emptyForm: FormState = { code: '', name: '', note: '' }
type FormState = { id?: string; code: string; name: string; parentId: string; note: string }
const emptyForm: FormState = { code: '', name: '', parentId: '', note: '' }
export function DepartmentsPage() {
const qc = useQueryClient()
@ -38,9 +39,24 @@ export function DepartmentsPage() {
},
})
// Toàn bộ phòng ban (không phân trang) để chọn "Phòng cha" + tra tên phòng cha cho cột bảng.
const allDepts = useQuery({
queryKey: ['departments-all'],
queryFn: async () =>
(await api.get<Paged<Department>>('/departments', { params: { page: 1, pageSize: 200 } })).data.items,
})
const deptNameById = new Map((allDepts.data ?? []).map(d => [d.id, `${d.code}${d.name}`]))
const mutate = useMutation({
mutationFn: async (d: FormState) => {
const payload = { id: d.id, code: d.code, name: d.name, managerUserId: null, note: d.note || null }
const payload = {
id: d.id,
code: d.code,
name: d.name,
parentId: d.parentId || null,
managerUserId: null,
note: d.note || null,
}
if (d.id) await api.put(`/departments/${d.id}`, payload)
else await api.post('/departments', payload)
},
@ -65,6 +81,7 @@ export function DepartmentsPage() {
const columns: Column<Department>[] = [
{ key: 'code', header: 'Mã', sortable: true, render: d => <span className="font-mono text-xs">{d.code}</span>, width: 'w-32' },
{ key: 'name', header: 'Tên phòng ban', sortable: true, render: d => d.name },
{ key: 'parentId', header: 'Thuộc', render: d => (d.parentId ? deptNameById.get(d.parentId) ?? '—' : '—') },
{ key: 'note', header: 'Ghi chú', render: d => d.note ?? '—' },
{
key: 'actions',
@ -75,7 +92,7 @@ export function DepartmentsPage() {
<div className="flex justify-end gap-1">
<PermissionGuard menuKey={MenuKeys.Departments} action="Update">
<Button size="sm" variant="ghost" onClick={() => {
setForm({ id: d.id, code: d.code, name: d.name, note: d.note ?? '' })
setForm({ id: d.id, code: d.code, name: d.name, parentId: d.parentId ?? '', note: d.note ?? '' })
setOpen(true)
}}>
<Pencil className="h-3.5 w-3.5" />
@ -149,6 +166,17 @@ export function DepartmentsPage() {
<Label>Tên phòng ban *</Label>
<Input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} required />
</div>
<div className="space-y-1.5">
<Label>Phòng cha (Thuộc khối/phòng)</Label>
<Select value={form.parentId} onChange={e => setForm({ ...form, parentId: e.target.value })}>
<option value=""> Không (cấp gốc) </option>
{(allDepts.data ?? [])
.filter(d => d.id !== form.id)
.map(d => (
<option key={d.id} value={d.id}>{d.code} {d.name}</option>
))}
</Select>
</div>
<div className="space-y-1.5">
<Label>Ghi chú</Label>
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />

View File

@ -66,6 +66,7 @@ export type Department = {
id: string
code: string
name: string
parentId: string | null
managerUserId: string | null
note: string | null
createdAt: string