Add aftersales work order frontend pages

- Public scan-to-confirm page (/aftersales/:sn) with phone last-4 verification
- Admin list + detail pages with state machine, QR generation, reassign, force-close
- PublicLayout extracted from PublicQuery so both pages share logo + 备案 chrome
- PublicQuery auto-redirects scanned zjbf-sh-* serials to the aftersales page
- AdminLayout: new 售后工单 menu entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frudrax Cheng
2026-05-26 10:51:25 +08:00
parent 11f3eda668
commit 6fef517556
12 changed files with 1505 additions and 51 deletions
+83
View File
@@ -117,4 +117,87 @@ export interface EmployeeSerialPagination {
export interface EmployeeSerialResponse {
data: EmployeeSerial[];
pagination: EmployeeSerialPagination;
}
export type AftersalesServiceType = 'software' | 'hardware' | 'other';
export type AftersalesWorkOrderStatus = 'created' | 'pending_confirmation' | 'closed' | 'rejected';
export type AftersalesAuthorizationStatus = 'pending' | 'authorized' | 'unauthorized';
export interface AftersalesOrder {
id: number;
serialNumber: string;
companyName: string;
companyAddress: string;
contactName: string;
contactPhone: string;
serviceType: AftersalesServiceType;
issueDescription: string;
resolutionNote: string;
workOrderStatus: AftersalesWorkOrderStatus;
authorizationStatus: AftersalesAuthorizationStatus;
technicianId?: number;
createdBy?: number;
scannedAt?: string;
confirmedAt?: string;
rejectCount: number;
createdAt: string;
updatedAt: string;
technician?: User;
creator?: User;
}
export interface AftersalesPublicView {
serialNumber: string;
companyName: string;
companyAddress: string;
contactName: string;
serviceType: AftersalesServiceType;
issueDescription: string;
resolutionNote: string;
workOrderStatus: AftersalesWorkOrderStatus;
authorizationStatus: AftersalesAuthorizationStatus;
technicianName: string;
createdAt: string;
confirmedAt?: string;
}
export interface CreateAftersalesRequest {
companyName: string;
companyAddress: string;
contactName: string;
contactPhone: string;
serviceType: AftersalesServiceType;
issueDescription: string;
technicianId?: number;
}
export interface UpdateAftersalesRequest {
companyAddress?: string;
contactName?: string;
contactPhone?: string;
serviceType?: AftersalesServiceType;
issueDescription?: string;
resolutionNote?: string;
technicianId?: number;
}
export interface AftersalesListFilter {
page?: number;
limit?: number;
search?: string;
workOrderStatus?: AftersalesWorkOrderStatus;
serviceType?: AftersalesServiceType;
technicianId?: number;
mine?: boolean;
}
export interface AftersalesListResponse {
data: AftersalesOrder[];
pagination: EmployeeSerialPagination;
}
export interface CustomerConfirmRequest {
phoneLast4: string;
action: 'authorize' | 'reject';
rejectReason?: string;
}