工单和发票
酒厂银行账户
This commit is contained in:
@@ -38,6 +38,10 @@ export const HQ_OPERATION_ACTION_OPTIONS = [
|
|||||||
{ value: 'DELIVERY_UPDATE', label: '编辑配送单' },
|
{ value: 'DELIVERY_UPDATE', label: '编辑配送单' },
|
||||||
{ value: 'TICKET_APPROVE', label: '工单通过' },
|
{ value: 'TICKET_APPROVE', label: '工单通过' },
|
||||||
{ value: 'TICKET_REJECT', label: '工单驳回' },
|
{ value: 'TICKET_REJECT', label: '工单驳回' },
|
||||||
|
{ value: 'TICKET_CREATE', label: '创建工单' },
|
||||||
|
{ value: 'INVOICE_CREATE', label: '创建发票申请' },
|
||||||
|
{ value: 'INVOICE_ISSUE', label: '开具发票' },
|
||||||
|
{ value: 'INVOICE_REJECT', label: '驳回发票' },
|
||||||
{ value: 'STORE_PAYOUT_CONFIRM', label: '门店打款确认' },
|
{ value: 'STORE_PAYOUT_CONFIRM', label: '门店打款确认' },
|
||||||
{ value: 'STORE_PAYOUT_BATCH_CONFIRM', label: '批量门店打款' },
|
{ value: 'STORE_PAYOUT_BATCH_CONFIRM', label: '批量门店打款' },
|
||||||
{ value: 'STORE_BILL_CONFIRM', label: '门店对账单确认打款' },
|
{ value: 'STORE_BILL_CONFIRM', label: '门店对账单确认打款' },
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
|
Space,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -47,6 +49,19 @@ type Row = {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CreateFormValues = {
|
||||||
|
orderNo: string;
|
||||||
|
titleType: InvoiceTitleType;
|
||||||
|
invoiceKind: InvoiceKind;
|
||||||
|
titleName: string;
|
||||||
|
taxNo?: string;
|
||||||
|
addressPhone?: string;
|
||||||
|
bankAccount?: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
remark?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function InvoicesPage() {
|
export default function InvoicesPage() {
|
||||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||||
@@ -61,6 +76,11 @@ export default function InvoicesPage() {
|
|||||||
const [detail, setDetail] = useState<Row | null>(null);
|
const [detail, setDetail] = useState<Row | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [createForm] = Form.useForm<CreateFormValues>();
|
||||||
|
const invoiceKind = Form.useWatch('invoiceKind', createForm);
|
||||||
|
const titleType = Form.useWatch('titleType', createForm);
|
||||||
|
|
||||||
async function openDetail(id: string) {
|
async function openDetail(id: string) {
|
||||||
setDetail(await request(`/admin/invoices/${id}`));
|
setDetail(await request(`/admin/invoices/${id}`));
|
||||||
@@ -98,6 +118,36 @@ export default function InvoicesPage() {
|
|||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitCreate() {
|
||||||
|
const values = await createForm.validateFields();
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
await request('/admin/invoices', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
orderNo: values.orderNo.trim(),
|
||||||
|
titleType: values.titleType,
|
||||||
|
invoiceKind: values.invoiceKind,
|
||||||
|
titleName: values.titleName.trim(),
|
||||||
|
taxNo: values.taxNo?.trim() || undefined,
|
||||||
|
addressPhone: values.addressPhone?.trim() || undefined,
|
||||||
|
bankAccount: values.bankAccount?.trim() || undefined,
|
||||||
|
email: values.email.trim(),
|
||||||
|
phone: values.phone.trim(),
|
||||||
|
remark: values.remark?.trim() || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
message.success('发票申请已创建');
|
||||||
|
setCreateOpen(false);
|
||||||
|
createForm.resetFields();
|
||||||
|
reload();
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '创建失败');
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<Row> = [
|
const columns: ColumnsType<Row> = [
|
||||||
{ title: '申请单号', dataIndex: 'invoiceNo', width: 180 },
|
{ title: '申请单号', dataIndex: 'invoiceNo', width: 180 },
|
||||||
{ title: '订单号', dataIndex: 'orderNo', width: 160 },
|
{ title: '订单号', dataIndex: 'orderNo', width: 160 },
|
||||||
@@ -138,7 +188,30 @@ export default function InvoicesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4}>发票管理</Typography.Title>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
发票管理
|
||||||
|
</Typography.Title>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
createForm.setFieldsValue({
|
||||||
|
titleType: 'PERSONAL',
|
||||||
|
invoiceKind: 'NORMAL',
|
||||||
|
});
|
||||||
|
setCreateOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
创建发票申请
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Form
|
<Form
|
||||||
layout="inline"
|
layout="inline"
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
@@ -187,7 +260,7 @@ export default function InvoicesPage() {
|
|||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
extra={
|
extra={
|
||||||
detail?.status === 'PENDING' ? (
|
detail?.status === 'PENDING' ? (
|
||||||
<>
|
<Space>
|
||||||
<Upload
|
<Upload
|
||||||
accept="image/*,.pdf"
|
accept="image/*,.pdf"
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
@@ -196,14 +269,14 @@ export default function InvoicesPage() {
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type="primary" loading={uploading} style={{ marginRight: 8 }}>
|
<Button type="primary" loading={uploading}>
|
||||||
上传并开票
|
上传并开票
|
||||||
</Button>
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
<Button danger onClick={() => void reject()}>
|
<Button danger onClick={() => void reject()}>
|
||||||
驳回
|
驳回
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</Space>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -242,6 +315,110 @@ export default function InvoicesPage() {
|
|||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="创建发票申请"
|
||||||
|
open={createOpen}
|
||||||
|
onCancel={() => setCreateOpen(false)}
|
||||||
|
onOk={() => void submitCreate()}
|
||||||
|
confirmLoading={creating}
|
||||||
|
destroyOnClose
|
||||||
|
okText="提交"
|
||||||
|
width={520}
|
||||||
|
>
|
||||||
|
<Form form={createForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="orderNo"
|
||||||
|
label="订单号"
|
||||||
|
rules={[{ required: true, message: '请填写已完成订单号' }]}
|
||||||
|
extra="仅已完成订单可开票"
|
||||||
|
>
|
||||||
|
<Input placeholder="订单号" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="invoiceKind"
|
||||||
|
label="发票类型"
|
||||||
|
rules={[{ required: true, message: '请选择发票类型' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={(Object.keys(INVOICE_KIND_LABELS) as InvoiceKind[]).map((k) => ({
|
||||||
|
value: k,
|
||||||
|
label: INVOICE_KIND_LABELS[k],
|
||||||
|
}))}
|
||||||
|
onChange={(k: InvoiceKind) => {
|
||||||
|
if (k === 'SPECIAL') createForm.setFieldValue('titleType', 'ENTERPRISE');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="titleType"
|
||||||
|
label="抬头类型"
|
||||||
|
rules={[{ required: true, message: '请选择抬头类型' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={(Object.keys(INVOICE_TITLE_TYPE_LABELS) as InvoiceTitleType[]).map((t) => ({
|
||||||
|
value: t,
|
||||||
|
label: INVOICE_TITLE_TYPE_LABELS[t],
|
||||||
|
disabled: invoiceKind === 'SPECIAL' && t === 'PERSONAL',
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="titleName"
|
||||||
|
label="抬头名称"
|
||||||
|
rules={[{ required: true, message: '请填写抬头名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="个人姓名或企业全称" />
|
||||||
|
</Form.Item>
|
||||||
|
{(titleType === 'ENTERPRISE' || invoiceKind === 'SPECIAL') && (
|
||||||
|
<Form.Item
|
||||||
|
name="taxNo"
|
||||||
|
label="税号"
|
||||||
|
rules={[{ required: true, message: '企业抬头须填写税号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="纳税人识别号" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{invoiceKind === 'SPECIAL' && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="addressPhone"
|
||||||
|
label="地址电话"
|
||||||
|
rules={[{ required: true, message: '专用发票须填写地址电话' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="注册地址及电话" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="bankAccount"
|
||||||
|
label="开户行账号"
|
||||||
|
rules={[{ required: true, message: '专用发票须填写开户行账号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="开户行及账号" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
label="接收邮箱"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请填写邮箱' },
|
||||||
|
{ type: 'email', message: '邮箱格式不正确' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="发票发送邮箱" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
label="手机号"
|
||||||
|
rules={[{ required: true, message: '请填写手机号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="联系手机" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="remark" label="备注">
|
||||||
|
<Input.TextArea rows={2} maxLength={512} showCount />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Descriptions, Drawer, Form, Image, Input, Select, Table, Typography, message } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Descriptions,
|
||||||
|
Drawer,
|
||||||
|
Form,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { TICKET_TYPE_LABELS, type TicketTypeDto } from '@dukang/shared-types';
|
import { TICKET_TYPE_LABELS, type TicketTypeDto } from '@dukang/shared-types';
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
@@ -32,6 +45,13 @@ export default function TicketsPage() {
|
|||||||
);
|
);
|
||||||
const [detail, setDetail] = useState<Row | null>(null);
|
const [detail, setDetail] = useState<Row | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [createForm] = Form.useForm<{
|
||||||
|
ticketType: TicketTypeDto;
|
||||||
|
orderNo: string;
|
||||||
|
remark?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
async function approve(id: string) {
|
async function approve(id: string) {
|
||||||
await request(`/admin/tickets/${id}/approve`, { method: 'POST', body: JSON.stringify({}) });
|
await request(`/admin/tickets/${id}/approve`, { method: 'POST', body: JSON.stringify({}) });
|
||||||
@@ -50,6 +70,29 @@ export default function TicketsPage() {
|
|||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitCreate() {
|
||||||
|
const values = await createForm.validateFields();
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
await request('/admin/tickets', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
ticketType: values.ticketType,
|
||||||
|
orderNo: values.orderNo.trim(),
|
||||||
|
remark: values.remark?.trim() || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
message.success('工单已创建');
|
||||||
|
setCreateOpen(false);
|
||||||
|
createForm.resetFields();
|
||||||
|
reload();
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '创建失败');
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<Row> = [
|
const columns: ColumnsType<Row> = [
|
||||||
{ title: '工单号', dataIndex: 'ticketNo', width: 180 },
|
{ title: '工单号', dataIndex: 'ticketNo', width: 180 },
|
||||||
{
|
{
|
||||||
@@ -84,7 +127,21 @@ export default function TicketsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4}>工单中心</Typography.Title>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
工单中心
|
||||||
|
</Typography.Title>
|
||||||
|
<Button type="primary" onClick={() => setCreateOpen(true)}>
|
||||||
|
创建工单
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Form
|
<Form
|
||||||
layout="inline"
|
layout="inline"
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
@@ -137,14 +194,14 @@ export default function TicketsPage() {
|
|||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
extra={
|
extra={
|
||||||
detail && (detail.status === 'PENDING' || detail.status === 'OPEN') ? (
|
detail && (detail.status === 'PENDING' || detail.status === 'OPEN') ? (
|
||||||
<>
|
<Space>
|
||||||
<Button type="primary" onClick={() => approve(String(detail.id))} style={{ marginRight: 8 }}>
|
<Button type="primary" onClick={() => approve(String(detail.id))}>
|
||||||
通过
|
通过
|
||||||
</Button>
|
</Button>
|
||||||
<Button danger onClick={() => reject(String(detail.id))}>
|
<Button danger onClick={() => reject(String(detail.id))}>
|
||||||
驳回
|
驳回
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</Space>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -173,6 +230,44 @@ export default function TicketsPage() {
|
|||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="创建工单"
|
||||||
|
open={createOpen}
|
||||||
|
onCancel={() => setCreateOpen(false)}
|
||||||
|
onOk={() => void submitCreate()}
|
||||||
|
confirmLoading={creating}
|
||||||
|
destroyOnClose
|
||||||
|
okText="提交"
|
||||||
|
>
|
||||||
|
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'REFUND' }}>
|
||||||
|
<Form.Item
|
||||||
|
name="ticketType"
|
||||||
|
label="工单类型"
|
||||||
|
rules={[{ required: true, message: '请选择类型' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'REFUND', label: '仅退款' },
|
||||||
|
{ value: 'RESHIPMENT', label: '破损补发' },
|
||||||
|
{ value: 'DAMAGE_RETURN', label: '破损退货' },
|
||||||
|
{ value: 'RETURN_REFUND', label: '退货退款' },
|
||||||
|
{ value: 'ALERT', label: '异常' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="orderNo"
|
||||||
|
label="订单号"
|
||||||
|
rules={[{ required: true, message: '请填写订单号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="关联订单号" allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="remark" label="备注">
|
||||||
|
<Input.TextArea rows={3} placeholder="可选" maxLength={512} showCount />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
@@ -17,10 +18,13 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import { WINERY_SETTLEMENT_RATE } from '@dukang/shared-types';
|
import {
|
||||||
|
WINERY_SETTLEMENT_RATE,
|
||||||
|
type SystemConfigFormResponse,
|
||||||
|
} from '@dukang/shared-types';
|
||||||
import { fmtTime } from '../lib/constants';
|
import { fmtTime } from '../lib/constants';
|
||||||
import { downloadExcelCsv } from '../lib/exportExcel';
|
import { downloadExcelCsv } from '../lib/exportExcel';
|
||||||
import { request } from '../lib/api';
|
import { request, type HqProfile } from '../lib/api';
|
||||||
import { useAdminList } from '../lib/useAdminList';
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
|
|
||||||
type Row = {
|
type Row = {
|
||||||
@@ -59,8 +63,16 @@ const DELIVERY_LABELS: Record<string, string> = {
|
|||||||
CROSS_CITY: '跨城',
|
CROSS_CITY: '跨城',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WINERY_BANK_KEYS = [
|
||||||
|
'WINERY_BANK_ACCOUNT_NAME',
|
||||||
|
'WINERY_BANK_NAME',
|
||||||
|
'WINERY_BANK_BRANCH',
|
||||||
|
'WINERY_BANK_ACCOUNT_NO',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export default function WineryBillsPage() {
|
export default function WineryBillsPage() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [bankForm] = Form.useForm<Record<string, string>>();
|
||||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||||
'/admin/winery-bills',
|
'/admin/winery-bills',
|
||||||
@@ -79,6 +91,18 @@ export default function WineryBillsPage() {
|
|||||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||||
const [detail, setDetail] = useState<(Row & { items?: BillItem[] }) | null>(null);
|
const [detail, setDetail] = useState<(Row & { items?: BillItem[] }) | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||||||
|
const [bankOpen, setBankOpen] = useState(false);
|
||||||
|
const [bankLoading, setBankLoading] = useState(false);
|
||||||
|
const [bankSaving, setBankSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const canEditWineryBank =
|
||||||
|
profile?.adminRole === 'SUPER_ADMIN' ||
|
||||||
|
(profile?.permissionKeys ?? []).includes('system_settings_winery_bank');
|
||||||
|
|
||||||
function confirmPay(ids: string[], amountHint?: number) {
|
function confirmPay(ids: string[], amountHint?: number) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -126,6 +150,41 @@ export default function WineryBillsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openBankModal() {
|
||||||
|
setBankOpen(true);
|
||||||
|
setBankLoading(true);
|
||||||
|
try {
|
||||||
|
const cfg = await request<SystemConfigFormResponse>('/admin/system-config');
|
||||||
|
const values: Record<string, string> = {};
|
||||||
|
for (const key of WINERY_BANK_KEYS) {
|
||||||
|
values[key] = cfg.values[key] ?? '';
|
||||||
|
}
|
||||||
|
bankForm.setFieldsValue(values);
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '加载失败');
|
||||||
|
setBankOpen(false);
|
||||||
|
} finally {
|
||||||
|
setBankLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBank() {
|
||||||
|
const values = await bankForm.validateFields();
|
||||||
|
setBankSaving(true);
|
||||||
|
try {
|
||||||
|
await request('/admin/system-config', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ values }),
|
||||||
|
});
|
||||||
|
message.success('酒厂银行账户已保存');
|
||||||
|
setBankOpen(false);
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '保存失败');
|
||||||
|
} finally {
|
||||||
|
setBankSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const summary = data?.summary;
|
const summary = data?.summary;
|
||||||
const ratePct = Math.round(WINERY_SETTLEMENT_RATE * 100);
|
const ratePct = Math.round(WINERY_SETTLEMENT_RATE * 100);
|
||||||
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
||||||
@@ -185,7 +244,16 @@ export default function WineryBillsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Space direction="vertical" size={0} style={{ marginBottom: 16 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 16,
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
酒厂对账单
|
酒厂对账单
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
@@ -193,6 +261,12 @@ export default function WineryBillsPage() {
|
|||||||
每日 8:00 汇总昨日已付订单(实付 × {ratePct}%);未打款红色、已打款绿色,可展开订单明细
|
每日 8:00 汇总昨日已付订单(实付 × {ratePct}%);未打款红色、已打款绿色,可展开订单明细
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
{canEditWineryBank ? (
|
||||||
|
<Button type="default" onClick={() => void openBankModal()}>
|
||||||
|
酒厂银行账户信息配置
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
{summary && (
|
{summary && (
|
||||||
<Card size="small" style={{ marginBottom: 16 }}>
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
@@ -337,6 +411,43 @@ export default function WineryBillsPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="酒厂银行账户信息配置"
|
||||||
|
open={bankOpen}
|
||||||
|
onCancel={() => setBankOpen(false)}
|
||||||
|
onOk={() => void saveBank()}
|
||||||
|
confirmLoading={bankSaving}
|
||||||
|
destroyOnClose
|
||||||
|
okText="保存"
|
||||||
|
>
|
||||||
|
<Form form={bankForm} layout="vertical" disabled={bankLoading}>
|
||||||
|
<Form.Item
|
||||||
|
name="WINERY_BANK_ACCOUNT_NAME"
|
||||||
|
label="户名"
|
||||||
|
rules={[{ required: true, message: '请填写户名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="收款账户户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="WINERY_BANK_NAME"
|
||||||
|
label="开户银行"
|
||||||
|
rules={[{ required: true, message: '请填写开户银行' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="如:中国工商银行" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="WINERY_BANK_BRANCH" label="开户支行">
|
||||||
|
<Input placeholder="可选" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="WINERY_BANK_ACCOUNT_NO"
|
||||||
|
label="银行账号"
|
||||||
|
rules={[{ required: true, message: '请填写银行账号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="银行卡号" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const HQ_PERMISSION_CATALOG = [
|
|||||||
{ key: 'system_settings_oss', label: '对象存储 OSS', group: '系统设置' },
|
{ key: 'system_settings_oss', label: '对象存储 OSS', group: '系统设置' },
|
||||||
{ key: 'system_settings_app', label: '应用链接', group: '系统设置' },
|
{ key: 'system_settings_app', label: '应用链接', group: '系统设置' },
|
||||||
{ key: 'system_settings_deploy', label: '发布部署', group: '系统设置' },
|
{ key: 'system_settings_deploy', label: '发布部署', group: '系统设置' },
|
||||||
|
{ key: 'system_settings_winery_bank', label: '酒厂银行账户', group: '系统设置' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
||||||
@@ -37,6 +38,7 @@ export const SYSTEM_CONFIG_GROUP_PERMISSION: Record<string, HqPermissionKey> = {
|
|||||||
oss: 'system_settings_oss',
|
oss: 'system_settings_oss',
|
||||||
app: 'system_settings_app',
|
app: 'system_settings_app',
|
||||||
deploy: 'system_settings_deploy',
|
deploy: 'system_settings_deploy',
|
||||||
|
winery_bank: 'system_settings_winery_bank',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SYSTEM_SETTINGS_PERMISSION_KEYS = Object.values(
|
export const SYSTEM_SETTINGS_PERMISSION_KEYS = Object.values(
|
||||||
@@ -97,6 +99,7 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
|||||||
'benefit',
|
'benefit',
|
||||||
'invoices',
|
'invoices',
|
||||||
'logs',
|
'logs',
|
||||||
|
'system_settings_winery_bank',
|
||||||
],
|
],
|
||||||
CUSTOMER_SERVICE: ['dashboard', 'users', 'orders', 'tickets', 'invoices', 'logs'],
|
CUSTOMER_SERVICE: ['dashboard', 'users', 'orders', 'tickets', 'invoices', 'logs'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export const HqOperationAction = {
|
|||||||
DELIVERY_UPDATE: 'DELIVERY_UPDATE',
|
DELIVERY_UPDATE: 'DELIVERY_UPDATE',
|
||||||
TICKET_APPROVE: 'TICKET_APPROVE',
|
TICKET_APPROVE: 'TICKET_APPROVE',
|
||||||
TICKET_REJECT: 'TICKET_REJECT',
|
TICKET_REJECT: 'TICKET_REJECT',
|
||||||
|
TICKET_CREATE: 'TICKET_CREATE',
|
||||||
|
INVOICE_CREATE: 'INVOICE_CREATE',
|
||||||
|
INVOICE_ISSUE: 'INVOICE_ISSUE',
|
||||||
|
INVOICE_REJECT: 'INVOICE_REJECT',
|
||||||
STORE_PAYOUT_CONFIRM: 'STORE_PAYOUT_CONFIRM',
|
STORE_PAYOUT_CONFIRM: 'STORE_PAYOUT_CONFIRM',
|
||||||
STORE_PAYOUT_BATCH_CONFIRM: 'STORE_PAYOUT_BATCH_CONFIRM',
|
STORE_PAYOUT_BATCH_CONFIRM: 'STORE_PAYOUT_BATCH_CONFIRM',
|
||||||
STORE_BILL_CONFIRM: 'STORE_BILL_CONFIRM',
|
STORE_BILL_CONFIRM: 'STORE_BILL_CONFIRM',
|
||||||
@@ -119,6 +123,10 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
|
|||||||
[HqOperationAction.DELIVERY_UPDATE]: '编辑配送单',
|
[HqOperationAction.DELIVERY_UPDATE]: '编辑配送单',
|
||||||
[HqOperationAction.TICKET_APPROVE]: '工单通过',
|
[HqOperationAction.TICKET_APPROVE]: '工单通过',
|
||||||
[HqOperationAction.TICKET_REJECT]: '工单驳回',
|
[HqOperationAction.TICKET_REJECT]: '工单驳回',
|
||||||
|
[HqOperationAction.TICKET_CREATE]: '创建工单',
|
||||||
|
[HqOperationAction.INVOICE_CREATE]: '创建发票申请',
|
||||||
|
[HqOperationAction.INVOICE_ISSUE]: '开具发票',
|
||||||
|
[HqOperationAction.INVOICE_REJECT]: '驳回发票',
|
||||||
[HqOperationAction.STORE_PAYOUT_CONFIRM]: '门店打款确认',
|
[HqOperationAction.STORE_PAYOUT_CONFIRM]: '门店打款确认',
|
||||||
[HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM]: '批量门店打款',
|
[HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM]: '批量门店打款',
|
||||||
[HqOperationAction.STORE_BILL_CONFIRM]: '门店对账单确认打款',
|
[HqOperationAction.STORE_BILL_CONFIRM]: '门店对账单确认打款',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const SYSTEM_CONFIG_GROUPS: SystemConfigGroupMeta[] = [
|
|||||||
{ key: 'oss', label: '对象存储 OSS' },
|
{ key: 'oss', label: '对象存储 OSS' },
|
||||||
{ key: 'app', label: '应用链接' },
|
{ key: 'app', label: '应用链接' },
|
||||||
{ key: 'deploy', label: '发布部署' },
|
{ key: 'deploy', label: '发布部署' },
|
||||||
|
{ key: 'winery_bank', label: '酒厂银行账户' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const G = {
|
const G = {
|
||||||
@@ -19,6 +20,7 @@ const G = {
|
|||||||
oss: 'oss',
|
oss: 'oss',
|
||||||
app: 'app',
|
app: 'app',
|
||||||
deploy: 'deploy',
|
deploy: 'deploy',
|
||||||
|
winery_bank: 'winery_bank',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** HQ 可维护字段(不含 NODE_ENV / DATABASE_URL / JWT 等基础设施项) */
|
/** HQ 可维护字段(不含 NODE_ENV / DATABASE_URL / JWT 等基础设施项) */
|
||||||
@@ -101,6 +103,37 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
|||||||
|
|
||||||
{ key: 'DEPLOY_WEBHOOK_URL', label: '发布 Webhook URL', group: G.deploy, type: 'string', requiresRestart: false },
|
{ key: 'DEPLOY_WEBHOOK_URL', label: '发布 Webhook URL', group: G.deploy, type: 'string', requiresRestart: false },
|
||||||
{ key: 'DEPLOY_WEBHOOK_SECRET', label: '发布 Webhook Secret', group: G.deploy, type: 'password', secret: true, requiresRestart: false },
|
{ key: 'DEPLOY_WEBHOOK_SECRET', label: '发布 Webhook Secret', group: G.deploy, type: 'password', secret: true, requiresRestart: false },
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'WINERY_BANK_ACCOUNT_NAME',
|
||||||
|
label: '户名',
|
||||||
|
group: G.winery_bank,
|
||||||
|
type: 'string',
|
||||||
|
requiresRestart: false,
|
||||||
|
description: '酒厂收款账户户名,打款时对照',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'WINERY_BANK_NAME',
|
||||||
|
label: '开户银行',
|
||||||
|
group: G.winery_bank,
|
||||||
|
type: 'string',
|
||||||
|
requiresRestart: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'WINERY_BANK_BRANCH',
|
||||||
|
label: '开户支行',
|
||||||
|
group: G.winery_bank,
|
||||||
|
type: 'string',
|
||||||
|
requiresRestart: false,
|
||||||
|
placeholder: '可选',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'WINERY_BANK_ACCOUNT_NO',
|
||||||
|
label: '银行账号',
|
||||||
|
group: G.winery_bank,
|
||||||
|
type: 'string',
|
||||||
|
requiresRestart: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 已从 HQ 配置移除、仅保留在 .env 的键(启动时从 DB 清理) */
|
/** 已从 HQ 配置移除、仅保留在 .env 的键(启动时从 DB 清理) */
|
||||||
|
|||||||
@@ -170,6 +170,23 @@ export class CreateTicketDto {
|
|||||||
extraJson?: Record<string, unknown>;
|
extraJson?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminCreateTicketDto {
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['REFUND', 'RESHIPMENT', 'ALERT', 'DAMAGE_RETURN', 'RETURN_REFUND'])
|
||||||
|
ticketType: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
orderNo: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
remark?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
evidenceUrls?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdateTicketStatusDto {
|
export class UpdateTicketStatusDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/co
|
|||||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
import { AuthUser } from '../../common/guards/jwt-auth.guard';
|
import { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||||
|
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||||
import { TradeService } from '../trade/trade.service';
|
import { TradeService } from '../trade/trade.service';
|
||||||
import { IssueInvoiceDto, RejectInvoiceDto } from '../trade/dto/after-sale.dto';
|
import {
|
||||||
|
AdminCreateInvoiceDto,
|
||||||
|
IssueInvoiceDto,
|
||||||
|
RejectInvoiceDto,
|
||||||
|
} from '../trade/dto/after-sale.dto';
|
||||||
|
|
||||||
@Controller('admin/invoices')
|
@Controller('admin/invoices')
|
||||||
@UseGuards(HqAuthGuard)
|
@UseGuards(HqAuthGuard)
|
||||||
@@ -23,12 +29,29 @@ export class AdminInvoicesController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HqOperation({
|
||||||
|
action: HqOperationAction.INVOICE_CREATE,
|
||||||
|
refType: 'INVOICE',
|
||||||
|
batch: true,
|
||||||
|
includeBody: true,
|
||||||
|
})
|
||||||
|
create(@Body() body: AdminCreateInvoiceDto) {
|
||||||
|
return this.tradeService.adminCreateInvoice(body);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
detail(@Param('id') id: string) {
|
detail(@Param('id') id: string) {
|
||||||
return this.tradeService.adminGetInvoice(BigInt(id));
|
return this.tradeService.adminGetInvoice(BigInt(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/issue')
|
@Post(':id/issue')
|
||||||
|
@HqOperation({
|
||||||
|
action: HqOperationAction.INVOICE_ISSUE,
|
||||||
|
refType: 'INVOICE',
|
||||||
|
refIdParam: 'id',
|
||||||
|
includeBody: true,
|
||||||
|
})
|
||||||
issue(
|
issue(
|
||||||
@CurrentUser() user: AuthUser,
|
@CurrentUser() user: AuthUser,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@@ -38,6 +61,12 @@ export class AdminInvoicesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/reject')
|
@Post(':id/reject')
|
||||||
|
@HqOperation({
|
||||||
|
action: HqOperationAction.INVOICE_REJECT,
|
||||||
|
refType: 'INVOICE',
|
||||||
|
refIdParam: 'id',
|
||||||
|
includeBody: true,
|
||||||
|
})
|
||||||
reject(
|
reject(
|
||||||
@CurrentUser() user: AuthUser,
|
@CurrentUser() user: AuthUser,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
|||||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||||
import { AdminTicketsService } from './admin-tickets.service';
|
import { AdminTicketsService } from './admin-tickets.service';
|
||||||
import { TicketListQueryDto } from '../common/dto/common-query.dto';
|
import { TicketListQueryDto } from '../common/dto/common-query.dto';
|
||||||
|
import { AdminCreateTicketDto } from '../common/dto/common-mutate.dto';
|
||||||
|
|
||||||
@Controller('admin/tickets')
|
@Controller('admin/tickets')
|
||||||
@UseGuards(HqAuthGuard)
|
@UseGuards(HqAuthGuard)
|
||||||
@@ -19,6 +20,17 @@ export class AdminTicketsController {
|
|||||||
return this.service.list(query);
|
return this.service.list(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HqOperation({
|
||||||
|
action: HqOperationAction.TICKET_CREATE,
|
||||||
|
refType: 'TICKET',
|
||||||
|
batch: true,
|
||||||
|
includeBody: true,
|
||||||
|
})
|
||||||
|
create(@Body() body: AdminCreateTicketDto) {
|
||||||
|
return this.service.createByHq(body);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
detail(@Param('id') id: string) {
|
detail(@Param('id') id: string) {
|
||||||
return this.service.detail(BigInt(id));
|
return this.service.detail(BigInt(id));
|
||||||
|
|||||||
@@ -40,6 +40,43 @@ export class AdminTicketsService {
|
|||||||
return this.ticketService.detail(id);
|
return this.ticketService.detail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createByHq(body: {
|
||||||
|
ticketType: string;
|
||||||
|
orderNo: string;
|
||||||
|
remark?: string;
|
||||||
|
evidenceUrls?: string[];
|
||||||
|
}) {
|
||||||
|
const orderNo = body.orderNo?.trim();
|
||||||
|
if (!orderNo) throw new BadRequestException('请填写订单号');
|
||||||
|
const order = await this.prisma.order.findFirst({ where: { orderNo } });
|
||||||
|
if (!order) throw new NotFoundException('订单不存在');
|
||||||
|
if (order.status === 'PENDING_PAY' || order.status === 'CANCELLED') {
|
||||||
|
throw new BadRequestException('当前订单不可创建工单');
|
||||||
|
}
|
||||||
|
if (['REFUNDING', 'REFUNDED'].includes(order.status) && body.ticketType !== 'ALERT') {
|
||||||
|
throw new BadRequestException('订单已在退款流程中');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = await this.prisma.commonTicket.findFirst({
|
||||||
|
where: {
|
||||||
|
ticketType: body.ticketType as never,
|
||||||
|
refType: 'ORDER',
|
||||||
|
refId: order.id,
|
||||||
|
status: { in: ['PENDING', 'OPEN'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (pending) throw new BadRequestException('该类型工单已在处理中');
|
||||||
|
|
||||||
|
const evidenceUrls = (body.evidenceUrls ?? []).filter((u) => typeof u === 'string' && u.trim());
|
||||||
|
return this.ticketService.create({
|
||||||
|
ticketType: body.ticketType,
|
||||||
|
refType: 'ORDER',
|
||||||
|
refId: order.id.toString(),
|
||||||
|
remark: body.remark ?? '',
|
||||||
|
extraJson: evidenceUrls.length ? { evidenceUrls } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private parseExtra(raw: unknown): TicketCollabExtra {
|
private parseExtra(raw: unknown): TicketCollabExtra {
|
||||||
if (!raw || typeof raw !== 'object') return {};
|
if (!raw || typeof raw !== 'object') return {};
|
||||||
return raw as TicketCollabExtra;
|
return raw as TicketCollabExtra;
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ export class CreateInvoiceDto {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminCreateInvoiceDto extends CreateInvoiceDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(32)
|
||||||
|
orderNo: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class IssueInvoiceDto {
|
export class IssueInvoiceDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -754,6 +754,27 @@ export class TradeService {
|
|||||||
return days;
|
return days;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async adminCreateInvoice(
|
||||||
|
body: {
|
||||||
|
orderNo: string;
|
||||||
|
titleType: string;
|
||||||
|
invoiceKind: string;
|
||||||
|
titleName: string;
|
||||||
|
taxNo?: string;
|
||||||
|
addressPhone?: string;
|
||||||
|
bankAccount?: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
remark?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const orderNo = body.orderNo?.trim();
|
||||||
|
if (!orderNo) throw new BadRequestException('请填写订单号');
|
||||||
|
const order = await this.prisma.order.findFirst({ where: { orderNo } });
|
||||||
|
if (!order) throw new NotFoundException('订单不存在');
|
||||||
|
return this.createInvoice(order.userId, order.id, body);
|
||||||
|
}
|
||||||
|
|
||||||
async adminListInvoices(query: { status?: string; page?: number; pageSize?: number }) {
|
async adminListInvoices(query: { status?: string; page?: number; pageSize?: number }) {
|
||||||
const page = query.page ?? 1;
|
const page = query.page ?? 1;
|
||||||
const pageSize = query.pageSize ?? 20;
|
const pageSize = query.pageSize ?? 20;
|
||||||
|
|||||||
Reference in New Issue
Block a user