[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
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:
@ -8,6 +8,7 @@ import { PermissionGuard } from '@/components/PermissionGuard'
|
|||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Label } from '@/components/ui/Label'
|
import { Label } from '@/components/ui/Label'
|
||||||
|
import { Select } from '@/components/ui/Select'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
import { Dialog } from '@/components/ui/Dialog'
|
import { Dialog } from '@/components/ui/Dialog'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
@ -15,8 +16,8 @@ import { getErrorMessage } from '@/lib/apiError'
|
|||||||
import { MenuKeys } from '@/lib/menuKeys'
|
import { MenuKeys } from '@/lib/menuKeys'
|
||||||
import type { Department, Paged } from '@/types/master'
|
import type { Department, Paged } from '@/types/master'
|
||||||
|
|
||||||
type FormState = { id?: string; code: string; name: string; note: string }
|
type FormState = { id?: string; code: string; name: string; parentId: string; note: string }
|
||||||
const emptyForm: FormState = { code: '', name: '', note: '' }
|
const emptyForm: FormState = { code: '', name: '', parentId: '', note: '' }
|
||||||
|
|
||||||
export function DepartmentsPage() {
|
export function DepartmentsPage() {
|
||||||
const qc = useQueryClient()
|
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({
|
const mutate = useMutation({
|
||||||
mutationFn: async (d: FormState) => {
|
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)
|
if (d.id) await api.put(`/departments/${d.id}`, payload)
|
||||||
else await api.post('/departments', payload)
|
else await api.post('/departments', payload)
|
||||||
},
|
},
|
||||||
@ -65,6 +81,7 @@ export function DepartmentsPage() {
|
|||||||
const columns: Column<Department>[] = [
|
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: '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: '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: 'note', header: 'Ghi chú', render: d => d.note ?? '—' },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
@ -75,7 +92,7 @@ export function DepartmentsPage() {
|
|||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
<PermissionGuard menuKey={MenuKeys.Departments} action="Update">
|
<PermissionGuard menuKey={MenuKeys.Departments} action="Update">
|
||||||
<Button size="sm" variant="ghost" onClick={() => {
|
<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)
|
setOpen(true)
|
||||||
}}>
|
}}>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
@ -149,6 +166,17 @@ export function DepartmentsPage() {
|
|||||||
<Label>Tên phòng ban *</Label>
|
<Label>Tên phòng ban *</Label>
|
||||||
<Input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} required />
|
<Input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} required />
|
||||||
</div>
|
</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ó (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">
|
<div className="space-y-1.5">
|
||||||
<Label>Ghi chú</Label>
|
<Label>Ghi chú</Label>
|
||||||
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
|
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export type Department = {
|
|||||||
id: string
|
id: string
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
|
parentId: string | null
|
||||||
managerUserId: string | null
|
managerUserId: string | null
|
||||||
note: string | null
|
note: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
|||||||
Reference in New Issue
Block a user