From 2ab80c4df8c83f72c4c4e2e821aeaae6c2dca6b4 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 28 Jul 2026 10:41:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E8=B5=84=E9=87=91=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../006_worker_profile_finance_requests.sql | 34 + .../src/repositories/worker-platform-repo.ts | 268 ++++- apps/backend/src/routes/worker.ts | 52 + .../worker-platform-service.ts | 319 ++++++ .../src/pages/worker/WorkerProfilePage.tsx | 986 +++++++++++++++++- apps/frontend/src/services/worker.ts | 52 +- apps/frontend/src/styles/main.css | 228 +++- apps/frontend/src/types/worker-platform.ts | 44 + 8 files changed, 1960 insertions(+), 23 deletions(-) create mode 100644 apps/backend/src/db/migrations/006_worker_profile_finance_requests.sql diff --git a/apps/backend/src/db/migrations/006_worker_profile_finance_requests.sql b/apps/backend/src/db/migrations/006_worker_profile_finance_requests.sql new file mode 100644 index 00000000..7775cabb --- /dev/null +++ b/apps/backend/src/db/migrations/006_worker_profile_finance_requests.sql @@ -0,0 +1,34 @@ +-- 006_worker_profile_finance_requests.sql —— 接单端个人中心资金申请能力。 +-- +-- 说明: +-- 1. 充值 / 提现先走申请单,由后台后续审核处理; +-- 2. 不直接改动现有钱包结算逻辑,避免影响抢单、验收、押金释放链路。 + +CREATE TABLE IF NOT EXISTS worker_finance_requests ( + id BIGSERIAL PRIMARY KEY, + worker_id BIGINT NOT NULL REFERENCES worker_users(id) ON DELETE CASCADE, + request_type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + amount INTEGER NOT NULL DEFAULT 0, + account_channel TEXT NOT NULL DEFAULT '', + account_name TEXT NOT NULL DEFAULT '', + account_no TEXT NOT NULL DEFAULT '', + note TEXT NOT NULL DEFAULT '', + reviewed_note TEXT NOT NULL DEFAULT '', + payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + reviewed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_worker_finance_requests_worker_created + ON worker_finance_requests(worker_id, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_worker_finance_requests_status_type + ON worker_finance_requests(status, request_type, id DESC); + +COMMENT ON TABLE worker_finance_requests IS '打手充值 / 提现申请记录'; +COMMENT ON COLUMN worker_finance_requests.request_type IS '申请类型:recharge / withdraw'; +COMMENT ON COLUMN worker_finance_requests.status IS '申请状态:pending / approved / rejected / cancelled'; +COMMENT ON COLUMN worker_finance_requests.amount IS '申请金额,单位分'; +COMMENT ON COLUMN worker_finance_requests.payload_json IS '扩展字段,便于后续接支付凭证、审核信息'; diff --git a/apps/backend/src/repositories/worker-platform-repo.ts b/apps/backend/src/repositories/worker-platform-repo.ts index 5bddf475..56a79486 100644 --- a/apps/backend/src/repositories/worker-platform-repo.ts +++ b/apps/backend/src/repositories/worker-platform-repo.ts @@ -31,6 +31,8 @@ export type WorkerUserRow = { level_permission_json?: string | Record available_amount?: number frozen_deposit_amount?: number + total_credited_amount?: number + total_settled_amount?: number } export type WorkerWalletRow = { @@ -43,6 +45,37 @@ export type WorkerWalletRow = { updated_at: string } +export type WorkerWalletLedgerRow = { + id: number + worker_id: number + ledger_type: string + amount: number + balance_after: number + frozen_after: number + related_work_order_id: number | null + audit_status: string + note: string + payload_json: string | Record + created_at: string +} + +export type WorkerFinanceRequestRow = { + id: number + worker_id: number + request_type: string + status: string + amount: number + account_channel: string + account_name: string + account_no: string + note: string + reviewed_note: string + payload_json: string | Record + created_at: string + updated_at: string + reviewed_at: string | null +} + export type WorkCategoryRow = { id: number category_key: string @@ -133,6 +166,21 @@ type ProductRuleListInput = { keyword?: string } +type WalletLedgerListInput = { + page?: number + pageSize?: number + workerId?: number + ledgerType?: string +} + +type FinanceRequestListInput = { + page?: number + pageSize?: number + workerId?: number + status?: string + requestType?: string +} + type CreateWorkerInput = { username: string passwordHash: string @@ -167,7 +215,9 @@ const WORKER_USER_SELECT = ` wl.name AS level_name, wl.permission_json AS level_permission_json, ww.available_amount, - ww.frozen_deposit_amount + ww.frozen_deposit_amount, + ww.total_credited_amount, + ww.total_settled_amount FROM worker_users wu LEFT JOIN worker_levels wl ON wl.id = wu.level_id LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id @@ -333,6 +383,30 @@ export async function updateWorkerUser( return getWorkerUserById(workerId) } +export async function updateWorkerPassword(input: { + workerId: number + passwordHash: string + sessionVersion: number + now: string +}): Promise { + const result = await query<{ id: number }>( + ` + UPDATE worker_users + SET + password_hash = $1, + session_version = $2, + updated_at = $3 + WHERE id = $4 + RETURNING id + `, + [input.passwordHash, input.sessionVersion, input.now, input.workerId], + ) + if (!result.rows[0]) { + return null + } + return getWorkerUserById(input.workerId) +} + export async function incrementWorkerSessionVersion( workerId: number | string, now: string, @@ -394,6 +468,38 @@ export async function getWorkerWallet(workerId: number | string): Promise { + const { whereClause, params } = buildWorkerWalletLedgerWhere({ + workerId, + ledgerType, + }) + const totalResult = await query<{ total: number }>( + `SELECT COUNT(*)::int AS total FROM worker_wallet_ledgers wwl ${whereClause}`, + params, + ) + const offset = (page - 1) * pageSize + params.push(pageSize, offset) + const itemsResult = await query( + ` + SELECT * + FROM worker_wallet_ledgers wwl + ${whereClause} + ORDER BY wwl.created_at DESC, wwl.id DESC + LIMIT $${params.length - 1} OFFSET $${params.length} + `, + params, + ) + return { + items: itemsResult.rows, + total: Number(totalResult.rows[0]?.total || 0), + } +} + export async function addWorkerWalletCredit(input: { workerId: number amount: number @@ -438,6 +544,121 @@ export async function addWorkerWalletCredit(input: { }) } +export async function createWorkerFinanceRequest(input: { + workerId: number + requestType: string + amount: number + accountChannel: string + accountName: string + accountNo: string + note: string + payloadJson: string + now: string +}): Promise { + const result = await query( + ` + INSERT INTO worker_finance_requests ( + worker_id, request_type, status, amount, + account_channel, account_name, account_no, + note, reviewed_note, payload_json, created_at, updated_at + ) VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, '', $8::jsonb, $9, $10) + RETURNING * + `, + [ + input.workerId, + input.requestType, + input.amount, + input.accountChannel, + input.accountName, + input.accountNo, + input.note, + input.payloadJson, + input.now, + input.now, + ], + ) + return result.rows[0] || null +} + +export async function listWorkerFinanceRequests({ + page = 1, + pageSize = 20, + workerId = 0, + status = '', + requestType = '', +}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> { + const { whereClause, params } = buildWorkerFinanceRequestWhere({ + workerId, + status, + requestType, + }) + const totalResult = await query<{ total: number }>( + `SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr ${whereClause}`, + params, + ) + const offset = (page - 1) * pageSize + params.push(pageSize, offset) + const itemsResult = await query( + ` + SELECT * + FROM worker_finance_requests wfr + ${whereClause} + ORDER BY wfr.created_at DESC, wfr.id DESC + LIMIT $${params.length - 1} OFFSET $${params.length} + `, + params, + ) + return { + items: itemsResult.rows, + total: Number(totalResult.rows[0]?.total || 0), + } +} + +export async function getWorkerFinanceRequestSummary(workerId: number | string) { + const result = await query<{ + pending_withdraw_amount: number + approved_withdraw_amount: number + pending_recharge_amount: number + }>( + ` + SELECT + COALESCE( + SUM(CASE WHEN request_type = 'withdraw' AND status = 'pending' THEN amount ELSE 0 END), + 0 + )::int AS pending_withdraw_amount, + COALESCE( + SUM(CASE WHEN request_type = 'withdraw' AND status = 'approved' THEN amount ELSE 0 END), + 0 + )::int AS approved_withdraw_amount, + COALESCE( + SUM(CASE WHEN request_type = 'recharge' AND status = 'pending' THEN amount ELSE 0 END), + 0 + )::int AS pending_recharge_amount + FROM worker_finance_requests + WHERE worker_id = $1 + `, + [Number(workerId)], + ) + return { + pendingWithdrawAmount: Number(result.rows[0]?.pending_withdraw_amount || 0), + approvedWithdrawAmount: Number(result.rows[0]?.approved_withdraw_amount || 0), + pendingRechargeAmount: Number(result.rows[0]?.pending_recharge_amount || 0), + } +} + +export async function countWorkerAcceptedOrders(workerId: number | string): Promise { + const result = await query<{ total: number }>( + ` + SELECT COUNT(*)::int AS total + FROM work_orders + WHERE assigned_worker_id = $1 + AND status = 'accepted' + `, + [Number(workerId)], + ) + return Number(result.rows[0]?.total || 0) +} + export async function getWorkCategoryByKey(categoryKey: string): Promise { const result = await query( 'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1', @@ -1255,6 +1476,51 @@ function buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListIn } } +function buildWorkerWalletLedgerWhere({ + workerId = 0, + ledgerType = '', +}: WalletLedgerListInput) { + const filters: string[] = [] + const params: unknown[] = [] + if (workerId) { + params.push(workerId) + filters.push(`wwl.worker_id = $${params.length}`) + } + if (ledgerType) { + params.push(ledgerType) + filters.push(`wwl.ledger_type = $${params.length}`) + } + return { + whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', + params, + } +} + +function buildWorkerFinanceRequestWhere({ + workerId = 0, + status = '', + requestType = '', +}: FinanceRequestListInput) { + const filters: string[] = [] + const params: unknown[] = [] + if (workerId) { + params.push(workerId) + filters.push(`wfr.worker_id = $${params.length}`) + } + if (status) { + params.push(status) + filters.push(`wfr.status = $${params.length}`) + } + if (requestType) { + params.push(requestType) + filters.push(`wfr.request_type = $${params.length}`) + } + return { + whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', + params, + } +} + function buildWorkProductRuleWhere({ enabled = null, keyword = '' }: ProductRuleListInput) { const filters: string[] = [] const params: unknown[] = [] diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index 8b663ae5..c3209dd4 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -4,9 +4,14 @@ import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../middlewa import { uploadSingleFile } from './file-upload.js' import { uploadFileAsset } from '../services/file-storage/file-storage-service.js' import { + changeWorkerPassword, + createWorkerRechargeRequest, + createWorkerWithdrawRequest, getWorkerProfile, getWorkerSessionSummary, grabWorkerHallOrder, + listWorkerProfileFinanceRequests, + listWorkerProfileWalletLedgers, listWorkerHallOrders, listWorkerMyOrders, loginWorker, @@ -84,6 +89,53 @@ router.get( }), ) +router.get( + '/profile/wallet-ledgers', + createRouteHandler((req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)), { + successMessage: 'ok', + errorMessage: '读取钱包流水失败', + scope: '[worker/profile/wallet-ledgers]', + }), +) + +router.get( + '/profile/finance-requests', + createRouteHandler((req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)), { + successMessage: 'ok', + errorMessage: '读取资金申请记录失败', + scope: '[worker/profile/finance-requests]', + }), +) + +router.post( + '/profile/recharge-requests', + requireActiveWorker, + createRouteHandler((req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)), { + successMessage: '充值申请已提交', + errorMessage: '提交充值申请失败', + scope: '[worker/profile/recharge-requests]', + }), +) + +router.post( + '/profile/withdraw-requests', + requireActiveWorker, + createRouteHandler((req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)), { + successMessage: '提现申请已提交', + errorMessage: '提交提现申请失败', + scope: '[worker/profile/withdraw-requests]', + }), +) + +router.post( + '/profile/change-password', + createRouteHandler((req) => changeWorkerPassword(req.body || {}, getRequiredWorkerSession(req)), { + successMessage: '密码已修改', + errorMessage: '修改密码失败', + scope: '[worker/profile/change-password]', + }), +) + router.post( '/files/upload', requireActiveWorker, 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 4cbb5197..3adb4f45 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.ts @@ -7,17 +7,22 @@ import { findLatestOrderByAnyPlatformOrderId, getOrderById } from '../../reposit import { acceptWorkOrderAndSettle, addWorkerWalletCredit, + countWorkerAcceptedOrders, countWorkerActiveOrders, createWorkOrder, createWorkOrderEvent, + createWorkerFinanceRequest, createWorkerUser, + getWorkerFinanceRequestSummary, incrementWorkerSessionVersion, getWorkOrderById, getWorkOrderByOrderItemId, getWorkerUserById, getWorkerUserByUsername, listAllWorkCategories, + listWorkerFinanceRequests, listPendingMaterialWorkOrdersByPlatformOrderId, + listWorkerWalletLedgers, grabWorkOrder, listWorkOrders, listWorkProductRules, @@ -25,14 +30,17 @@ import { listWorkerUsers, resolveProblemWorkOrder, updateWorkOrder, + updateWorkerPassword, updateWorkerUser, upsertWorkCategory, upsertWorkProductRule, upsertWorkerLevel, type WorkCategoryRow, + type WorkerFinanceRequestRow, type WorkOrderRow, type WorkProductRuleRow, type WorkerLevelRow, + type WorkerWalletLedgerRow, type WorkerUserRow, type GrabWorkOrderFailureReason, type ProblemWorkOrderResolutionAction, @@ -278,9 +286,226 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef export async function getWorkerProfile(session: WorkerSession) { const worker = await getRequiredWorker(session.workerId) + const [financeSummary, acceptedOrderCount] = await Promise.all([ + getWorkerFinanceRequestSummary(session.workerId), + countWorkerAcceptedOrders(session.workerId), + ]) return { worker: mapWorkerUser(worker), permissions: resolveWorkerPermissions(worker), + summary: { + acceptedOrderCount, + pendingWithdrawAmount: financeSummary.pendingWithdrawAmount, + approvedWithdrawAmount: financeSummary.approvedWithdrawAmount, + pendingRechargeAmount: financeSummary.pendingRechargeAmount, + }, + } +} + +export async function listWorkerProfileWalletLedgers( + query: JsonObject = {}, + session: WorkerSession, +) { + const page = normalizePage(query.page) + const pageSize = normalizePageSize(query.pageSize) + const ledgerType = normalizeWalletLedgerType(query.ledgerType) + const { items, total } = await listWorkerWalletLedgers({ + page, + pageSize, + workerId: session.workerId, + ledgerType, + }) + return { + items: items.map(mapWalletLedger), + pagination: { page, pageSize, total }, + } +} + +export async function listWorkerProfileFinanceRequests( + query: JsonObject = {}, + session: WorkerSession, +) { + const page = normalizePage(query.page) + const pageSize = normalizePageSize(query.pageSize) + const status = normalizeFinanceRequestStatus(query.status) + const requestType = normalizeFinanceRequestType(query.requestType) + const { items, total } = await listWorkerFinanceRequests({ + page, + pageSize, + workerId: session.workerId, + status, + requestType, + }) + return { + items: items.map(mapFinanceRequest), + pagination: { page, pageSize, total }, + } +} + +export async function createWorkerRechargeRequest( + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const worker = await getRequiredWorker(session.workerId) + const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0) + if (amount <= 0) { + throw createHttpError('充值金额必须大于 0', { + statusCode: 400, + errorCode: 'worker_recharge_amount_invalid', + }) + } + + const note = String(payload.note || payload.remark || '').trim() + const created = await createWorkerFinanceRequest({ + workerId: worker.id, + requestType: 'recharge', + amount, + accountChannel: 'manual', + accountName: worker.display_name || worker.username, + accountNo: worker.phone || '', + note: note || '个人中心充值申请', + payloadJson: JSON.stringify({ + source: 'worker_profile_recharge', + username: worker.username, + phone: worker.phone || '', + }), + now: nowIso(), + }) + if (!created) { + throw createHttpError('充值申请创建失败', { + statusCode: 500, + errorCode: 'worker_recharge_request_create_failed', + }) + } + return { + request: mapFinanceRequest(created), + } +} + +export async function createWorkerWithdrawRequest( + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const worker = await getRequiredWorker(session.workerId) + const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0) + if (amount <= 0) { + throw createHttpError('提现金额必须大于 0', { + statusCode: 400, + errorCode: 'worker_withdraw_amount_invalid', + }) + } + + const accountChannel = normalizeWithdrawChannel( + payload.accountChannel || payload.channel, + ) + const accountName = String(payload.accountName || payload.realName || '').trim() + const accountNo = String(payload.accountNo || payload.account || '').trim() + if (!accountName) { + throw createHttpError('请填写收款人姓名', { + statusCode: 400, + errorCode: 'worker_withdraw_account_name_required', + }) + } + if (!accountNo) { + throw createHttpError('请填写收款账号', { + statusCode: 400, + errorCode: 'worker_withdraw_account_no_required', + }) + } + + const financeSummary = await getWorkerFinanceRequestSummary(worker.id) + const availableAmount = Number(worker.available_amount || 0) + const availableForWithdraw = Math.max( + 0, + availableAmount - financeSummary.pendingWithdrawAmount, + ) + if (amount > availableForWithdraw) { + throw createHttpError('可提现余额不足,请先减少提现金额或等待已提交申请处理', { + statusCode: 409, + errorCode: 'worker_withdraw_amount_insufficient', + }) + } + + const note = String(payload.note || payload.remark || '').trim() + const created = await createWorkerFinanceRequest({ + workerId: worker.id, + requestType: 'withdraw', + amount, + accountChannel, + accountName, + accountNo, + note: note || '个人中心提现申请', + payloadJson: JSON.stringify({ + source: 'worker_profile_withdraw', + username: worker.username, + phone: worker.phone || '', + }), + now: nowIso(), + }) + if (!created) { + throw createHttpError('提现申请创建失败', { + statusCode: 500, + errorCode: 'worker_withdraw_request_create_failed', + }) + } + return { + request: mapFinanceRequest(created), + availableForWithdraw, + } +} + +export async function changeWorkerPassword( + payload: JsonObject = {}, + session: WorkerSession, +) { + const worker = await getRequiredWorker(session.workerId) + const currentPassword = normalizePassword( + payload.currentPassword || payload.oldPassword, + ) + const nextPassword = normalizePassword(payload.newPassword || payload.password) + + if (!currentPassword) { + throw createHttpError('请输入当前密码', { + statusCode: 400, + errorCode: 'worker_password_current_required', + }) + } + + if (!verifyWorkerPassword(currentPassword, worker.password_hash)) { + throw createHttpError('当前密码不正确', { + statusCode: 400, + errorCode: 'worker_password_current_invalid', + }) + } + + if (!nextPassword) { + throw createHttpError('请输入新密码', { + statusCode: 400, + errorCode: 'worker_password_new_required', + }) + } + + validateWorkerPassword(nextPassword) + + if (currentPassword === nextPassword) { + throw createHttpError('新密码不能与当前密码一致', { + statusCode: 400, + errorCode: 'worker_password_unchanged', + }) + } + + const updated = await updateWorkerPassword({ + workerId: worker.id, + passwordHash: hashWorkerPassword(nextPassword), + sessionVersion: normalizeSessionVersion(worker.session_version) + 1, + now: nowIso(), + }) + + return { + worker: mapWorkerUser(updated || worker), + reloginRequired: true, } } @@ -1291,6 +1516,8 @@ function mapWorkerUser(worker: WorkerUserRow) { wallet: { availableAmount: Number(worker.available_amount || 0), frozenDepositAmount: Number(worker.frozen_deposit_amount || 0), + totalCreditedAmount: Number(worker.total_credited_amount || 0), + totalSettledAmount: Number(worker.total_settled_amount || 0), }, createdAt: worker.created_at, updatedAt: worker.updated_at, @@ -1298,6 +1525,43 @@ function mapWorkerUser(worker: WorkerUserRow) { } } +function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) { + if (!ledger) return null + return { + ledgerId: Number(ledger.id), + ledgerType: ledger.ledger_type, + amount: Number(ledger.amount || 0), + balanceAfter: Number(ledger.balance_after || 0), + frozenAfter: Number(ledger.frozen_after || 0), + relatedWorkOrderId: ledger.related_work_order_id + ? Number(ledger.related_work_order_id) + : null, + auditStatus: ledger.audit_status || '', + note: ledger.note || '', + payload: safeParseJson(ledger.payload_json), + createdAt: ledger.created_at, + } +} + +function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined) { + if (!request) return null + return { + requestId: Number(request.id), + requestType: request.request_type || '', + status: request.status || '', + amount: Number(request.amount || 0), + accountChannel: request.account_channel || '', + accountName: request.account_name || '', + accountNo: request.account_no || '', + note: request.note || '', + reviewedNote: request.reviewed_note || '', + payload: safeParseJson(request.payload_json), + createdAt: request.created_at, + updatedAt: request.updated_at, + reviewedAt: request.reviewed_at, + } +} + function mapWorkOrderAdmin(workOrder: WorkOrderRow) { return { workOrderId: Number(workOrder.id), @@ -1632,6 +1896,61 @@ function normalizeEnabledStatus(value: unknown) { return String(value || 'active').trim() === 'disabled' ? 'disabled' : 'active' } +function normalizeWalletLedgerType(value: unknown) { + const ledgerType = String(value || '').trim() + if (!ledgerType) return '' + if ( + [ + 'manual_credit', + 'deposit_freeze', + 'deposit_release', + 'deposit_deduction', + 'reward_settlement', + ].includes(ledgerType) + ) { + return ledgerType + } + throw createHttpError('钱包流水类型不正确', { + statusCode: 400, + errorCode: 'worker_wallet_ledger_type_invalid', + }) +} + +function normalizeFinanceRequestType(value: unknown) { + const requestType = String(value || '').trim() + if (!requestType) return '' + if (['recharge', 'withdraw'].includes(requestType)) { + return requestType + } + throw createHttpError('资金申请类型不正确', { + statusCode: 400, + errorCode: 'worker_finance_request_type_invalid', + }) +} + +function normalizeFinanceRequestStatus(value: unknown) { + const status = String(value || '').trim() + if (!status) return '' + if (['pending', 'approved', 'rejected', 'cancelled'].includes(status)) { + return status + } + throw createHttpError('资金申请状态不正确', { + statusCode: 400, + errorCode: 'worker_finance_request_status_invalid', + }) +} + +function normalizeWithdrawChannel(value: unknown) { + const channel = String(value || '').trim() + if (['alipay', 'wechat', 'bank'].includes(channel)) { + return channel + } + throw createHttpError('提现方式不正确', { + statusCode: 400, + errorCode: 'worker_withdraw_channel_invalid', + }) +} + function normalizeMatchType(value: unknown) { const matchType = String(value || '').trim() if (matchType === 'exact') return 'exact' diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx index a37be204..bf03b907 100644 --- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx @@ -1,10 +1,96 @@ -import { ReloadOutlined } from '@ant-design/icons' -import { useQuery } from '@tanstack/react-query' -import { Button, Card, Descriptions, Empty, Space, Statistic, Typography } from 'antd' +import { + BankOutlined, + CustomerServiceOutlined, + HistoryOutlined, + LockOutlined, + LogoutOutlined, + ReloadOutlined, + SafetyCertificateOutlined, + WalletOutlined, +} from '@ant-design/icons' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { + Alert, + App, + Button, + Card, + Descriptions, + Empty, + Form, + Input, + InputNumber, + Modal, + Select, + Space, + Table, + Tag, + Typography, +} from 'antd' +import type { TableColumnsType } from 'antd' +import { useRef, useState, type ReactNode, type RefObject } from 'react' +import { useNavigate } from 'react-router' -import { fetchWorkerProfile } from '@/services/worker' +import { + changeWorkerPassword, + createWorkerRechargeRequest, + createWorkerWithdrawRequest, + fetchWorkerFinanceRequests, + fetchWorkerProfile, + fetchWorkerWalletLedgers, + logoutWorker, +} from '@/services/worker' +import { clearWorkerSession } from '@/utils/worker-auth' +import type { + WorkerFinanceRequest, + WorkerProfileSummary, + WorkerUser, + WorkerWalletLedger, +} from '@/types/worker-platform' + +type RechargeFormValues = { + amount?: number + note?: string +} + +type WithdrawFormValues = { + amount?: number + accountChannel?: string + accountName?: string + accountNo?: string + note?: string +} + +type PasswordFormValues = { + currentPassword?: string + newPassword?: string + confirmPassword?: string +} export default function WorkerProfilePage() { + const { message } = App.useApp() + const navigate = useNavigate() + const queryClient = useQueryClient() + const ledgerSectionRef = useRef(null) + const requestSectionRef = useRef(null) + + const [ledgerPage, setLedgerPage] = useState(1) + const [ledgerPageSize, setLedgerPageSize] = useState(10) + const [ledgerType, setLedgerType] = useState('') + const [requestPage, setRequestPage] = useState(1) + const [requestPageSize, setRequestPageSize] = useState(10) + const [requestType, setRequestType] = useState('') + const [requestStatus, setRequestStatus] = useState('') + const [rechargeOpen, setRechargeOpen] = useState(false) + const [withdrawOpen, setWithdrawOpen] = useState(false) + const [passwordOpen, setPasswordOpen] = useState(false) + const [contactOpen, setContactOpen] = useState(false) + const [submittingRecharge, setSubmittingRecharge] = useState(false) + const [submittingWithdraw, setSubmittingWithdraw] = useState(false) + const [submittingPassword, setSubmittingPassword] = useState(false) + const [rechargeForm] = Form.useForm() + const [withdrawForm] = Form.useForm() + const [passwordForm] = Form.useForm() + const profileQuery = useQuery({ queryKey: ['worker-profile'], queryFn: () => fetchWorkerProfile(), @@ -12,12 +98,286 @@ export default function WorkerProfilePage() { }) const worker = profileQuery.data?.data.worker + const summary = profileQuery.data?.data.summary + const canUseFinanceActions = worker?.status === 'active' + + const ledgersQuery = useQuery({ + queryKey: ['worker-wallet-ledgers', ledgerType, ledgerPage, ledgerPageSize], + queryFn: () => + fetchWorkerWalletLedgers({ + page: ledgerPage, + pageSize: ledgerPageSize, + ledgerType, + }), + enabled: Boolean(worker), + retry: false, + }) + + const requestsQuery = useQuery({ + queryKey: [ + 'worker-finance-requests', + requestType, + requestStatus, + requestPage, + requestPageSize, + ], + queryFn: () => + fetchWorkerFinanceRequests({ + page: requestPage, + pageSize: requestPageSize, + requestType, + status: requestStatus, + }), + enabled: Boolean(worker), + retry: false, + }) + + async function refreshAll() { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['worker-profile'] }), + queryClient.invalidateQueries({ queryKey: ['worker-wallet-ledgers'] }), + queryClient.invalidateQueries({ queryKey: ['worker-finance-requests'] }), + ]) + } + + async function submitLogout() { + try { + await logoutWorker() + } catch { + // 接单端退出为无状态操作,本地清理优先。 + } finally { + clearWorkerSession() + message.success('已退出登录') + navigate('/worker/login', { replace: true }) + } + } + + function scrollToSection(ref: RefObject) { + ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + + function jumpToLedgerSection(nextLedgerType = '') { + setLedgerType(nextLedgerType) + setLedgerPage(1) + scrollToSection(ledgerSectionRef) + } + + function jumpToRequestSection(nextRequestType = '', nextStatus = '') { + setRequestType(nextRequestType) + setRequestStatus(nextStatus) + setRequestPage(1) + scrollToSection(requestSectionRef) + } + + async function submitRecharge(values: RechargeFormValues) { + setSubmittingRecharge(true) + try { + await createWorkerRechargeRequest({ + amount: values.amount, + note: String(values.note || '').trim(), + }) + message.success('充值申请已提交,请等待管理员处理') + setRechargeOpen(false) + rechargeForm.resetFields() + await refreshAll() + jumpToRequestSection('recharge') + } catch (error) { + message.error(error instanceof Error ? error.message : '提交充值申请失败') + } finally { + setSubmittingRecharge(false) + } + } + + async function submitWithdraw(values: WithdrawFormValues) { + setSubmittingWithdraw(true) + try { + await createWorkerWithdrawRequest({ + amount: values.amount, + accountChannel: values.accountChannel, + accountName: String(values.accountName || '').trim(), + accountNo: String(values.accountNo || '').trim(), + note: String(values.note || '').trim(), + }) + message.success('提现申请已提交,请等待管理员审核') + setWithdrawOpen(false) + withdrawForm.resetFields() + await refreshAll() + jumpToRequestSection('withdraw') + } catch (error) { + message.error(error instanceof Error ? error.message : '提交提现申请失败') + } finally { + setSubmittingWithdraw(false) + } + } + + async function submitPasswordChange(values: PasswordFormValues) { + setSubmittingPassword(true) + try { + await changeWorkerPassword({ + currentPassword: String(values.currentPassword || ''), + newPassword: String(values.newPassword || ''), + }) + message.success('密码已修改,请重新登录') + clearWorkerSession() + navigate('/worker/login', { replace: true }) + } catch (error) { + message.error(error instanceof Error ? error.message : '修改密码失败') + } finally { + setSubmittingPassword(false) + } + } + + const ledgerColumns: TableColumnsType = [ + { + title: '时间', + dataIndex: 'createdAt', + width: 180, + render: (value) => String(value || '-'), + }, + { + title: '类型', + width: 150, + render: (_, row) => ( + + {formatLedgerType(row.ledgerType)} + + ), + }, + { + title: '金额', + width: 120, + render: (_, row) => ( + = 0 ? 'worker-amount-positive' : 'worker-amount-negative'} + > + {row.amount >= 0 ? '+' : ''} + {formatMoney(row.amount)} + + ), + }, + { + title: '余额', + width: 120, + render: (_, row) => formatMoney(row.balanceAfter), + }, + { + title: '冻结押金', + width: 120, + render: (_, row) => formatMoney(row.frozenAfter), + }, + { + title: '说明', + minWidth: 260, + render: (_, row) => ( +
+ {row.note || '-'} + {row.relatedWorkOrderId ? ( + + 关联工单 #{row.relatedWorkOrderId} + + ) : null} +
+ ), + }, + ] + + const requestColumns: TableColumnsType = [ + { + title: '申请时间', + dataIndex: 'createdAt', + width: 180, + render: (value) => String(value || '-'), + }, + { + title: '类型', + width: 120, + render: (_, row) => ( + + {formatRequestType(row.requestType)} + + ), + }, + { + title: '金额', + width: 120, + render: (_, row) => {formatMoney(row.amount)}, + }, + { + title: '收款信息', + minWidth: 220, + render: (_, row) => + row.requestType === 'withdraw' ? ( +
+ + {formatWithdrawChannel(row.accountChannel)} / {row.accountName || '-'} + + + {maskAccountNo(row.accountNo)} + +
+ ) : ( + 线下充值,等待管理员入账 + ), + }, + { + title: '状态', + width: 130, + render: (_, row) => ( + + {formatRequestStatus(row.status)} + + ), + }, + { + title: '备注', + minWidth: 260, + render: (_, row) => ( +
+ {row.note || '-'} + {row.reviewedNote ? ( + + 审核备注:{row.reviewedNote} + + ) : null} +
+ ), + }, + ] + + if (profileQuery.error) { + return ( +
+
+ 个人中心 + +
+ + + {profileQuery.error instanceof Error ? profileQuery.error.message : '读取个人中心失败'} + + +
+ ) + } return (
个人中心 -
@@ -26,43 +386,639 @@ export default function WorkerProfilePage() { {worker ? ( <> - - - - - - - + {!canUseFinanceActions ? ( + + ) : null} + + +
+
+
+ {String(worker.displayName || worker.username || '打') + .trim() + .slice(0, 1) + .toUpperCase()} +
+
+
+ + {worker.displayName || worker.username} + + + + {worker.level?.name || '未分级'} + + + {formatWorkerStatus(worker.status)} + + +
+ + 账号:{worker.username} + + + 手机号:{worker.phone || '-'} + +
+
+
+ + 累计充值 + + {formatMoney(worker.wallet.totalCreditedAmount)} + + + + 累计结算 + + {formatMoney(worker.wallet.totalSettledAmount)} + + +
+
+ +
+ {buildMetricCards(worker, summary).map((item) => ( +
+ {item.label} + {item.value} + {item.note} +
+ ))} +
+
+ } + title="充值申请" + description="线下充值后提交金额与备注,等待管理员入账。" + tone="blue" + disabled={!canUseFinanceActions} + onClick={() => setRechargeOpen(true)} + /> + } + title="提现申请" + description="填写收款账号并发起提现,系统会保留申请记录。" + tone="green" + disabled={!canUseFinanceActions} + onClick={() => setWithdrawOpen(true)} + /> + } + title="资金流水" + description="查看充值、押金冻结、结算奖励等钱包变动明细。" + tone="slate" + onClick={() => jumpToLedgerSection()} + /> + } + title="提现记录" + description="快速查看提现申请状态、审核备注和收款信息。" + tone="orange" + onClick={() => jumpToRequestSection('withdraw')} + /> + } + title="修改密码" + description="更新接单账号密码,修改后会要求重新登录。" + tone="violet" + onClick={() => setPasswordOpen(true)} + /> + } + title="联系管理员" + description="查看处理说明和当前账号资料,沟通充值、提现或审核问题。" + tone="amber" + onClick={() => setContactOpen(true)} + /> + } + title="退出登录" + description="当前设备退出接单端登录,会清理本地登录态。" + tone="rose" + onClick={submitLogout} + /> +
+ - + {worker.username} {worker.displayName} {worker.phone || '-'} + + + {formatWorkerStatus(worker.status)} + + + + {worker.level?.name || '-'} + {formatMoney(worker.level?.permissions.depositFreeAmount || 0)} {worker.level?.permissions.maxActiveOrders || 0} 单 - {worker.reviewNote || '-'} + + {worker.reviewNote || '-'} + + + {worker.createdAt || '-'} + + + {worker.updatedAt || '-'} + + +
+ + { + setRequestType(nextType) + setRequestPage(1) + }} + /> + + + + + + + + + + + + + + + { + setPasswordOpen(false) + passwordForm.resetFields() + }} + onOk={() => passwordForm.submit()} + > +
+ + + + + + + ({ + validator(_, value) { + if (!value || value === getFieldValue('newPassword')) { + return Promise.resolve() + } + return Promise.reject(new Error('两次输入的新密码不一致')) + }, + }), + ]} + > + + +
+
+ + setContactOpen(false)}> + 我知道了 + + } + onCancel={() => setContactOpen(false)} + > + + + + + {worker?.username || '-'} + + + {worker?.displayName || '-'} + + + {worker?.phone || '-'} + + + {formatWorkerStatus(worker?.status || '')} + + + {formatMoney(worker?.wallet.availableAmount)} + + + + 建议沟通时一起提供:申请类型、金额、订单号或转账备注、收款方式,以及当前账号信息。 + + +
) } +function ProfileActionCard({ + icon, + title, + description, + tone, + disabled = false, + onClick, +}: { + icon: ReactNode + title: string + description: string + tone: 'blue' | 'green' | 'slate' | 'orange' | 'violet' | 'amber' | 'rose' + disabled?: boolean + onClick: () => void +}) { + return ( + + ) +} + +function buildMetricCards(worker: WorkerUser, summary?: WorkerProfileSummary) { + return [ + { + label: '账户余额', + value: formatMoney(worker.wallet.availableAmount), + note: '可用于抢单冻结或提现申请', + }, + { + label: '冻结押金', + value: formatMoney(worker.wallet.frozenDepositAmount), + note: '抢单后冻结,验收通过会自动释放', + }, + { + label: '提现中金额', + value: formatMoney(summary?.pendingWithdrawAmount), + note: '待管理员处理的提现申请金额', + }, + { + label: '累计成单', + value: `${Number(summary?.acceptedOrderCount || 0)} 单`, + note: '已完成并验收通过的工单数量', + }, + ] +} + +function resolveAvailableForWithdraw( + worker: WorkerUser | undefined, + summary: WorkerProfileSummary | undefined, +) { + return Math.max( + 0, + Number(worker?.wallet.availableAmount || 0) - + Number(summary?.pendingWithdrawAmount || 0), + ) +} + function formatMoney(value: number | undefined) { return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}` } -function formatStatus(status: string) { +function formatWorkerStatus(status: string) { if (status === 'active') return '已审核' if (status === 'pending_review') return '待审核' if (status === 'rejected') return '审核未通过' if (status === 'disabled') return '已停用' return status || '-' } + +function resolveWorkerStatusColor(status: string) { + if (status === 'active') return 'green' + if (status === 'pending_review') return 'gold' + if (status === 'rejected') return 'red' + if (status === 'disabled') return 'default' + return 'blue' +} + +function formatLedgerType(ledgerType: string) { + const labels: Record = { + manual_credit: '人工充值', + deposit_freeze: '冻结押金', + deposit_release: '释放押金', + deposit_deduction: '扣除押金', + reward_settlement: '结算报酬', + } + return labels[ledgerType] || ledgerType || '-' +} + +function resolveLedgerTagColor(ledgerType: string) { + if (ledgerType === 'manual_credit') return 'blue' + if (ledgerType === 'deposit_freeze') return 'gold' + if (ledgerType === 'deposit_release') return 'green' + if (ledgerType === 'deposit_deduction') return 'red' + if (ledgerType === 'reward_settlement') return 'cyan' + return 'default' +} + +function formatRequestType(requestType: string) { + if (requestType === 'recharge') return '充值申请' + if (requestType === 'withdraw') return '提现申请' + return requestType || '-' +} + +function formatRequestStatus(status: string) { + const labels: Record = { + pending: '待处理', + approved: '已通过', + rejected: '已拒绝', + cancelled: '已取消', + } + return labels[status] || status || '-' +} + +function resolveRequestStatusColor(status: string) { + if (status === 'approved') return 'green' + if (status === 'pending') return 'gold' + if (status === 'rejected') return 'red' + if (status === 'cancelled') return 'default' + return 'blue' +} + +function formatWithdrawChannel(channel: string) { + if (channel === 'alipay') return '支付宝' + if (channel === 'wechat') return '微信收款' + if (channel === 'bank') return '银行卡' + return channel || '-' +} + +function maskAccountNo(value: string) { + const text = String(value || '').trim() + if (!text) return '-' + if (text.length <= 8) return text + return `${text.slice(0, 4)} **** ${text.slice(-4)}` +} diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index c5d2e167..3ba53582 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -3,9 +3,12 @@ import type { CollectLookupResponse, UploadedFile, WorkOrder, + WorkerFinanceRequest, WorkerListResponse, WorkerLoginResponse, + WorkerProfileResponse, WorkerUser, + WorkerWalletLedger, } from '@/types/worker-platform' export function registerWorker(payload: { @@ -37,8 +40,53 @@ export function logoutWorker() { } export function fetchWorkerProfile() { - return apiGet<{ worker: WorkerUser; permissions: Record }>( - '/api/v1/worker/profile', + return apiGet('/api/v1/worker/profile') +} + +export function fetchWorkerWalletLedgers(params?: Record) { + return apiGet>( + '/api/v1/worker/profile/wallet-ledgers', + params, + ) +} + +export function fetchWorkerFinanceRequests(params?: Record) { + return apiGet>( + '/api/v1/worker/profile/finance-requests', + params, + ) +} + +export function createWorkerRechargeRequest(payload: { + amount?: number + note?: string +}) { + return apiPost<{ request: WorkerFinanceRequest }>( + '/api/v1/worker/profile/recharge-requests', + payload, + ) +} + +export function createWorkerWithdrawRequest(payload: { + amount?: number + accountChannel?: string + accountName?: string + accountNo?: string + note?: string +}) { + return apiPost<{ request: WorkerFinanceRequest }>( + '/api/v1/worker/profile/withdraw-requests', + payload, + ) +} + +export function changeWorkerPassword(payload: { + currentPassword: string + newPassword: string +}) { + return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>( + '/api/v1/worker/profile/change-password', + payload, ) } diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 22a11d62..214f882b 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -1688,13 +1688,222 @@ select { color: #f97316; } -.worker-profile-card { - background: #c8b172; +.worker-profile-hero { + overflow: hidden; + border: 1px solid rgba(25, 94, 125, 0.14); + background: + radial-gradient(circle at top right, rgba(116, 227, 214, 0.18), transparent 30%), + linear-gradient(135deg, #14324a 0%, #1b5d73 55%, #237b77 100%); } -.worker-profile-card .ant-statistic-title, -.worker-profile-card .ant-statistic-content { - color: #fff; +.worker-profile-hero .ant-card-body { + display: grid; + gap: 24px; +} + +.worker-profile-hero-head { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: flex-start; +} + +.worker-profile-identity { + display: flex; + align-items: center; + gap: 16px; + min-width: 0; +} + +.worker-profile-avatar { + width: 72px; + height: 72px; + border-radius: 22px; + display: grid; + place-items: center; + flex-shrink: 0; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.24); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); + color: #ffffff; + font-size: 28px; + font-weight: 700; +} + +.worker-profile-identity-copy { + display: grid; + gap: 6px; + min-width: 0; +} + +.worker-profile-identity-copy .ant-typography { + color: rgba(239, 248, 255, 0.88); +} + +.worker-profile-identity-title { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.worker-profile-identity-title .ant-typography { + margin: 0; + color: #ffffff; +} + +.worker-profile-tag { + border-color: transparent; + background: rgba(255, 255, 255, 0.14); + color: #f8fafc; +} + +.worker-profile-hero-side { + display: flex; + gap: 24px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.worker-profile-hero-side .ant-space { + padding: 10px 14px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.1); +} + +.worker-profile-hero-side .ant-typography { + color: rgba(239, 248, 255, 0.88); +} + +.worker-profile-metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; +} + +.worker-profile-metric { + display: grid; + gap: 6px; + padding: 16px 18px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.worker-profile-metric-label, +.worker-profile-metric-note { + color: rgba(239, 248, 255, 0.75); + font-size: 12px; +} + +.worker-profile-metric-value { + color: #ffffff; + font-size: 26px; + line-height: 1.2; +} + +.worker-profile-actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.worker-profile-action-card { + width: 100%; + padding: 18px; + border-radius: 18px; + border: 1px solid #e2e8f0; + background: #ffffff; + text-align: left; + display: grid; + gap: 10px; + cursor: pointer; + transition: + transform 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04); +} + +.worker-profile-action-card:hover { + transform: translateY(-2px); + box-shadow: 0 16px 34px rgba(15, 23, 42, 0.08); +} + +.worker-profile-action-card:disabled { + cursor: not-allowed; + opacity: 0.58; + transform: none; + box-shadow: none; +} + +.worker-profile-action-icon { + width: 48px; + height: 48px; + border-radius: 14px; + display: grid; + place-items: center; + font-size: 20px; +} + +.worker-profile-action-title { + color: #0f172a; + font-size: 16px; + font-weight: 600; +} + +.worker-profile-action-description { + color: #64748b; + font-size: 13px; + line-height: 1.6; +} + +.worker-profile-action-card.is-blue .worker-profile-action-icon { + background: #dbeafe; + color: #2563eb; +} + +.worker-profile-action-card.is-green .worker-profile-action-icon { + background: #dcfce7; + color: #16a34a; +} + +.worker-profile-action-card.is-slate .worker-profile-action-icon { + background: #e2e8f0; + color: #334155; +} + +.worker-profile-action-card.is-orange .worker-profile-action-icon { + background: #ffedd5; + color: #ea580c; +} + +.worker-profile-action-card.is-violet .worker-profile-action-icon { + background: #ede9fe; + color: #7c3aed; +} + +.worker-profile-action-card.is-amber .worker-profile-action-icon { + background: #fef3c7; + color: #d97706; +} + +.worker-profile-action-card.is-rose .worker-profile-action-icon { + background: #ffe4e6; + color: #e11d48; +} + +.worker-amount-positive { + color: #047857; +} + +.worker-amount-negative { + color: #dc2626; +} + +.worker-profile-help-text { + margin: 0; + color: #64748b; } .collect-order-input { @@ -1752,4 +1961,13 @@ select { .worker-order-grid { grid-template-columns: 1fr; } + + .worker-profile-hero-head { + flex-direction: column; + } + + .worker-profile-hero-side { + width: 100%; + justify-content: flex-start; + } } diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 27a5dd3d..19353c28 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -17,12 +17,56 @@ export type WorkerUser = { wallet: { availableAmount: number frozenDepositAmount: number + totalCreditedAmount: number + totalSettledAmount: number } createdAt: string updatedAt: string reviewedAt: string | null } +export type WorkerProfileSummary = { + acceptedOrderCount: number + pendingWithdrawAmount: number + approvedWithdrawAmount: number + pendingRechargeAmount: number +} + +export type WorkerWalletLedger = { + ledgerId: number + ledgerType: string + amount: number + balanceAfter: number + frozenAfter: number + relatedWorkOrderId: number | null + auditStatus: string + note: string + payload: Record + createdAt: string +} + +export type WorkerFinanceRequest = { + requestId: number + requestType: string + status: string + amount: number + accountChannel: string + accountName: string + accountNo: string + note: string + reviewedNote: string + payload: Record + createdAt: string + updatedAt: string + reviewedAt: string | null +} + +export type WorkerProfileResponse = { + worker: WorkerUser + permissions: Record + summary: WorkerProfileSummary +} + export type WorkerLevel = { levelId: number levelKey: string