[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 { 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ó (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 })} />
|
||||
|
||||
@ -66,6 +66,7 @@ export type Department = {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
parentId: string | null
|
||||
managerUserId: string | null
|
||||
note: string | null
|
||||
createdAt: string
|
||||
|
||||
Reference in New Issue
Block a user