diff --git a/apps/backend/src/repositories/worker-platform-repo.ts b/apps/backend/src/repositories/worker-platform-repo.ts index bfb8d28f..e727ff53 100644 --- a/apps/backend/src/repositories/worker-platform-repo.ts +++ b/apps/backend/src/repositories/worker-platform-repo.ts @@ -166,6 +166,7 @@ type ListInput = { status?: string keyword?: string workerId?: number + categoryId?: number } type ProductRuleListInput = { @@ -912,6 +913,14 @@ export async function getWorkCategoryByKey(categoryKey: string): Promise { + const result = await query( + 'SELECT * FROM work_categories WHERE id = $1 LIMIT 1', + [Number(categoryId)], + ) + return result.rows[0] || null +} + export async function listWorkCategories(): Promise { const result = await query( `SELECT * FROM work_categories WHERE status = 'active' ORDER BY sort_order ASC, id ASC`, @@ -946,6 +955,36 @@ export async function upsertWorkCategory(input: { return result.rows[0] || null } +export async function countWorkCategoryUsages(categoryId: number | string): Promise<{ + workOrderCount: number + productRuleCount: number +}> { + const [orders, rules] = await Promise.all([ + query<{ total: number }>( + 'SELECT COUNT(*)::int AS total FROM work_orders WHERE category_id = $1', + [Number(categoryId)], + ), + query<{ total: number }>( + 'SELECT COUNT(*)::int AS total FROM work_product_rules WHERE category_id = $1', + [Number(categoryId)], + ), + ]) + return { + workOrderCount: Number(orders.rows[0]?.total || 0), + productRuleCount: Number(rules.rows[0]?.total || 0), + } +} + +export async function deleteWorkCategory( + categoryId: number | string, +): Promise<{ deleted: boolean }> { + const result = await query<{ id: number }>( + 'DELETE FROM work_categories WHERE id = $1 RETURNING id', + [Number(categoryId)], + ) + return { deleted: Boolean(result.rows[0]) } +} + export async function listWorkProductRules({ enabled = null, keyword = '', @@ -1123,8 +1162,14 @@ export async function listWorkOrders({ status = '', keyword = '', workerId = 0, + categoryId = 0, }: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> { - const { whereClause, params } = buildWorkOrderWhere({ status, keyword, workerId }) + const { whereClause, params } = buildWorkOrderWhere({ + status, + keyword, + workerId, + categoryId, + }) const totalResult = await query<{ total: number }>( ` SELECT COUNT(*)::int AS total @@ -1713,7 +1758,12 @@ function buildWorkerUserWhere({ } } -function buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListInput) { +function buildWorkOrderWhere({ + status = '', + keyword = '', + workerId = 0, + categoryId = 0, +}: ListInput) { const filters: string[] = [] const params: unknown[] = [] if (status) { @@ -1730,6 +1780,10 @@ function buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListIn params.push(workerId) filters.push(`wo.assigned_worker_id = $${params.length}`) } + if (categoryId) { + params.push(categoryId) + filters.push(`wo.category_id = $${params.length}`) + } return { whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', params, diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index bc400d89..1216d9f4 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -4,6 +4,7 @@ import { acceptAdminWorkOrder, creditAdminWorkerWallet, createAdminMockWorkOrder, + deleteAdminWorkCategory, getAdminWorkerFinanceConfig, getAdminWorkerPlatformSummary, listAdminWorkerFinanceRequests, @@ -92,6 +93,22 @@ router.post( }), ) +router.delete( + '/worker-platform/categories/:categoryId', + requireAdminRoles(['admin', 'operator']), + createJsonHandler((req) => deleteAdminWorkCategory(String(req.params.categoryId || '')), { + successMessage: '接单分类已删除', + errorMessage: '删除接单分类失败', + scope: '[admin/worker-platform/categories/:categoryId]', + audit: (_req, data) => ({ + action: 'work_category_deleted', + targetType: 'work_category', + targetId: String(_req.params.categoryId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }), +) + router.get( '/worker-platform/product-rules', requireAdminRoles(['admin', 'operator', 'support']), diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.ts b/apps/backend/src/services/worker-platform/worker-platform-service.ts index 51d73218..d159deb4 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.ts @@ -9,12 +9,15 @@ import { addWorkerWalletCredit, countWorkerAcceptedOrders, countWorkerActiveOrders, + countWorkCategoryUsages, createWorkOrder, createWorkOrderEvent, createWorkerFinanceRequest, createWorkerUser, + deleteWorkCategory, getWorkerFinanceRequestById, getWorkerFinanceRequestSummary, + getWorkCategoryById, getWorkerLevelById, getWorkerLevelByKey, getWorkerUserByInviteCode, @@ -28,6 +31,7 @@ import { listPendingMaterialWorkOrdersByPlatformOrderId, listWorkerWalletLedgers, grabWorkOrder, + listWorkCategories, listWorkOrders, listWorkProductRules, listWorkerLevels, @@ -627,17 +631,21 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work requireActiveWorkerSession(session) const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) + const categoryId = normalizeOptionalId(query.categoryId) const { items, total } = await listWorkOrders({ page, pageSize, status: WORK_ORDER_STATUS.OPEN, keyword: String(query.keyword || '').trim(), + categoryId: categoryId || 0, }) const worker = await getRequiredWorker(session.workerId) const permissions = resolveWorkerPermissions(worker) + const categories = (await listWorkCategories()).map(mapWorkCategory) return { items: items.map((item) => mapWorkOrderForWorker(item, permissions)), pagination: { page, pageSize, total }, + categories, } } @@ -880,6 +888,28 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) { return { category: mapWorkCategory(category) } } +export async function deleteAdminWorkCategory(categoryId: number | string) { + const category = await getWorkCategoryById(Number(categoryId)) + if (!category) { + throw createHttpError('分类不存在', { + statusCode: 404, + errorCode: 'work_category_not_found', + }) + } + const usages = await countWorkCategoryUsages(Number(categoryId)) + if (usages.workOrderCount > 0 || usages.productRuleCount > 0) { + throw createHttpError( + `该分类已被 ${usages.workOrderCount} 个工单和 ${usages.productRuleCount} 条物品规则使用,无法删除,请先停用`, + { + statusCode: 409, + errorCode: 'work_category_in_use', + }, + ) + } + const { deleted } = await deleteWorkCategory(Number(categoryId)) + return { deleted } +} + export async function saveAdminWorkerLevel(payload: JsonObject = {}) { const now = nowIso() const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY diff --git a/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx b/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx index 3e7320cd..ef626f77 100644 --- a/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx +++ b/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx @@ -39,6 +39,7 @@ import { acceptAdminWorkOrder, createAdminMockWorkOrder, creditAdminWorkerWallet, + deleteAdminWorkCategory, fetchAdminWorkerFinanceConfig, fetchAdminWorkerFinanceRequests, fetchAdminWorkCategories, @@ -1034,11 +1035,34 @@ function CategoriesPanel() { const { message } = App.useApp() const queryClient = useQueryClient() const [form] = Form.useForm() + const [editingCategory, setEditingCategory] = useState(null) + const [deletingCategory, setDeletingCategory] = useState(null) const categoriesQuery = useQuery({ queryKey: ['admin-worker-platform-categories'], queryFn: () => fetchAdminWorkCategories(), }) + async function refreshCategories() { + await queryClient.invalidateQueries({ + queryKey: ['admin-worker-platform-categories'], + }) + } + + function startEdit(category: WorkCategory) { + setEditingCategory(category) + form.setFieldsValue({ + categoryKey: category.categoryKey, + name: category.name, + sortOrder: category.sortOrder, + status: category.status, + }) + } + + function cancelEdit() { + setEditingCategory(null) + form.resetFields() + } + async function saveCategory(values: { categoryKey: string name: string @@ -1047,16 +1071,42 @@ function CategoriesPanel() { }) { try { await saveAdminWorkCategory(values) - message.success('分类已保存') - form.resetFields() - await queryClient.invalidateQueries({ - queryKey: ['admin-worker-platform-categories'], - }) + message.success(editingCategory ? '分类已更新' : '分类已保存') + cancelEdit() + await refreshCategories() } catch (error) { message.error(error instanceof Error ? error.message : '保存失败') } } + async function toggleCategory(category: WorkCategory) { + try { + await saveAdminWorkCategory({ + categoryKey: category.categoryKey, + name: category.name, + sortOrder: category.sortOrder, + status: category.status === 'active' ? 'disabled' : 'active', + }) + message.success(category.status === 'active' ? '分类已停用' : '分类已启用') + await refreshCategories() + } catch (error) { + message.error(error instanceof Error ? error.message : '操作失败') + } + } + + async function submitDelete() { + if (!deletingCategory) return + try { + await deleteAdminWorkCategory(deletingCategory.categoryId) + message.success('分类已删除') + setDeletingCategory(null) + await refreshCategories() + } catch (error) { + message.error(error instanceof Error ? error.message : '删除失败') + setDeletingCategory(null) + } + } + const columns: TableColumnsType = [ { title: '分类', dataIndex: 'name' }, { title: '标识', dataIndex: 'categoryKey' }, @@ -1070,11 +1120,28 @@ function CategoriesPanel() { ), }, + { + title: '操作', + width: 240, + render: (_, row) => ( + + + + + + ), + }, ] return (
- +
- + + + {editingCategory ? : null} +
@@ -1123,6 +1193,19 @@ function CategoriesPanel() { pagination={false} />
+ setDeletingCategory(null)} + okButtonProps={{ danger: true }} + okText="删除" + cancelText="取消" + > + + 确定删除分类「{deletingCategory?.name}」?已引用该分类的工单和物品规则将无法删除,需先处理。 + +
) } diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index 545cd349..b69ec05a 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -12,6 +12,7 @@ import { Pagination, Space, Spin, + Tabs, Tag, Typography, } from 'antd' @@ -37,14 +38,16 @@ export default function WorkerHallPage() { const queryClient = useQueryClient() const [keywordInput, setKeywordInput] = useState('') const [keyword, setKeyword] = useState('') + const [categoryId, setCategoryId] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [grabbingId, setGrabbingId] = useState(0) const [detailOrder, setDetailOrder] = useState(null) const ordersQuery = useQuery({ - queryKey: ['worker-hall-orders', keyword, page, pageSize], - queryFn: () => fetchWorkerHallOrders({ keyword, page, pageSize }), + queryKey: ['worker-hall-orders', keyword, categoryId, page, pageSize], + queryFn: () => + fetchWorkerHallOrders({ keyword, page, pageSize, categoryId: categoryId || undefined }), retry: false, }) @@ -94,6 +97,7 @@ export default function WorkerHallPage() { const orders = ordersQuery.data?.data.items || [] const pagination = ordersQuery.data?.data.pagination + const categories = ordersQuery.data?.data.categories || [] const worker = profileQuery.data?.data.worker const activeCount = hallSummaryQuery.data?.activeCount || 0 const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0 @@ -210,6 +214,24 @@ export default function WorkerHallPage() { ))} + {categories.length > 0 ? ( + ({ + key: String(category.categoryId), + label: category.name, + })), + ]} + onChange={(nextKey) => { + setCategoryId(Number(nextKey) || 0) + setPage(1) + }} + /> + ) : null} + {shouldWarn ? ( ( + `/api/v1/admin/worker-platform/categories/${categoryId}`, + ) +} + export function saveAdminWorkerLevel(payload: { levelKey: string name: string diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index f034e239..d9603e08 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -4,6 +4,7 @@ import type { UploadedFile, WorkOrder, WorkerFinanceRequest, + WorkerHallOrdersResponse, WorkerListResponse, WorkerLoginResponse, WorkerProfileResponse, @@ -92,7 +93,7 @@ export function changeWorkerPassword(payload: { } export function fetchWorkerHallOrders(params?: Record) { - return apiGet>( + return apiGet( '/api/v1/worker/hall/orders', params, ) diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 1a2361c2..0536a0eb 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -224,6 +224,12 @@ export type WorkerListResponse = { pagination: Pagination } +export type WorkerHallOrdersResponse = { + items: WorkOrder[] + pagination: Pagination + categories: WorkCategory[] +} + export type CollectField = { key: string label: string