新增好友赠送验收模式支持打手资料提交加好友冷却与模板配置

This commit is contained in:
yml2213
2026-08-22 11:19:49 +08:00
parent ba35d27976
commit 6aec4a7b40
36 changed files with 1646 additions and 52 deletions
@@ -208,6 +208,8 @@ export default function ProductRulesPanel() {
: undefined,
timeoutMinutes: rule.timeoutMinutes || 0,
timeoutPolicy: rule.timeoutPolicy || 'reopen',
acceptanceMode: rule.acceptanceMode || 'standard',
giftCooldownHours: rule.giftCooldownHours || 72,
fieldsText: formatRuleFields(rule.requirement?.fields || []),
sortOrder: rule.sortOrder,
enabled: rule.enabled,
@@ -244,6 +246,8 @@ export default function ProductRulesPanel() {
sharingTotalAmount?: number
timeoutMinutes?: number
timeoutPolicy?: string
acceptanceMode?: string
giftCooldownHours?: number
fieldsText?: string
enabled?: boolean
autoCreate?: boolean
@@ -251,6 +255,11 @@ export default function ProductRulesPanel() {
}) {
try {
const unitMode = values.pricingMode === 'unit'
const isFriendGift = values.acceptanceMode === 'friend_gift'
if (isFriendGift && values.sharingEnabled === true) {
message.error('好友赠送验收模式不支持拼单,请先关闭拼单')
return
}
const hasManualSharingOverride =
form.isFieldTouched('sharingTotalQuantity') || form.isFieldTouched('sharingUnitReward')
? Number(values.sharingTotalQuantity || 0) > 0 ||
@@ -262,6 +271,8 @@ export default function ProductRulesPanel() {
ruleId: editingRule?.ruleId,
ruleKey: editingRule?.ruleKey || '',
unitPrice: unitMode ? Number(values.unitPrice || 0) : 0,
acceptanceMode: isFriendGift ? 'friend_gift' : 'standard',
sharingEnabled: isFriendGift ? false : values.sharingEnabled === true,
sharingAutoFromOrder:
unitMode && values.sharingEnabled === true && !hasManualSharingOverride,
})
@@ -638,6 +649,11 @@ export default function ProductRulesPanel() {
</Tag>
) : null}
{row.acceptanceMode === 'friend_gift' ? (
<Tag color="orange" style={{ fontSize: 10, margin: 0, padding: '0 3px' }}>
</Tag>
) : null}
</Space>
</Space>
),
@@ -728,6 +744,8 @@ export default function ProductRulesPanel() {
enabled: true,
autoCreate: true,
sharingEnabled: false,
acceptanceMode: 'standard',
giftCooldownHours: 72,
fieldsText:
'gameId:游戏编号\ngameNickname:游戏昵称\nsystem:系统#安卓,苹果\nserverZone:区服#QQ区,微信区',
}}
@@ -794,6 +812,53 @@ export default function ProductRulesPanel() {
</Form.Item>
</div>
{/* 验收模式:好友赠送模式与拼单互斥 */}
<div className="form-grid-3col">
<Form.Item
label="验收模式"
name="acceptanceMode"
extra="好友赠送:打手接单后先提交本人资料,后台加好友并冷却后才能赠送交付"
>
<Radio.Group
optionType="button"
buttonStyle="solid"
className="full-width"
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
]}
onChange={(event) => {
if (event.target.value === 'friend_gift') {
form.setFieldsValue({ sharingEnabled: false })
}
}}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) =>
getFieldValue('acceptanceMode') === 'friend_gift' ? (
<Form.Item
label="赠送冷却"
name="giftCooldownHours"
rules={[{ required: true, message: '请填写冷却小时数' }]}
extra="确认加好友后需冷却该时长才能赠送(默认 72 小时 = 3 天)"
>
<InputNumber
min={1}
max={720}
step={1}
addonAfter="小时"
className="full-width"
/>
</Form.Item>
) : (
<Form.Item label={<span />} />
)
}
</Form.Item>
</div>
{/* Row 3: 超时策略 + 来源 + 平台 */}
<div className="form-grid-3col">
<Form.Item label="超时策略" name="timeoutPolicy">
@@ -825,6 +890,7 @@ export default function ProductRulesPanel() {
{({ getFieldValue }) => {
const sharingEnabled = getFieldValue('sharingEnabled')
const unitMode = getFieldValue('pricingMode') === 'unit'
const friendGiftMode = getFieldValue('acceptanceMode') === 'friend_gift'
return (
<div className="switches-inline-group span-2">
@@ -834,8 +900,13 @@ export default function ProductRulesPanel() {
<Form.Item label="自动建单" name="autoCreate" valuePropName="checked">
<Switch size="small" />
</Form.Item>
<Form.Item label="启用拼单" name="sharingEnabled" valuePropName="checked">
<Switch size="small" />
<Form.Item
label="启用拼单"
name="sharingEnabled"
valuePropName="checked"
tooltip={friendGiftMode ? '好友赠送模式不支持拼单' : undefined}
>
<Switch size="small" disabled={friendGiftMode} />
</Form.Item>
{sharingEnabled ? (
@@ -72,10 +72,14 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
rewardAmount: number
paymentAmount: number
materialComplete?: boolean
acceptanceMode?: string
}) {
setLoading(true)
try {
await createAdminMockWorkOrder(values)
await createAdminMockWorkOrder({
...values,
acceptanceMode: values.acceptanceMode || 'standard',
})
message.success('Mock 工单已生成')
await onCreated()
} catch (error) {
@@ -147,6 +151,20 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
>
<Switch />
</Form.Item>
<Form.Item
className="worker-order-mock-field-complete"
label="验收模式"
name="acceptanceMode"
initialValue="standard"
>
<Select
style={{ width: 110 }}
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
]}
/>
</Form.Item>
<Form.Item className="worker-order-mock-action">
<Button type="primary" icon={<PlusOutlined />} htmlType="submit" loading={loading}>
@@ -49,6 +49,7 @@ import {
assignAdminWorkOrderToWorker,
cancelAdminWorkOrder,
cancelAdminWorkOrderShare,
confirmAdminWorkOrderFriend,
deductAdminWorkOrderPendingDeposit,
deleteAdminWorkOrder,
fetchAdminWorkCategories,
@@ -60,6 +61,7 @@ import {
pinAdminWorkOrder,
publishAdminWorkOrder,
reopenAdminCancelledWorkOrder,
resetAdminWorkOrderGiftMaterial,
resolveAdminProblemWorkOrder,
submitAdminWorkOrderMaterial,
unpublishAdminWorkOrder,
@@ -74,6 +76,8 @@ import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import {
DeadlineCountdown,
formatAcceptanceMode,
formatGiftPhase,
formatTimeoutPolicyLabel,
getAcceptanceFiles,
getAcceptanceImageUrls,
@@ -88,6 +92,7 @@ import {
formatMoney,
formatSharingShareStatus,
formatStatus,
resolveGiftPhaseColor,
resolveSharingShareStatusColor,
resolveStatusColor,
} from './shared'
@@ -117,6 +122,14 @@ const FILTERABLE_WORK_ORDER_STATUSES = [
] as const
const VIP_EVIDENCE_PENDING_STATUS = 'vip_evidence_pending'
const GIFT_PHASE_FILTER_OPTIONS = [
'material_required',
'material_submitted',
'friend_countdown',
'gift_ready',
] as const
type MaterialFormValues = {
fields?: Record<string, string>
rewardAmount?: number
@@ -145,6 +158,8 @@ export default function WorkOrdersPanel() {
const [keywordInput, setKeywordInput] = useState('')
const [workerId, setWorkerId] = useState<number | undefined>()
const [workerLevelIds, setWorkerLevelIds] = useState<number[]>([])
const [acceptanceMode, setAcceptanceMode] = useState<string | undefined>()
const [giftPhase, setGiftPhase] = useState<string | undefined>()
const [filterWorkerKeyword, setFilterWorkerKeyword] = useState('')
const [assignWorkerKeyword, setAssignWorkerKeyword] = useState('')
const [page, setPage] = useState(1)
@@ -220,6 +235,8 @@ export default function WorkOrdersPanel() {
workerId,
workerLevelIds.join(','),
targetWorkOrderId,
acceptanceMode || '',
giftPhase || '',
page,
pageSize,
createdRange ? createdRange[0] : '',
@@ -230,6 +247,8 @@ export default function WorkOrdersPanel() {
status:
selectedWorkOrderStatuses.length > 0 ? selectedWorkOrderStatuses.join(',') : undefined,
vipEvidencePending: vipEvidencePending || undefined,
acceptanceMode: acceptanceMode || undefined,
giftPhase: giftPhase || undefined,
workOrderId: targetWorkOrderId,
keyword,
workerId,
@@ -277,12 +296,26 @@ export default function WorkOrdersPanel() {
})
useEffect(() => {
setSelectedAcceptanceOrderIds([])
}, [statuses, keyword, workerId, workerLevelIds, targetWorkOrderId, page, pageSize])
}, [
statuses,
keyword,
workerId,
workerLevelIds,
targetWorkOrderId,
acceptanceMode,
giftPhase,
page,
pageSize,
])
useEffect(() => {
if (!showWorkerLevelFilter) setWorkerLevelIds([])
}, [showWorkerLevelFilter])
useEffect(() => {
if (acceptanceMode !== 'friend_gift') setGiftPhase(undefined)
}, [acceptanceMode])
function openAssignModal(row: WorkOrder) {
setAssignOrder(row)
setAssignWorkerId(undefined)
@@ -593,6 +626,8 @@ export default function WorkOrdersPanel() {
setStatuses([])
setWorkerId(undefined)
setWorkerLevelIds([])
setAcceptanceMode(undefined)
setGiftPhase(undefined)
setCreatedRange(null)
setPage(1)
}
@@ -716,6 +751,47 @@ export default function WorkOrdersPanel() {
})
}
function confirmFriendAdded(row: WorkOrder) {
const cooldownHours = Number(row.gift?.cooldownHours || 72)
modal.confirm({
title: '确认已添加打手为游戏好友?',
content: (
<div>
<Typography.Paragraph>
/ ID /
</Typography.Paragraph>
<Typography.Paragraph>
{cooldownHours}{' '}
</Typography.Paragraph>
</div>
),
okText: '已加好友,开始冷却',
cancelText: '取消',
onOk: () =>
runAction(
() => confirmAdminWorkOrderFriend(row.workOrderId),
`已确认加好友,${cooldownHours} 小时冷却开始`,
),
})
}
function requestResetGiftMaterial(row: WorkOrder) {
modal.confirm({
title: '重置打手好友赠送资料?',
content:
'打手已提交的资料、加好友时间与冷却进度将全部清零,订单回到“待打手提交资料”阶段,需打手重新提交后再次确认加好友。',
okText: '确认重置',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () =>
runAction(
() => resetAdminWorkOrderGiftMaterial(row.workOrderId),
'已重置,等待打手重新提交资料',
),
})
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单',
@@ -828,6 +904,12 @@ export default function WorkOrdersPanel() {
</Tag>
) : null}
{row.acceptanceMode === 'friend_gift' ? <Tag color="purple"></Tag> : null}
{row.acceptanceMode === 'friend_gift' && row.gift ? (
<Tag color={resolveGiftPhaseColor(row.gift.phase)}>
{formatGiftPhase(row.gift.phase)}
</Tag>
) : null}
</Space>
)
},
@@ -1003,6 +1085,20 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.capabilities?.canConfirmFriend ? (
<Button
type="primary"
icon={<UserAddOutlined />}
onClick={() => confirmFriendAdded(row)}
>
</Button>
) : null}
{row.capabilities?.canResetGiftFlow ? (
<Button danger onClick={() => requestResetGiftMaterial(row)}>
</Button>
) : null}
{row.capabilities?.canMarkProblem ? (
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
@@ -1101,6 +1197,36 @@ export default function WorkOrdersPanel() {
}}
/>
) : null}
<Select
allowClear
value={acceptanceMode}
placeholder="全部验收模式"
style={{ width: 150 }}
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
]}
onChange={(nextMode) => {
setAcceptanceMode(nextMode)
setPage(1)
}}
/>
{acceptanceMode === 'friend_gift' ? (
<Select
allowClear
value={giftPhase}
placeholder="全部赠送阶段"
style={{ width: 170 }}
options={GIFT_PHASE_FILTER_OPTIONS.map((phase) => ({
value: phase,
label: <Tag color={resolveGiftPhaseColor(phase)}>{formatGiftPhase(phase)}</Tag>,
}))}
onChange={(nextPhase) => {
setGiftPhase(nextPhase)
setPage(1)
}}
/>
) : null}
<AdminRangePicker
value={createdRange ? [dayjs(createdRange[0]), dayjs(createdRange[1])] : null}
onChange={(dates) => {
@@ -1305,6 +1431,93 @@ export default function WorkOrdersPanel() {
</div>
</Card>
{detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift ? (
<Card
title="好友赠送进度"
size="small"
extra={
<Space>
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
{formatGiftPhase(detailOrder.gift.phase)}
</Tag>
{detailOrder.capabilities?.canConfirmFriend ? (
<Button
type="primary"
size="small"
onClick={() => confirmFriendAdded(detailOrder)}
>
</Button>
) : null}
{detailOrder.capabilities?.canResetGiftFlow ? (
<Button
size="small"
danger
onClick={() => requestResetGiftMaterial(detailOrder)}
>
</Button>
) : null}
</Space>
}
>
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="冷却时长">
{detailOrder.gift.cooldownHours}
</Descriptions.Item>
<Descriptions.Item label="打手资料提交">
{formatAdminDateTime(detailOrder.gift.boosterMaterial?.submittedAt)}
</Descriptions.Item>
<Descriptions.Item label="已加好友">
{formatAdminDateTime(detailOrder.gift.friendAddedAt)}
</Descriptions.Item>
<Descriptions.Item label="可赠送时间">
{detailOrder.gift.giftAvailableAt
? formatAdminDateTime(detailOrder.gift.giftAvailableAt)
: '-'}
</Descriptions.Item>
</Descriptions>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
{(
[
['gameNickname', '游戏昵称'],
['gameId', '游戏ID'],
['serverZone', '区服'],
['system', '系统'],
] as const
).map(([key, label]) => (
<Descriptions.Item key={key} label={label}>
<span className="worker-order-detail-value">
{String(detailOrder.gift?.boosterMaterial?.fields?.[key] || '').trim() ||
'-'}
</span>
</Descriptions.Item>
))}
<Descriptions.Item label="打手备注" span={2}>
<span className="worker-order-detail-value">
{String(detailOrder.gift?.boosterMaterial?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
<div>
<Typography.Text type="secondary"></Typography.Text>
<div style={{ marginTop: 8 }}>
{detailOrder.gift.boosterMaterial?.screenshots?.length ? (
<ImagePreviewList
files={detailOrder.gift.boosterMaterial.screenshots}
size={72}
/>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
</div>
</div>
</Card>
) : null}
<Card title="验收信息" size="small">
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
@@ -244,6 +244,31 @@ export function formatSharingShareStatus(status: string) {
return labels[status] || status
}
export function formatAcceptanceMode(mode: string) {
if (mode === 'friend_gift') return '好友赠送'
return '标准验收'
}
export function formatGiftPhase(phase: string) {
const labels: Record<string, string> = {
material_required: '待打手提交资料',
material_submitted: '待确认加好友',
friend_countdown: '赠送冷却中',
gift_ready: '可赠送',
}
return labels[phase] || phase
}
export function resolveGiftPhaseColor(phase: string) {
const colors: Record<string, string> = {
material_required: 'orange',
material_submitted: 'geekblue',
friend_countdown: 'gold',
gift_ready: 'green',
}
return colors[phase] || 'default'
}
export function resolveSharingShareStatusColor(status: string) {
if (status === 'accepted') return 'green'
if (status === 'submitted') return 'gold'
@@ -1,5 +1,5 @@
import { MessageOutlined, PictureOutlined } from '@ant-design/icons'
import { Button, Card, Descriptions, Drawer, Tag, Typography } from 'antd'
import { MessageOutlined, PictureOutlined, UserAddOutlined } from '@ant-design/icons'
import { Button, Card, Descriptions, Drawer, Space, Steps, Tag, Typography } from 'antd'
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
import ImagePreviewList from '@/components/files/ImagePreviewList'
@@ -11,19 +11,22 @@ import {
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSubmitGiftMaterial,
formatMoney,
formatStatus,
getAcceptanceSubmittedAt,
getDisplayOrderNo,
getGiftPhaseMeta,
getMaterialDetailItems,
getMaterialScreenshots,
getOrderStatusDisplay,
getOrderStatusHint,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
GiftCountdownTag,
hasSubmittedAcceptance,
isFriendGiftOrder,
renderDetailFieldValues,
resolveStatusColor,
getStatusHint,
} from './worker-order-utils'
export function WorkerOrderDetailDrawer({
@@ -32,6 +35,7 @@ export function WorkerOrderDetailDrawer({
orderTimelineVisible,
onClose,
onOpenAcceptance,
onOpenGiftMaterial,
onOpenCancelRequest,
onOpenFeedback,
}: {
@@ -40,6 +44,7 @@ export function WorkerOrderDetailDrawer({
orderTimelineVisible: boolean
onClose: () => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
}) {
@@ -55,7 +60,18 @@ export function WorkerOrderDetailDrawer({
{order ? (
<div className="worker-order-detail-stack">
<div className="worker-order-detail-actions">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
<Tag color={getOrderStatusDisplay(order).color}>
{getOrderStatusDisplay(order).label}
</Tag>
{canSubmitGiftMaterial(order) ? (
<Button
type="primary"
icon={<UserAddOutlined />}
onClick={() => onOpenGiftMaterial(order)}
>
</Button>
) : null}
{canEditAcceptanceImages(order) ? (
<Button
type={canSubmitAcceptance(order) ? 'primary' : 'default'}
@@ -82,9 +98,11 @@ export function WorkerOrderDetailDrawer({
<Descriptions.Item label="分类">{order.categoryName || '-'}</Descriptions.Item>
<Descriptions.Item label="平台订单号">{getDisplayOrderNo(order)}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
<Tag color={getOrderStatusDisplay(order).color}>
{getOrderStatusDisplay(order).label}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="阶段说明">{getStatusHint(order.status)}</Descriptions.Item>
<Descriptions.Item label="阶段说明">{getOrderStatusHint(order)}</Descriptions.Item>
<Descriptions.Item label="接单奖励">
{formatMoney(order.rewardAmount)}
</Descriptions.Item>
@@ -118,6 +136,8 @@ export function WorkerOrderDetailDrawer({
) : null}
</Card>
{isFriendGiftOrder(order) && order.gift ? <GiftProgressCard order={order} /> : null}
<Card title="验收信息" size="small">
<AcceptanceSummary order={order} />
</Card>
@@ -141,6 +161,97 @@ export function WorkerOrderDetailDrawer({
)
}
function GiftProgressCard({ order }: { order: WorkOrder }) {
const gift = order.gift
if (!gift) return null
const phaseMeta = getGiftPhaseMeta(gift.phase)
const stepIndex =
gift.phase === 'material_required'
? 0
: gift.phase === 'material_submitted'
? 1
: gift.phase === 'friend_countdown'
? 2
: 3
const boosterFields = gift.boosterMaterial?.fields || {}
const boosterFieldLabels: Record<string, string> = {
gameNickname: '游戏昵称',
gameId: '游戏ID',
serverZone: '区服',
system: '系统',
}
const boosterItems = Object.entries(boosterFields).filter(([, value]) =>
String(value || '').trim(),
)
return (
<Card title="好友赠送进度" size="small">
<div className="worker-order-detail-stack">
<Space size={8} wrap>
<Tag color={phaseMeta.color}>{phaseMeta.label}</Tag>
{gift.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={gift.giftAvailableAt} />
) : null}
{gift.phase === 'gift_ready' ? (
<Tag color="green"></Tag>
) : null}
</Space>
<Steps
size="small"
current={stepIndex}
items={[
{ title: '提交资料' },
{ title: '添加好友' },
{ title: '冷却等待' },
{ title: '赠送交付' },
]}
/>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="冷却时长">{gift.cooldownHours} </Descriptions.Item>
<Descriptions.Item label="已加好友">
{formatDateTime(gift.friendAddedAt)}
</Descriptions.Item>
<Descriptions.Item label="可赠送时间" span={2}>
{gift.giftAvailableAt ? (
<Space size={8} wrap>
<span>{formatDateTime(gift.giftAvailableAt)}</span>
{gift.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={gift.giftAvailableAt} />
) : null}
</Space>
) : (
'-'
)}
</Descriptions.Item>
</Descriptions>
{boosterItems.length > 0 || (gift.boosterMaterial?.screenshots || []).length > 0 ? (
<div>
<Typography.Text type="secondary"></Typography.Text>
<div style={{ marginTop: 8 }}>
{boosterItems.length > 0 ? (
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
{boosterItems.map(([key, value]) => (
<Descriptions.Item key={key} label={boosterFieldLabels[key] || key}>
<span className="worker-order-detail-value">{String(value)}</span>
</Descriptions.Item>
))}
</Descriptions>
) : null}
{(gift.boosterMaterial?.screenshots || []).length > 0 ? (
<div style={{ marginTop: 8 }}>
<ImagePreviewList files={gift.boosterMaterial.screenshots} size={64} />
</div>
) : null}
</div>
</div>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
</Card>
)
}
function AcceptanceSummary({ order }: { order: WorkOrder }) {
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
@@ -25,19 +25,21 @@ import {
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSubmitGiftMaterial,
formatMoney,
formatStatus,
getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots,
getOrderStatusDisplay,
getSettlementLabel,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
GiftCountdownTag,
hasSubmittedAcceptance,
isFriendGiftOrder,
isVipEvidencePending,
resolveStatusColor,
} from './worker-order-utils'
export function WorkerOrderMobileList({
@@ -61,6 +63,7 @@ export function WorkerOrderMobileList({
onPageChange,
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
@@ -88,6 +91,7 @@ export function WorkerOrderMobileList({
onPageChange: (page: number, pageSize: number) => void
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
onRemindAcceptance: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
@@ -159,6 +163,7 @@ export function WorkerOrderMobileList({
order={order}
onOpenDetail={onOpenDetail}
onOpenAcceptance={onOpenAcceptance}
onOpenGiftMaterial={onOpenGiftMaterial}
onRemindAcceptance={onRemindAcceptance}
onOpenCancelRequest={onOpenCancelRequest}
onOpenFeedback={onOpenFeedback}
@@ -188,6 +193,7 @@ function WorkerOrderMobileCard({
order,
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
@@ -221,6 +227,7 @@ function WorkerOrderMobileCard({
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
const noteText = getWorkerAcceptance(order)?.note
const workerNote = String(order.workerNote || '').trim()
const statusDisplay = getOrderStatusDisplay(order)
return (
<article className="worker-order-mobile-card">
@@ -244,10 +251,15 @@ function WorkerOrderMobileCard({
<span className="worker-order-mobile-orderno-text">{getDisplayOrderNo(order)}</span>
</div>
<div className="worker-order-mobile-header-right">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
<Tag color={statusDisplay.color}>{statusDisplay.label}</Tag>
{order.status === 'in_progress' && order.deadlineAt ? (
<DeadlineCountdown deadlineAt={order.deadlineAt} />
) : null}
{isFriendGiftOrder(order) &&
order.status === 'in_progress' &&
order.gift?.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={order.gift.giftAvailableAt} />
) : null}
</div>
</div>
@@ -371,6 +383,16 @@ function WorkerOrderMobileCard({
<Button size="small" icon={<EyeOutlined />} onClick={() => onOpenDetail(order)}>
</Button>
{canSubmitGiftMaterial(order) ? (
<Button
size="small"
type="primary"
className="worker-order-mobile-action-submit"
onClick={() => onOpenGiftMaterial(order)}
>
</Button>
) : null}
{canEditAcceptanceImages(order) ? (
<Button
size="small"
@@ -1,5 +1,5 @@
import { PictureOutlined } from '@ant-design/icons'
import { Button, Form, Input, Modal, Space, Tag, Typography } from 'antd'
import { Button, Form, Input, Modal, Radio, Space, Tag, Typography } from 'antd'
import type { FormInstance } from 'antd'
import ImageUpload from '@/components/files/ImageUpload'
@@ -22,6 +22,13 @@ export type AcceptanceFormValues = { note?: string }
export type WorkerNoteFormValues = { note?: string }
export type CancelRequestFormValues = { reason?: string }
export type FeedbackFormValues = { content?: string }
export type GiftMaterialFormValues = {
gameNickname: string
gameId: string
serverZone: string
system?: string
note?: string
}
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
@@ -58,6 +65,12 @@ export function WorkerOrderModals({
onAcceptanceFilesChange,
onSubmitAcceptance,
onSaveAcceptanceImages,
giftMaterialOrder,
giftMaterialForm,
giftMaterialFiles,
onCloseGiftMaterial,
onGiftMaterialFilesChange,
onSubmitGiftMaterial,
}: {
isMobile: boolean
feedbackOrder: WorkOrder | null
@@ -82,6 +95,12 @@ export function WorkerOrderModals({
onAcceptanceFilesChange: (files: UploadedFile[]) => void
onSubmitAcceptance: (values: AcceptanceFormValues) => void | Promise<void>
onSaveAcceptanceImages: (values: AcceptanceFormValues) => void | Promise<void>
giftMaterialOrder: WorkOrder | null
giftMaterialForm: FormInstance<GiftMaterialFormValues>
giftMaterialFiles: UploadedFile[]
onCloseGiftMaterial: () => void
onGiftMaterialFilesChange: (files: UploadedFile[]) => void
onSubmitGiftMaterial: (values: GiftMaterialFormValues) => void | Promise<void>
}) {
const feedbackItems = feedbacks.filter((item) => item.workOrderId === feedbackOrder?.workOrderId)
return (
@@ -266,6 +285,66 @@ export function WorkerOrderModals({
</Form.Item>
</Form>
</Modal>
<Modal
title={`好友赠送资料 · ${getDisplayOrderNo(giftMaterialOrder)}`}
open={Boolean(giftMaterialOrder)}
width={isMobile ? '96%' : 620}
onCancel={onCloseGiftMaterial}
onOk={() => giftMaterialForm.submit()}
okText="提交资料"
destroyOnHidden
>
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Form form={giftMaterialForm} layout="vertical" onFinish={onSubmitGiftMaterial}>
<Form.Item
label="游戏昵称"
name="gameNickname"
rules={[{ required: true, whitespace: true, message: '请填写游戏昵称' }]}
>
<Input placeholder="本人游戏昵称" maxLength={50} />
</Form.Item>
<Form.Item
label="游戏ID"
name="gameId"
rules={[{ required: true, whitespace: true, message: '请填写游戏ID' }]}
>
<Input placeholder="本人游戏ID" maxLength={50} />
</Form.Item>
<Form.Item
label="区服"
name="serverZone"
rules={[{ required: true, whitespace: true, message: '请填写区服' }]}
>
<Input placeholder="例如:QQ区 / 微信区 / 具体区服名称" maxLength={50} />
</Form.Item>
<Form.Item label="系统" name="system" initialValue="">
<Radio.Group>
<Radio.Button value=""></Radio.Button>
<Radio.Button value="安卓"></Radio.Button>
<Radio.Button value="苹果"></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
label="主页截图"
required
extra="至少上传一张本人游戏主页截图,供后台核对身份。"
>
<ImageUpload
scene="worker-gift-material"
scope="worker"
value={giftMaterialFiles}
onChange={onGiftMaterialFilesChange}
maxCount={5}
showPasteHint
/>
</Form.Item>
<Form.Item label="备注" name="note">
<Input.TextArea rows={2} maxLength={200} showCount placeholder="选填" />
</Form.Item>
</Form>
</Modal>
</>
)
}
@@ -19,19 +19,21 @@ import {
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSubmitGiftMaterial,
formatMoney,
formatStatus,
getAcceptanceSubmittedAt,
getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots,
getOrderStatusDisplay,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
GiftCountdownTag,
hasSubmittedAcceptance,
isFriendGiftOrder,
isVipEvidencePending,
resolveStatusColor,
} from './worker-order-utils'
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
@@ -39,6 +41,7 @@ type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'd
export function buildWorkerOrderTableColumns({
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
@@ -48,6 +51,7 @@ export function buildWorkerOrderTableColumns({
}: {
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
onRemindAcceptance: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
@@ -96,25 +100,33 @@ export function buildWorkerOrderTableColumns({
{
title: '状态',
width: 130,
render: (_, row) => (
<div className="cell-stack">
<Space wrap size={4}>
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
render: (_, row) => {
const statusDisplay = getOrderStatusDisplay(row)
return (
<div className="cell-stack">
<Space wrap size={4}>
<Tag color={statusDisplay.color}>{statusDisplay.label}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
{isFriendGiftOrder(row) &&
row.status === 'in_progress' &&
row.gift?.phase === 'friend_countdown' ? (
<GiftCountdownTag giftAvailableAt={row.gift.giftAvailableAt} />
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text
type="danger"
ellipsis={{ tooltip: row.problemNote }}
style={{ fontSize: 12 }}
>
{row.problemNote}
</Typography.Text>
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text
type="danger"
ellipsis={{ tooltip: row.problemNote }}
style={{ fontSize: 12 }}
>
{row.problemNote}
</Typography.Text>
) : null}
</div>
),
</div>
)
},
},
{
title: '奖励 / 押金',
@@ -205,6 +217,11 @@ export function buildWorkerOrderTableColumns({
onClick={() => onOpenDetail(row)}
/>
</Tooltip>
{canSubmitGiftMaterial(row) ? (
<Button type="primary" onClick={() => onOpenGiftMaterial(row)}>
</Button>
) : null}
{canEditAcceptanceImages(row) ? (
<Button
icon={<PictureOutlined />}
@@ -14,10 +14,11 @@ import {
remindWorkerAcceptance,
saveWorkerAcceptanceDraft,
saveWorkerOrderNote,
supplementWorkerAcceptedOrderEvidence,
submitWorkerAcceptance,
submitWorkerCancelRequest,
submitWorkerGiftMaterial,
submitWorkerOrderFeedback,
supplementWorkerAcceptedOrderEvidence,
} from '@/services/worker'
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
import { useIsMobile } from '@/utils/use-is-mobile'
@@ -29,6 +30,7 @@ import {
type AcceptanceFormValues,
type CancelRequestFormValues,
type FeedbackFormValues,
type GiftMaterialFormValues,
type WorkerNoteFormValues,
WorkerOrderModals,
} from './WorkerOrderModals'
@@ -42,6 +44,7 @@ import {
getWorkerAcceptance,
getWorkerDraft,
hasSubmittedAcceptance,
isGiftSubmissionBlocked,
patchWorkOrderReadState,
toPngBlob,
} from './worker-order-utils'
@@ -165,7 +168,10 @@ export default function WorkerOrdersPage() {
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
const [feedbackOrder, setFeedbackOrder] = useState<WorkOrder | null>(null)
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
const [giftMaterialOrder, setGiftMaterialOrder] = useState<WorkOrder | null>(null)
const [giftMaterialFiles, setGiftMaterialFiles] = useState<UploadedFile[]>([])
const [form] = Form.useForm<AcceptanceFormValues>()
const [giftMaterialForm] = Form.useForm<GiftMaterialFormValues>()
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
const [cancelRequestForm] = Form.useForm<CancelRequestFormValues>()
const [feedbackForm] = Form.useForm<FeedbackFormValues>()
@@ -292,6 +298,43 @@ export default function WorkerOrdersPage() {
setDetailOrder(null)
}
function openGiftMaterialModal(order: WorkOrder) {
markOrderAsRead(order)
setGiftMaterialOrder(order)
setGiftMaterialFiles([])
giftMaterialForm.resetFields()
setDetailOrder(null)
}
async function submitGiftMaterial(values: GiftMaterialFormValues) {
if (!giftMaterialOrder) return
if (giftMaterialFiles.length === 0) {
message.error('请上传主页截图')
return
}
const fields: Record<string, string> = {
gameNickname: String(values.gameNickname || '').trim(),
gameId: String(values.gameId || '').trim(),
serverZone: String(values.serverZone || '').trim(),
}
const system = String(values.system || '').trim()
if (system) fields.system = system
try {
await submitWorkerGiftMaterial(giftMaterialOrder.workOrderId, {
fields,
screenshots: giftMaterialFiles,
note: String(values.note || '').trim(),
})
message.success('资料已提交,等待后台确认加好友')
setGiftMaterialOrder(null)
setGiftMaterialFiles([])
giftMaterialForm.resetFields()
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '提交资料失败')
}
}
function openNoteModal(order: WorkOrder) {
markOrderAsRead(order)
setNoteOrder(order)
@@ -386,6 +429,11 @@ export default function WorkerOrdersPage() {
return
}
if (isGiftSubmissionBlocked(editingOrder)) {
message.error('好友赠送冷却未结束,暂不能提交验收')
return
}
const canAutoAccept = canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
if (acceptanceFiles.length === 0 && !canAutoAccept) {
message.error('请上传验收图片')
@@ -437,6 +485,7 @@ export default function WorkerOrdersPage() {
const columns = buildWorkerOrderTableColumns({
onOpenDetail: openOrderDetail,
onOpenAcceptance: openAcceptanceModal,
onOpenGiftMaterial: openGiftMaterialModal,
onRemindAcceptance: (order) => {
void submitAcceptanceReminder(order)
},
@@ -519,6 +568,7 @@ export default function WorkerOrdersPage() {
}}
onOpenDetail={openOrderDetail}
onOpenAcceptance={openAcceptanceModal}
onOpenGiftMaterial={openGiftMaterialModal}
onRemindAcceptance={(order) => {
void submitAcceptanceReminder(order)
}}
@@ -574,6 +624,7 @@ export default function WorkerOrdersPage() {
orderTimelineVisible={orderTimelineVisible}
onClose={() => setDetailOrder(null)}
onOpenAcceptance={openAcceptanceModal}
onOpenGiftMaterial={openGiftMaterialModal}
onOpenCancelRequest={openCancelRequestModal}
onOpenFeedback={openFeedbackModal}
/>
@@ -614,6 +665,16 @@ export default function WorkerOrdersPage() {
onAcceptanceFilesChange={setAcceptanceFiles}
onSubmitAcceptance={submitAcceptance}
onSaveAcceptanceImages={saveAcceptanceImages}
giftMaterialOrder={giftMaterialOrder}
giftMaterialForm={giftMaterialForm}
giftMaterialFiles={giftMaterialFiles}
onCloseGiftMaterial={() => {
setGiftMaterialOrder(null)
setGiftMaterialFiles([])
giftMaterialForm.resetFields()
}}
onGiftMaterialFilesChange={setGiftMaterialFiles}
onSubmitGiftMaterial={submitGiftMaterial}
/>
</>
)
@@ -1,4 +1,5 @@
import { Descriptions, Space, Tag, Typography } from 'antd'
import { useEffect, useState } from 'react'
import type {
CollectField,
@@ -32,10 +33,65 @@ export function patchWorkOrderReadState(current: unknown, workOrderId: number) {
}
}
/** 好友赠送验收模式(整单,不参与拼单)。 */
export function isFriendGiftOrder(order: WorkOrder) {
return order.acceptanceMode === 'friend_gift' && !order.myShare
}
export function getGiftPhaseMeta(phase: string): { label: string; color: string } {
const metas: Record<string, { label: string; color: string }> = {
material_required: { label: '待提交资料', color: 'orange' },
material_submitted: { label: '待后台加好友', color: 'processing' },
friend_countdown: { label: '赠送冷却中', color: 'gold' },
gift_ready: { label: '可赠送', color: 'green' },
}
return metas[String(phase || '').trim()] || { label: String(phase || '-'), color: 'default' }
}
/** 好友赠送模式:待提交本人资料阶段。 */
export function canSubmitGiftMaterial(order: WorkOrder) {
return (
isFriendGiftOrder(order) &&
['in_progress', 'problem'].includes(order.status) &&
order.gift?.phase === 'material_required'
)
}
/** 好友赠送模式:冷却未结束前不能提交赠送凭证。 */
export function isGiftSubmissionBlocked(order: WorkOrder) {
if (!isFriendGiftOrder(order)) return false
if (!['in_progress', 'problem'].includes(order.status)) return false
return order.gift?.phase !== 'gift_ready'
}
/** 好友赠送单进行中按赠送阶段展示状态,其他模式维持原状态文案。 */
export function getOrderStatusDisplay(order: WorkOrder): { label: string; color: string } {
if (isFriendGiftOrder(order) && order.status === 'in_progress' && order.gift) {
return getGiftPhaseMeta(order.gift.phase)
}
return { label: formatStatus(order.status), color: resolveStatusColor(order.status) }
}
export function getOrderStatusHint(order: WorkOrder) {
if (isFriendGiftOrder(order) && order.status === 'in_progress' && order.gift) {
const hints: Record<string, string> = {
material_required: '请提交本人游戏资料(昵称/ID/区服 + 主页截图),后台核对后添加好友。',
material_submitted: '资料已提交,等待后台添加游戏好友并确认。',
friend_countdown: '好友已添加,冷却结束后才能赠送。',
gift_ready: '冷却已结束,赠送完成后请上传赠送凭证并提交验收。',
}
return hints[order.gift.phase] || getStatusHint(order.status)
}
return getStatusHint(order.status)
}
export function canSubmitAcceptance(order: WorkOrder) {
if (order.myShare) {
return order.myShare.status === 'joined' && order.status !== 'accepted'
}
if (isFriendGiftOrder(order)) {
return ['in_progress', 'problem'].includes(order.status) && order.gift?.phase === 'gift_ready'
}
return ['in_progress', 'problem'].includes(order.status)
}
@@ -59,6 +115,8 @@ export function canSubmitFeedback(order: WorkOrder) {
}
export function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
// 好友赠送模式必须由后台人工验收赠送凭证,不适用 VIP 免审核直通。
if (isFriendGiftOrder(order)) return false
if (!workerCanAutoAcceptWithoutEvidence) return false
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
return order.sharing?.enabled !== true && ['in_progress', 'problem'].includes(order.status)
@@ -265,6 +323,23 @@ export function getAcceptanceSubmittedAt(order: WorkOrder): string | null {
return order.submittedAt || acceptanceSubmittedAt || null
}
/** 好友赠送冷却倒计时:按分钟刷新,展示“X天X小时后可赠送”。 */
export function GiftCountdownTag({ giftAvailableAt }: { giftAvailableAt: string | null }) {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
const timer = window.setInterval(() => setNow(Date.now()), 30_000)
return () => window.clearInterval(timer)
}, [])
const targetMs = Date.parse(String(giftAvailableAt || ''))
if (!Number.isFinite(targetMs)) return null
const remaining = targetMs - now
if (remaining <= 0) return <Tag color="green"></Tag>
const hours = Math.ceil(remaining / 3_600_000)
const days = Math.floor(hours / 24)
const restHours = hours % 24
return <Tag color="gold">{days > 0 ? `${days}${restHours}小时` : `${hours}小时`}</Tag>
}
export function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
@@ -220,6 +220,8 @@ export function saveAdminWorkProductRule(payload: {
sharingTotalAmount?: number
timeoutMinutes?: number
timeoutPolicy?: string
acceptanceMode?: 'standard' | 'friend_gift' | string
giftCooldownHours?: number
fieldsText?: string
sortOrder?: number
}) {
@@ -525,6 +527,7 @@ export function createAdminMockWorkOrder(payload: {
rewardAmount: number
paymentAmount: number
materialComplete?: boolean
acceptanceMode?: 'standard' | 'friend_gift' | string
}) {
return apiPost<{ order: WorkOrder }>('/api/v1/admin/worker-platform/orders/mock', payload)
}
@@ -616,6 +619,21 @@ export function markAdminWorkOrderProblem(workOrderId: number, note: string) {
)
}
/** 好友赠送模式:核对打手资料并添加好友后确认,开始冷却倒计时 */
export function confirmAdminWorkOrderFriend(workOrderId: number) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/confirm-friend`,
)
}
/** 好友赠送模式:重置打手资料,冷却与好友时间清零 */
export function resetAdminWorkOrderGiftMaterial(workOrderId: number, note?: string) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/reset-gift-material`,
note ? { note } : {},
)
}
export function resolveAdminProblemWorkOrder(
workOrderId: number,
payload: {
+11
View File
@@ -288,6 +288,17 @@ export function submitWorkerAcceptance(
)
}
/** 好友赠送模式:提交打手本人资料(昵称/ID/区服/系统 + 主页截图) */
export function submitWorkerGiftMaterial(
workOrderId: number,
payload: { fields: Record<string, string>; screenshots: UploadedFile[]; note?: string },
) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/worker/orders/${workOrderId}/gift-material`,
payload,
)
}
export function remindWorkerAcceptance(workOrderId: number) {
return apiPost<{ reminderCount: number; nextRemindAt: string }>(
`/api/v1/worker/orders/${workOrderId}/remind-acceptance`,
@@ -338,6 +338,10 @@ export type WorkProductRule = {
}
timeoutMinutes?: number
timeoutPolicy?: 'reopen' | 'cancel_release' | 'cancel_deduct' | string
/** 验收模式:standard / friend_gift */
acceptanceMode?: 'standard' | 'friend_gift' | string
/** 好友赠送冷却小时数(默认 72) */
giftCooldownHours?: number
sortOrder: number
createdAt: string
updatedAt: string
@@ -456,6 +460,31 @@ export type WorkOrderShare = {
}
}
export type WorkOrderGiftPhase =
| 'material_required'
| 'material_submitted'
| 'friend_countdown'
| 'gift_ready'
| string
/** 好友赠送验收模式的打手本人资料 */
export type BoosterMaterial = {
fields: Record<string, string>
screenshots: UploadedFile[]
note: string
submittedAt: string
}
/** 好友赠送验收模式进度;phase 含实时计算的 gift_ready */
export type WorkOrderGiftProgress = {
phase: WorkOrderGiftPhase
rawPhase: string
cooldownHours: number
friendAddedAt: string | null
giftAvailableAt: string | null
boosterMaterial: BoosterMaterial
}
export type WorkOrder = {
workOrderId: number
orderId?: number | null
@@ -468,6 +497,10 @@ export type WorkOrder = {
status: WorkOrderStatus
rawStatus?: WorkOrderStatus
hallAvailable?: boolean
/** 验收模式:standard 标准验收 / friend_gift 好友赠送 */
acceptanceMode?: 'standard' | 'friend_gift' | string
/** 好友赠送模式进度;仅 acceptanceMode = friend_gift 时返回 */
gift?: WorkOrderGiftProgress
capabilities?: {
canEdit: boolean
canConfigureSharing: boolean
@@ -483,6 +516,8 @@ export type WorkOrder = {
canCancel: boolean
canResolveProblem: boolean
canDeduct: boolean
canConfirmFriend?: boolean
canResetGiftFlow?: boolean
}
rewardAmount: number
requiredDepositAmount?: number