From dbf66489a9dbe5dd2fb6d04cd218bcd7e05d7c69 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Mon, 8 Jun 2026 15:00:30 +0700 Subject: [PATCH] [CLAUDE] Office: P11-D ItTicket admin reassign-UI + P11-E AttendanceReport menu-key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task C - ItTicket admin reassign-UI (fe-admin only): - Per-card reassign dialog on ItTicketsPage kanban (admin override of round-robin auto-assign). - Reuses existing PUT /it-tickets/{id}/assign (Admin-only) + GET /users picker. No new BE endpoint. - fe-admin intentionally diverges from fe-user (read-only) — admin-management action. Task D - AttendanceReport menu-key (P11-E promote, no migration): - MenuKeys.OffAttendanceReport 'Báo cáo chấm công' leaf under Văn phòng số (order 8), Admin-perm auto via All[]. - DbInitializer idempotent seed + fe-admin menuKeys.ts/Layout staticMap -> existing /attendance/report route. Pipeline: implementer-backend -> implementer-frontend -> reviewer (PASS, 0 issues). dotnet+npm builds clean. Tests 203 (unchanged - no new BE logic/schema). Co-Authored-By: Claude Opus 4.8 (1M context) --- fe-admin/src/components/Layout.tsx | 2 + fe-admin/src/lib/menuKeys.ts | 2 + fe-admin/src/pages/office/ItTicketsPage.tsx | 103 +++++++++++++++++- .../SolutionErp.Domain/Identity/MenuKeys.cs | 3 +- .../Persistence/DbInitializer.cs | 1 + 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index f67c530..6c05724 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -83,6 +83,8 @@ function resolvePath(key: string): string | null { Off_DatXe: '/workflow-apps/vehicle', Off_ItTicket: '/it-tickets', Off_ChamCong: '/attendance', + // [P11-E S52] Báo cáo chấm công — admin-only leaf (route từ App.tsx S52). + Off_AttendanceReport: '/attendance/report', Hrm_Dashboard: '/hr/dashboard', } if (staticMap[key]) return staticMap[key] diff --git a/fe-admin/src/lib/menuKeys.ts b/fe-admin/src/lib/menuKeys.ts index 2699842..6441916 100644 --- a/fe-admin/src/lib/menuKeys.ts +++ b/fe-admin/src/lib/menuKeys.ts @@ -64,6 +64,8 @@ export const MenuKeys = { OffDatXe: 'Off_DatXe', OffItTicket: 'Off_ItTicket', OffChamCong: 'Off_ChamCong', + // P11-E (S52) — Báo cáo chấm công (admin-only leaf dưới Văn phòng số) + OffAttendanceReport: 'Off_AttendanceReport', HrmDashboard: 'Hrm_Dashboard', } as const diff --git a/fe-admin/src/pages/office/ItTicketsPage.tsx b/fe-admin/src/pages/office/ItTicketsPage.tsx index 4e0a653..643c050 100644 --- a/fe-admin/src/pages/office/ItTicketsPage.tsx +++ b/fe-admin/src/pages/office/ItTicketsPage.tsx @@ -1,16 +1,27 @@ // Ticket CNTT — Phase 10.3 G-O6 (S38) + P11-D auto-assign round-robin + SLA timer (S52). // Read-only kanban list + MaTicket + người xử lý (auto-assign dept IT) + SLA badge (đỏ khi quá hạn). -// File MIRROR SHA256 identical fe-user counterpart. -import { useQuery } from '@tanstack/react-query' -import { Ticket } from 'lucide-react' +// ⚠️ DIVERGES fe-user (KHÔNG còn SHA256 identical, S52 Task C): fe-admin thêm +// nút "Đổi" + Dialog gán lại người xử lý (PUT /it-tickets/{id}/assign, Admin-only). +// fe-user giữ bản read-only — KHÔNG mirror admin reassign sang đó. +import { useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { Pencil, Ticket } from 'lucide-react' +import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' +import { Button } from '@/components/ui/Button' +import { Dialog } from '@/components/ui/Dialog' +import { Select } from '@/components/ui/Select' import { api } from '@/lib/api' +import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' import { IT_TICKET_CATEGORY_LABELS, IT_TICKET_PRIORITY_BADGE, IT_TICKET_PRIORITY_LABELS, IT_TICKET_STATUS_LABELS, type ItTicketDto, type PagedResult, } from '@/types/workflowApps' +type UserOption = { id: string; fullName: string; email: string } +type Paged = { items: T[] } + function formatSlaDue(iso: string): string { return new Date(iso).toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', @@ -18,11 +29,44 @@ function formatSlaDue(iso: string): string { } export function ItTicketsPage() { + const qc = useQueryClient() const list = useQuery({ queryKey: ['it-tickets'], queryFn: async () => (await api.get>('/it-tickets', { params: { pageSize: 100 } })).data, }) + // Reassign Dialog state. `target` = ticket đang gán lại; `pick` = userId đã chọn. + const [target, setTarget] = useState(null) + const [pick, setPick] = useState('') + + // Admin user list (reuse endpoint sẵn có — KHÔNG thêm BE endpoint). + const users = useQuery({ + queryKey: ['users'], + queryFn: async () => (await api.get>('/users', { params: { page: 1, pageSize: 200 } })).data.items, + enabled: target !== null, // chỉ fetch khi mở dialog + }) + + const reassign = useMutation({ + // 204 NoContent — không đọc JSON body. + mutationFn: (input: { id: string; assignedToUserId: string }) => + api.put(`/it-tickets/${input.id}/assign`, { assignedToUserId: input.assignedToUserId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['it-tickets'] }) + toast.success('Đã gán lại người xử lý') + closeDialog() + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + function openDialog(t: ItTicketDto) { + setTarget(t) + setPick(t.assignedToUserId ?? '') // preselect người xử lý hiện tại + } + function closeDialog() { + setTarget(null) + setPick('') + } + const items = list.data?.items ?? [] // Group by status for kanban-ish display @@ -59,8 +103,20 @@ export function ItTicketsPage() { {IT_TICKET_CATEGORY_LABELS[t.category]} · {t.requesterFullName}
- - 👤 {t.assignedToFullName ?? Chưa giao} + + + 👤 {t.assignedToFullName ?? Chưa giao} + + {/* Admin-only reassign (BE PUT /assign gác [Authorize(Admin)]). + Nút nhỏ cạnh người xử lý → mở Dialog gán lại. */} + {t.slaDueAt && ( )} + + {/* Reassign Dialog — Admin only (BE gác quyền). Select danh sách user. */} + + + + + } + > + {target && ( +
+
+ Ticket {target.maTicket ?? '—'} · {target.title} +
+
+ + + {users.isLoading &&
Đang tải danh sách…
} +
+
+ )} +
) } diff --git a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs index 6835b4c..429b834 100644 --- a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs +++ b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs @@ -122,6 +122,7 @@ public static class MenuKeys public const string OffDatXe = "Off_DatXe"; // Đặt xe công public const string OffItTicket = "Off_ItTicket"; // Ticket CNTT helpdesk public const string OffChamCong = "Off_ChamCong"; // Chấm công GPS (G-P1) + public const string OffAttendanceReport = "Off_AttendanceReport"; // Báo cáo chấm công (P11-E, admin) public const string HrmDashboard = "Hrm_Dashboard"; // Dashboard HRM (G-H3) public static readonly string[] PurchaseEvaluationTypeCodes = @@ -155,7 +156,7 @@ public static class MenuKeys OffPhongHop, OffPhongHopView, OffPhongHopManage, OffPhongHopBook, // Phase 10.2 G-O2 — Phòng họp OffDeXuat, OffDeXuatList, OffDeXuatCreate, OffDeXuatInbox, // Phase 10.3 G-O3 — Đề xuất OffDonTu, OffDonTuLeave, OffDonTuOt, OffDonTuTravel, // Phase 10.3 G-O4 — Đơn từ - OffDatXe, OffItTicket, OffChamCong, HrmDashboard, // Phase 10.3-10.4 — G-O5/G-O6/G-P1/G-H3 + OffDatXe, OffItTicket, OffChamCong, OffAttendanceReport, HrmDashboard, // Phase 10.3-10.4 — G-O5/G-O6/G-P1/G-H3 + P11-E report System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows, ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22 ]; diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 4facd6b..245dde7 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -1785,6 +1785,7 @@ public static class DbInitializer (MenuKeys.OffDatXe, "Đặt xe công", MenuKeys.Off, 5, "Car"), (MenuKeys.OffItTicket, "Ticket CNTT", MenuKeys.Off, 6, "Ticket"), (MenuKeys.OffChamCong, "Chấm công", MenuKeys.Off, 7, "Fingerprint"), + (MenuKeys.OffAttendanceReport, "Báo cáo chấm công", MenuKeys.Off, 8, "FileBarChart"), // Phase 10.4 G-H3 — Dashboard NS dưới root Hrm. (MenuKeys.HrmDashboard, "Dashboard NS", MenuKeys.Hrm, 3, "BarChart3"), };