Compare commits
2 Commits
7fa2c6fbed
...
608e405468
| Author | SHA1 | Date | |
|---|---|---|---|
| 608e405468 | |||
| b36f2ea63a |
@@ -79,6 +79,11 @@ export type DashboardStats = {
|
|||||||
partnersTotal: number;
|
partnersTotal: number;
|
||||||
redeemToday: number;
|
redeemToday: number;
|
||||||
deliveriesTotal: number;
|
deliveriesTotal: number;
|
||||||
|
pendingPayouts?: number;
|
||||||
|
/** 合伙人已确认、待总部审核打款 */
|
||||||
|
pendingBills?: number;
|
||||||
|
pendingPartnerDraftBills?: number;
|
||||||
|
openTickets?: number;
|
||||||
ordersByStatus: Array<{ status: string; count: number }>;
|
ordersByStatus: Array<{ status: string; count: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button, Card, Col, Descriptions, Modal, Row, Space, Statistic, Table, Typography, message,
|
Button, Card, Col, Descriptions, Modal, Row, Space, Statistic, Table, Typography, message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
@@ -164,6 +165,47 @@ export default function DashboardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||||
|
<Col xs={24} sm={12} lg={8}>
|
||||||
|
<Card
|
||||||
|
loading={loading}
|
||||||
|
title="待审核合伙人打款"
|
||||||
|
extra={<Link to="/finance/partner-bills">去处理</Link>}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
value={stats?.pendingBills ?? 0}
|
||||||
|
suffix="笔"
|
||||||
|
valueStyle={{ color: (stats?.pendingBills ?? 0) > 0 ? '#fa8c16' : undefined }}
|
||||||
|
/>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
合伙人已确认申请,待总部通过或驳回
|
||||||
|
{(stats?.pendingPartnerDraftBills ?? 0) > 0
|
||||||
|
? `(另有 ${stats?.pendingPartnerDraftBills} 笔待合伙人确认)`
|
||||||
|
: ''}
|
||||||
|
</Typography.Text>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={8}>
|
||||||
|
<Card
|
||||||
|
loading={loading}
|
||||||
|
title="待打款门店结算"
|
||||||
|
extra={<Link to="/finance/store-bills">去处理</Link>}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
value={stats?.pendingPayouts ?? 0}
|
||||||
|
suffix="笔"
|
||||||
|
valueStyle={{ color: (stats?.pendingPayouts ?? 0) > 0 ? '#fa8c16' : undefined }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={8}>
|
||||||
|
<Card loading={loading} title="待处理工单">
|
||||||
|
<Statistic value={stats?.openTickets ?? 0} suffix="个" />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={24} lg={12}>
|
<Col xs={24} lg={12}>
|
||||||
<Card title="已合并访客账号" loading={loading}>
|
<Card title="已合并访客账号" loading={loading}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Form,
|
Form,
|
||||||
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
@@ -29,6 +30,7 @@ type Row = {
|
|||||||
status: string;
|
status: string;
|
||||||
periodStart: string;
|
periodStart: string;
|
||||||
periodEnd: string;
|
periodEnd: string;
|
||||||
|
rejectReason?: string | null;
|
||||||
partner?: { companyName?: string; phone?: string };
|
partner?: { companyName?: string; phone?: string };
|
||||||
partnerAccount?: { companyName?: string; phone?: string };
|
partnerAccount?: { companyName?: string; phone?: string };
|
||||||
};
|
};
|
||||||
@@ -36,15 +38,17 @@ type Row = {
|
|||||||
type PartnerOption = { id: string; companyName: string; phone?: string };
|
type PartnerOption = { id: string; companyName: string; phone?: string };
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
DRAFT: '草稿',
|
DRAFT: '待合伙人确认',
|
||||||
CONFIRMED: '已确认',
|
CONFIRMED: '待打款审核',
|
||||||
PAID: '已打款',
|
PAID: '已打款',
|
||||||
|
REJECTED: '已驳回',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
DRAFT: 'default',
|
DRAFT: 'default',
|
||||||
CONFIRMED: 'blue',
|
CONFIRMED: 'orange',
|
||||||
PAID: 'green',
|
PAID: 'green',
|
||||||
|
REJECTED: 'red',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PartnerBillsPage() {
|
export default function PartnerBillsPage() {
|
||||||
@@ -66,6 +70,10 @@ export default function PartnerBillsPage() {
|
|||||||
const [genOpen, setGenOpen] = useState(false);
|
const [genOpen, setGenOpen] = useState(false);
|
||||||
const [genForm] = Form.useForm();
|
const [genForm] = Form.useForm();
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const [rejectOpen, setRejectOpen] = useState(false);
|
||||||
|
const [rejectTarget, setRejectTarget] = useState<Row | null>(null);
|
||||||
|
const [rejectForm] = Form.useForm();
|
||||||
|
const [rejecting, setRejecting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||||
@@ -95,7 +103,7 @@ export default function PartnerBillsPage() {
|
|||||||
|
|
||||||
async function confirmBill(id: string) {
|
async function confirmBill(id: string) {
|
||||||
await request(`/admin/partner-bills/${id}/confirm`, { method: 'POST' });
|
await request(`/admin/partner-bills/${id}/confirm`, { method: 'POST' });
|
||||||
message.success('已确认');
|
message.success('已代确认,进入待打款审核');
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +112,33 @@ export default function PartnerBillsPage() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ paymentRef: `PAY-${Date.now()}` }),
|
body: JSON.stringify({ paymentRef: `PAY-${Date.now()}` }),
|
||||||
});
|
});
|
||||||
message.success('已标记打款');
|
message.success('已通过并标记打款');
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openReject(row: Row) {
|
||||||
|
setRejectTarget(row);
|
||||||
|
rejectForm.resetFields();
|
||||||
|
setRejectOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitReject(values: { reason: string }) {
|
||||||
|
if (!rejectTarget) return;
|
||||||
|
setRejecting(true);
|
||||||
|
try {
|
||||||
|
await request(`/admin/partner-bills/${rejectTarget.id}/reject`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ reason: values.reason.trim() }),
|
||||||
|
});
|
||||||
|
message.success('已驳回打款申请');
|
||||||
|
setRejectOpen(false);
|
||||||
|
setRejectTarget(null);
|
||||||
|
reload();
|
||||||
|
} finally {
|
||||||
|
setRejecting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function exportExcel() {
|
async function exportExcel() {
|
||||||
setExporting(true);
|
setExporting(true);
|
||||||
try {
|
try {
|
||||||
@@ -163,24 +194,38 @@ export default function PartnerBillsPage() {
|
|||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
width: 90,
|
width: 120,
|
||||||
render: (s) => <Tag color={STATUS_COLORS[s] || 'default'}>{STATUS_LABELS[s] || s}</Tag>,
|
render: (s, row) => (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Tag color={STATUS_COLORS[s] || 'default'}>{STATUS_LABELS[s] || s}</Tag>
|
||||||
|
{s === 'REJECTED' && row.rejectReason ? (
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{row.rejectReason}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 160,
|
width: 220,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={0}>
|
<Space size={0} wrap>
|
||||||
{row.status === 'DRAFT' && (
|
{row.status === 'DRAFT' && (
|
||||||
<Button type="link" size="small" onClick={() => void confirmBill(row.id)}>
|
<Button type="link" size="small" onClick={() => void confirmBill(row.id)}>
|
||||||
确认
|
代确认
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{row.status === 'CONFIRMED' && (
|
{row.status === 'CONFIRMED' && (
|
||||||
<Button type="link" size="small" onClick={() => void markPaid(row.id)}>
|
<>
|
||||||
标记打款
|
<Button type="link" size="small" onClick={() => void markPaid(row.id)}>
|
||||||
</Button>
|
通过打款
|
||||||
|
</Button>
|
||||||
|
<Button type="link" size="small" danger onClick={() => openReject(row)}>
|
||||||
|
驳回
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -194,7 +239,7 @@ export default function PartnerBillsPage() {
|
|||||||
合伙人账单
|
合伙人账单
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
T+30 结算:按自然月汇总酒单佣金(酒单价 × 酒单佣金比例)与门店核销佣金(核销额 × 核销佣金比例)
|
T+30 结算:合伙人确认并申请打款后,总部在此审核通过或驳回(驳回须填写理由)
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
@@ -242,7 +287,7 @@ export default function PartnerBillsPage() {
|
|||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="状态">
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
style={{ width: 120 }}
|
style={{ width: 140 }}
|
||||||
options={Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
options={Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -280,7 +325,7 @@ export default function PartnerBillsPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1280 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -320,6 +365,38 @@ export default function PartnerBillsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="驳回打款申请"
|
||||||
|
open={rejectOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setRejectOpen(false);
|
||||||
|
setRejectTarget(null);
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||||
|
账单 {rejectTarget?.billNo} · {rejectTarget ? partnerName(rejectTarget) : ''}
|
||||||
|
<br />
|
||||||
|
驳回理由将展示在合伙人 H5 账单页。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Form form={rejectForm} layout="vertical" onFinish={(v) => void submitReject(v)}>
|
||||||
|
<Form.Item
|
||||||
|
name="reason"
|
||||||
|
label="驳回理由"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请填写驳回理由' },
|
||||||
|
{ max: 500, message: '不超过 500 字' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={4} placeholder="请说明驳回原因,便于合伙人核对后重新申请" maxLength={500} showCount />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" danger htmlType="submit" block loading={rejecting}>
|
||||||
|
确认驳回
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import CenterPage from './pages/CenterPage';
|
|||||||
import PartnerMePage from './pages/PartnerMePage';
|
import PartnerMePage from './pages/PartnerMePage';
|
||||||
import BillsPage from './pages/BillsPage';
|
import BillsPage from './pages/BillsPage';
|
||||||
import SettlementPage from './pages/SettlementPage';
|
import SettlementPage from './pages/SettlementPage';
|
||||||
import ReshipPage from './pages/ReshipPage';
|
|
||||||
import WeeklyReportPage from './pages/WeeklyReportPage';
|
import WeeklyReportPage from './pages/WeeklyReportPage';
|
||||||
import ProxyOrderPage from './pages/ProxyOrderPage';
|
import ProxyOrderPage from './pages/ProxyOrderPage';
|
||||||
import StaffListPage from './pages/StaffListPage';
|
import StaffListPage from './pages/StaffListPage';
|
||||||
@@ -34,7 +33,6 @@ function PrimaryRoutes() {
|
|||||||
<Route path="/center/staff" element={<StaffListPage />} />
|
<Route path="/center/staff" element={<StaffListPage />} />
|
||||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||||
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
||||||
<Route path="/reshipments" element={<ReshipPage />} />
|
|
||||||
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
||||||
<Route path="/leaderboard" element={<LeaderboardPage />} />
|
<Route path="/leaderboard" element={<LeaderboardPage />} />
|
||||||
<Route path="/stores/:id" element={<StoreDetailPage />} />
|
<Route path="/stores/:id" element={<StoreDetailPage />} />
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ function accountFromProfile(profile: PartnerSessionProfile): PartnerAccount {
|
|||||||
hasWechat: profile.hasWechat,
|
hasWechat: profile.hasWechat,
|
||||||
wxNickname: profile.wxNickname,
|
wxNickname: profile.wxNickname,
|
||||||
wxAvatarUrl: profile.wxAvatarUrl,
|
wxAvatarUrl: profile.wxAvatarUrl,
|
||||||
|
managedWarehouseId: profile.managedWarehouseId,
|
||||||
|
hasWarehouseAccess: profile.hasWarehouseAccess,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export type PartnerSessionProfile = Pick<
|
|||||||
| 'hasWechat'
|
| 'hasWechat'
|
||||||
| 'wxNickname'
|
| 'wxNickname'
|
||||||
| 'wxAvatarUrl'
|
| 'wxAvatarUrl'
|
||||||
|
| 'managedWarehouseId'
|
||||||
|
| 'hasWarehouseAccess'
|
||||||
> & {
|
> & {
|
||||||
staffRole?: PartnerStaffRole;
|
staffRole?: PartnerStaffRole;
|
||||||
};
|
};
|
||||||
@@ -125,6 +127,8 @@ function profileFromMe(me: PartnerMe): PartnerSessionProfile {
|
|||||||
hasWechat: me.hasWechat,
|
hasWechat: me.hasWechat,
|
||||||
wxNickname: me.wxNickname,
|
wxNickname: me.wxNickname,
|
||||||
wxAvatarUrl: me.wxAvatarUrl,
|
wxAvatarUrl: me.wxAvatarUrl,
|
||||||
|
managedWarehouseId: me.managedWarehouseId,
|
||||||
|
hasWarehouseAccess: me.hasWarehouseAccess,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ export function getPartnerNavKind(account: PartnerMe | null | undefined): Partne
|
|||||||
return 'store_staff';
|
return 'store_staff';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否已配置管仓:主账号看 hasWarehouseAccess;子账号还需有仓库相关权限 */
|
||||||
|
export function hasWarehouseAccess(account: PartnerMe | null | undefined): boolean {
|
||||||
|
if (!account) return false;
|
||||||
|
if (account.hasWarehouseAccess === false) return false;
|
||||||
|
if (account.hasWarehouseAccess === true) return true;
|
||||||
|
// 旧缓存无字段时保守:主账号未知视为无,避免误展示全城订单
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function partnerHomePath(account: PartnerMe | null | undefined): string {
|
export function partnerHomePath(account: PartnerMe | null | undefined): string {
|
||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,96 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
|
import { toastError, toastSuccess } from '../lib/toast';
|
||||||
|
|
||||||
|
function fmtMoney(n: number) {
|
||||||
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function billStatusLabel(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'DRAFT': return '待确认';
|
||||||
|
case 'CONFIRMED': return '审核中';
|
||||||
|
case 'PAID': return '已结算';
|
||||||
|
case 'REJECTED': return '已驳回';
|
||||||
|
default: return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canApplyPayment(status: string) {
|
||||||
|
return status === 'DRAFT' || status === 'REJECTED';
|
||||||
|
}
|
||||||
|
|
||||||
export default function BillsPage() {
|
export default function BillsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [bills, setBills] = useState<Array<Record<string, unknown>>>([]);
|
const [bills, setBills] = useState<PartnerBillDto[]>([]);
|
||||||
const [confirmed, setConfirmed] = useState(false);
|
const [confirmed, setConfirmed] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
async function loadBills() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const list = await request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills');
|
||||||
|
setBills(Array.isArray(list) ? list : []);
|
||||||
|
} catch {
|
||||||
|
setBills([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||||
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
|
void loadBills();
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const billId = searchParams.get('id');
|
const billId = searchParams.get('id');
|
||||||
|
const actionable = useMemo(
|
||||||
|
() => bills.filter((b) => canApplyPayment(b.status)),
|
||||||
|
[bills],
|
||||||
|
);
|
||||||
const bill = billId
|
const bill = billId
|
||||||
? bills.find((b) => String(b.id) === billId) ?? bills[0]
|
? bills.find((b) => String(b.id) === billId) ?? actionable[0] ?? bills[0]
|
||||||
: bills[0];
|
: actionable[0] ?? bills[0];
|
||||||
|
|
||||||
function confirmBill() {
|
const status = String(bill?.status || '');
|
||||||
if (!confirmed) return;
|
const showApplyForm = !!bill && canApplyPayment(status);
|
||||||
|
const isReviewing = status === 'CONFIRMED';
|
||||||
|
const isRejected = status === 'REJECTED';
|
||||||
|
const isPaid = status === 'PAID';
|
||||||
|
|
||||||
|
async function confirmBill() {
|
||||||
|
if (!bill || !confirmed || !canApplyPayment(String(bill.status))) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setTimeout(() => {
|
try {
|
||||||
|
await request('PARTNER_H5', `/partner/settlement/bills/${bill.id}/confirm`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
toastSuccess('申请已提交,请耐心等待总部打款审核');
|
||||||
|
setConfirmed(false);
|
||||||
|
await loadBills();
|
||||||
|
} catch (e) {
|
||||||
|
toastError(e instanceof Error ? e.message : '提交失败');
|
||||||
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
alert('申请已提交,请耐心等待打款审核。(preV1 演示)');
|
}
|
||||||
}, 1500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="partner-bills-page">
|
<div className="partner-bills-page">
|
||||||
<PageHeader title="待确认账单" onBack={() => navigate('/center/settlement')} />
|
<PageHeader title="账单确认" onBack={() => navigate('/center/settlement')} />
|
||||||
|
|
||||||
<div className="partner-bill-stepper">
|
<div className="partner-bill-stepper">
|
||||||
<div className="partner-stepper-inner">
|
<div className="partner-stepper-inner">
|
||||||
<div className="partner-stepper-line" aria-hidden>
|
<div className="partner-stepper-line" aria-hidden>
|
||||||
<div className="partner-stepper-line-fill" style={{ width: '50%' }} />
|
<div
|
||||||
|
className="partner-stepper-line-fill"
|
||||||
|
style={{ width: isPaid ? '100%' : isReviewing || isRejected ? '75%' : '50%' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="partner-step">
|
<div className="partner-step">
|
||||||
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
|
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
|
||||||
@@ -45,17 +99,28 @@ export default function BillsPage() {
|
|||||||
<span className="partner-step-label partner-step-label--active">数据核算</span>
|
<span className="partner-step-label partner-step-label--active">数据核算</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="partner-step">
|
<div className="partner-step">
|
||||||
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--active">2</div>
|
<div className={`partner-step-circle partner-step-circle--sm${showApplyForm || isReviewing || isRejected || isPaid ? ' partner-step-circle--done' : ' partner-step-circle--active'}`}>
|
||||||
|
{showApplyForm && !confirmed ? '2' : (
|
||||||
|
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span className="partner-step-label partner-step-label--active">账单确认</span>
|
<span className="partner-step-label partner-step-label--active">账单确认</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="partner-step">
|
<div className="partner-step">
|
||||||
<div className="partner-step-circle partner-step-circle--sm">3</div>
|
<div className={`partner-step-circle partner-step-circle--sm${isPaid ? ' partner-step-circle--done' : isReviewing || isRejected ? ' partner-step-circle--active' : ''}`}>
|
||||||
<span className="partner-step-label">申请打款</span>
|
{isPaid ? (
|
||||||
|
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
||||||
|
) : '3'}
|
||||||
|
</div>
|
||||||
|
<span className={`partner-step-label${isReviewing || isRejected || isPaid ? ' partner-step-label--active' : ''}`}>
|
||||||
|
{isRejected ? '已驳回' : isPaid ? '已打款' : '申请打款'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!bill && <div className="empty">暂无账单</div>}
|
{loading && <div className="empty">加载中…</div>}
|
||||||
|
{!loading && !bill && <div className="empty">暂无账单</div>}
|
||||||
|
|
||||||
{bill && (
|
{bill && (
|
||||||
<section className="partner-bill-card">
|
<section className="partner-bill-card">
|
||||||
@@ -66,25 +131,46 @@ export default function BillsPage() {
|
|||||||
<p className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>SETTLEMENT PERIOD</p>
|
<p className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>SETTLEMENT PERIOD</p>
|
||||||
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '月度结算账单')}</h2>
|
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '月度结算账单')}</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className="partner-status-pill partner-status-pill--paused">{String(bill.status || '待确认')}</span>
|
<span className={`partner-status-pill${isRejected ? ' partner-status-pill--closed' : isReviewing ? ' partner-status-pill--paused' : isPaid ? ' partner-status-pill--open' : ' partner-status-pill--paused'}`}>
|
||||||
|
{billStatusLabel(status)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isRejected && bill.rejectReason && (
|
||||||
|
<div className="partner-info-banner" style={{ marginBottom: 16, background: 'rgba(166,29,36,0.06)' }}>
|
||||||
|
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
|
||||||
|
<div>
|
||||||
|
<p className="body-md text-primary" style={{ fontWeight: 600, marginBottom: 4 }}>打款申请已驳回</p>
|
||||||
|
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>{bill.rejectReason}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isReviewing && (
|
||||||
|
<div className="partner-info-banner" style={{ marginBottom: 16 }}>
|
||||||
|
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>hourglass_top</span>
|
||||||
|
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
||||||
|
您已提交打款申请,总部审核中,请耐心等待。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="partner-bill-amount">
|
<div className="partner-bill-amount">
|
||||||
<p className="text-muted body-md" style={{ marginBottom: 8 }}>应结总金额</p>
|
<p className="text-muted body-md" style={{ marginBottom: 8 }}>应结总金额</p>
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
|
||||||
<span className="text-primary" style={{ fontWeight: 700, fontSize: 20 }}>¥</span>
|
<span className="text-primary" style={{ fontWeight: 700, fontSize: 20 }}>¥</span>
|
||||||
<span className="amount-xl">{Number(bill.totalAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
|
<span className="amount-xl">{fmtMoney(Number(bill.totalAmount || 0))}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
<div className="partner-info-row">
|
<div className="partner-info-row">
|
||||||
<span className="text-variant body-md">订单分佣收入</span>
|
<span className="text-variant body-md">订单分佣收入</span>
|
||||||
<span className="body-md" style={{ fontWeight: 700 }}>¥ {(Number(bill.totalAmount || 0) * 0.82).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
|
<span className="body-md" style={{ fontWeight: 700 }}>¥ {fmtMoney(Number(bill.orderCommission || 0))}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="partner-info-row">
|
<div className="partner-info-row">
|
||||||
<span className="text-variant body-md">核销权益分佣</span>
|
<span className="text-variant body-md">核销权益分佣</span>
|
||||||
<span className="body-md" style={{ fontWeight: 700 }}>¥ {(Number(bill.totalAmount || 0) * 0.18).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
|
<span className="body-md" style={{ fontWeight: 700 }}>¥ {fmtMoney(Number(bill.redeemCommission || 0))}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,45 +179,58 @@ export default function BillsPage() {
|
|||||||
|
|
||||||
{bills.length > 1 && (
|
{bills.length > 1 && (
|
||||||
<div style={{ padding: '0 20px' }}>
|
<div style={{ padding: '0 20px' }}>
|
||||||
<h3 className="headline-md" style={{ marginBottom: 12 }}>历史账单</h3>
|
<h3 className="headline-md" style={{ marginBottom: 12 }}>全部账单</h3>
|
||||||
{bills.slice(1).map((b) => (
|
{bills.map((b) => (
|
||||||
<div key={String(b.id)} className="partner-store-card" style={{ margin: '0 0 12px' }}>
|
<button
|
||||||
|
key={String(b.id)}
|
||||||
|
type="button"
|
||||||
|
className="partner-store-card"
|
||||||
|
style={{ margin: '0 0 12px', width: '100%', textAlign: 'left', cursor: 'pointer' }}
|
||||||
|
onClick={() => navigate(`/center/bills?id=${b.id}`)}
|
||||||
|
>
|
||||||
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
|
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
|
||||||
<div>
|
<div>
|
||||||
<p className="body-md">{String(b.billNo)}</p>
|
<p className="body-md">{b.billNo}</p>
|
||||||
<p className="label-md text-muted">{String(b.status)}</p>
|
<p className="label-md text-muted">{billStatusLabel(b.status)}</p>
|
||||||
|
{b.status === 'REJECTED' && b.rejectReason ? (
|
||||||
|
<p className="label-md text-primary" style={{ marginTop: 4 }}>驳回:{b.rejectReason}</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="amount-lg" style={{ fontSize: 18 }}>¥{Number(b.totalAmount).toFixed(2)}</span>
|
<span className="amount-lg" style={{ fontSize: 18 }}>¥{Number(b.totalAmount).toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="partner-info-banner" style={{ marginTop: 16 }}>
|
{showApplyForm && (
|
||||||
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>info</span>
|
<>
|
||||||
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
<div className="partner-info-banner" style={{ marginTop: 16 }}>
|
||||||
账单确认后将正式进入打款流程。如有异议,请在确认前联系城市运营经理核实数据。
|
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>info</span>
|
||||||
</p>
|
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
||||||
</div>
|
{isRejected
|
||||||
|
? '请根据驳回理由核对后重新确认并申请打款。'
|
||||||
|
: '账单确认后将正式进入打款审核。如有异议,请在确认前联系城市运营经理核实数据。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{bill && (
|
<footer className="partner-bill-footer">
|
||||||
<footer className="partner-bill-footer">
|
<label className="partner-checkbox-row" style={{ marginBottom: 16 }}>
|
||||||
<label className="partner-checkbox-row" style={{ marginBottom: 16 }}>
|
<input type="checkbox" checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)} />
|
||||||
<input type="checkbox" checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)} />
|
<span>我已核对数据无误,同意根据此账单进行结算</span>
|
||||||
<span>我已核对数据无误,同意根据此账单进行结算</span>
|
</label>
|
||||||
</label>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="partner-btn-primary"
|
||||||
className="partner-btn-primary"
|
disabled={!confirmed || submitting}
|
||||||
disabled={!confirmed || submitting}
|
style={{ opacity: confirmed ? 1 : 0.5 }}
|
||||||
style={{ opacity: confirmed ? 1 : 0.5 }}
|
onClick={() => void confirmBill()}
|
||||||
onClick={confirmBill}
|
>
|
||||||
>
|
{submitting ? '正在提交...' : isRejected ? '重新申请打款' : '确认并申请打款'}
|
||||||
{submitting ? '正在提交...' : '确认并申请打款'}
|
{!submitting && <span className="material-symbols-outlined">payments</span>}
|
||||||
{!submitting && <span className="material-symbols-outlined">payments</span>}
|
</button>
|
||||||
</button>
|
</footer>
|
||||||
</footer>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,12 +53,16 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
}, [isPrimary]);
|
}, [isPrimary]);
|
||||||
|
|
||||||
const finance = useMemo(() => {
|
const finance = useMemo(() => {
|
||||||
const pending = bills.filter((b) => b.status === 'DRAFT');
|
const pending = bills.filter((b) => b.status === 'DRAFT' || b.status === 'REJECTED');
|
||||||
const settled = bills.filter((b) => b.status === 'PAID');
|
const settled = bills.filter((b) => b.status === 'PAID');
|
||||||
const paidCount = settled.length;
|
const paidCount = settled.length;
|
||||||
|
const rejectedCount = bills.filter((b) => b.status === 'REJECTED').length;
|
||||||
return {
|
return {
|
||||||
pendingBillCount: pending.length,
|
pendingBillCount: pending.length,
|
||||||
pendingTotal: pending.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
rejectedCount,
|
||||||
|
pendingTotal: bills
|
||||||
|
.filter((b) => b.status === 'DRAFT' || b.status === 'CONFIRMED' || b.status === 'REJECTED')
|
||||||
|
.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
balance: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
balance: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
paidCount,
|
paidCount,
|
||||||
@@ -180,9 +184,13 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
<span className="material-symbols-outlined">receipt_long</span>
|
<span className="material-symbols-outlined">receipt_long</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="body-md text-primary" style={{ fontWeight: 600 }}>待确认账单</p>
|
<p className="body-md text-primary" style={{ fontWeight: 600 }}>
|
||||||
|
{finance.rejectedCount > 0 ? '待处理账单' : '待确认账单'}
|
||||||
|
</p>
|
||||||
<p className="label-md text-primary" style={{ opacity: 0.85 }}>
|
<p className="label-md text-primary" style={{ opacity: 0.85 }}>
|
||||||
您有 {finance.pendingBillCount} 笔账单待确认
|
{finance.rejectedCount > 0
|
||||||
|
? `您有 ${finance.pendingBillCount} 笔账单待处理(含 ${finance.rejectedCount} 笔已驳回)`
|
||||||
|
: `您有 ${finance.pendingBillCount} 笔账单待确认`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
||||||
import { getPartnerNavKind } from '../lib/partnerAccess';
|
import { getPartnerNavKind, hasWarehouseAccess } from '../lib/partnerAccess';
|
||||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
|
|
||||||
function fmtMoney(n: number) {
|
function fmtMoney(n: number) {
|
||||||
@@ -135,6 +135,7 @@ export default function HomePage() {
|
|||||||
const navKind = getPartnerNavKind(account);
|
const navKind = getPartnerNavKind(account);
|
||||||
const isPrimary = navKind === 'primary';
|
const isPrimary = navKind === 'primary';
|
||||||
const isWarehouse = navKind === 'warehouse_staff';
|
const isWarehouse = navKind === 'warehouse_staff';
|
||||||
|
const warehouseOk = hasWarehouseAccess(account);
|
||||||
|
|
||||||
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
||||||
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
|
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
|
||||||
@@ -147,10 +148,12 @@ export default function HomePage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||||
if (isWarehouse || isPrimary) {
|
if ((isWarehouse || isPrimary) && warehouseOk) {
|
||||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders')
|
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders')
|
||||||
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
||||||
.catch(() => setOrders([]));
|
.catch(() => setOrders([]));
|
||||||
|
} else {
|
||||||
|
setOrders([]);
|
||||||
}
|
}
|
||||||
if (!isWarehouse) {
|
if (!isWarehouse) {
|
||||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
||||||
@@ -165,7 +168,7 @@ export default function HomePage() {
|
|||||||
} else {
|
} else {
|
||||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
||||||
}
|
}
|
||||||
}, [navigate, isWarehouse, isPrimary]);
|
}, [navigate, isWarehouse, isPrimary, warehouseOk]);
|
||||||
|
|
||||||
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
||||||
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
||||||
@@ -190,7 +193,6 @@ export default function HomePage() {
|
|||||||
const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0);
|
const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0);
|
||||||
const revenue = orderCount * 128.45;
|
const revenue = orderCount * 128.45;
|
||||||
const profit = revenue * 0.25;
|
const profit = revenue * 0.25;
|
||||||
const pendingShipBadge = orderStats.pendingShip;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page partner-home partner-home--flush-top">
|
<div className="page partner-home partner-home--flush-top">
|
||||||
@@ -224,13 +226,6 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="partner-quick-action-label">录入新店</span>
|
<span className="partner-quick-action-label">录入新店</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/reshipments" className="partner-quick-action">
|
|
||||||
<div className="partner-quick-action-icon partner-quick-action-icon--amber">
|
|
||||||
<span className="material-symbols-outlined">assignment_return</span>
|
|
||||||
{pendingShipBadge > 0 && <span className="partner-quick-badge-count">{pendingShipBadge}</span>}
|
|
||||||
</div>
|
|
||||||
<span className="partner-quick-action-label">补发处理</span>
|
|
||||||
</Link>
|
|
||||||
<Link to="/center/settlement" className="partner-quick-action">
|
<Link to="/center/settlement" className="partner-quick-action">
|
||||||
<div className="partner-quick-action-icon partner-quick-action-icon--green">
|
<div className="partner-quick-action-icon partner-quick-action-icon--green">
|
||||||
<span className="material-symbols-outlined">account_balance</span>
|
<span className="material-symbols-outlined">account_balance</span>
|
||||||
@@ -252,7 +247,7 @@ export default function HomePage() {
|
|||||||
<p className="headline-md" style={{ marginBottom: 8 }}>订单管理</p>
|
<p className="headline-md" style={{ marginBottom: 8 }}>订单管理</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isPrimary || isWarehouse) && (
|
{(isPrimary || isWarehouse) && warehouseOk && (
|
||||||
<OrderSummarySection
|
<OrderSummarySection
|
||||||
title={isWarehouse ? '今日订单' : undefined}
|
title={isWarehouse ? '今日订单' : undefined}
|
||||||
todayCount={orderStats.todayCount}
|
todayCount={orderStats.todayCount}
|
||||||
@@ -262,6 +257,18 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(isPrimary || isWarehouse) && !warehouseOk && (
|
||||||
|
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||||
|
<div className="partner-warehouse-denied">
|
||||||
|
<span className="material-symbols-outlined text-primary">warehouse</span>
|
||||||
|
<p className="body-md" style={{ fontWeight: 600, marginTop: 8 }}>未配置仓库管理权限</p>
|
||||||
|
<p className="label-md text-muted" style={{ marginTop: 4, lineHeight: 1.5 }}>
|
||||||
|
购酒订单由总部履约,不会推送到本账号。如需管仓发货,请联系总部配置仓库。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isWarehouse && (
|
{!isWarehouse && (
|
||||||
<>
|
<>
|
||||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
|
import { hasWarehouseAccess } from '../lib/partnerAccess';
|
||||||
|
|
||||||
type DateFilter = 'today' | '7d' | '30d';
|
type DateFilter = 'today' | '7d' | '30d';
|
||||||
type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL';
|
type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL';
|
||||||
@@ -34,15 +36,24 @@ type OrderListPageProps = {
|
|||||||
tabRoot?: boolean;
|
tabRoot?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OrdersResponse = {
|
||||||
|
list: Array<Record<string, unknown>>;
|
||||||
|
hasWarehouseAccess?: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [data, setData] = useState<{ list: Array<Record<string, unknown>> }>({ list: [] });
|
const { account } = usePartnerSession();
|
||||||
|
const warehouseOk = hasWarehouseAccess(account);
|
||||||
|
const [data, setData] = useState<OrdersResponse>({ list: [] });
|
||||||
const [tab, setTab] = useState<'orders' | 'coupons'>('orders');
|
const [tab, setTab] = useState<'orders' | 'coupons'>('orders');
|
||||||
const [dateFilter, setDateFilter] = useState<DateFilter>('today');
|
const [dateFilter, setDateFilter] = useState<DateFilter>('today');
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
|
||||||
const [shippingId, setShippingId] = useState<string | null>(null);
|
const [shippingId, setShippingId] = useState<string | null>(null);
|
||||||
const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
|
const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
|
||||||
const [shipModalId, setShipModalId] = useState<string | null>(null);
|
const [shipModalId, setShipModalId] = useState<string | null>(null);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = '订单管理';
|
document.title = '订单管理';
|
||||||
@@ -50,8 +61,22 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders').then(setData);
|
if (!warehouseOk) {
|
||||||
}, [navigate]);
|
setData({ list: [], hasWarehouseAccess: false, message: '未配置仓库管理权限' });
|
||||||
|
setLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request<OrdersResponse>('PARTNER_H5', '/partner/orders')
|
||||||
|
.then((res) => {
|
||||||
|
setData({
|
||||||
|
list: Array.isArray(res.list) ? res.list : [],
|
||||||
|
hasWarehouseAccess: res.hasWarehouseAccess,
|
||||||
|
message: res.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => setData({ list: [] }))
|
||||||
|
.finally(() => setLoaded(true));
|
||||||
|
}, [navigate, warehouseOk]);
|
||||||
|
|
||||||
const filtered = useMemo(() => data.list.filter((o) => {
|
const filtered = useMemo(() => data.list.filter((o) => {
|
||||||
if (statusFilter === 'ALL') return true;
|
if (statusFilter === 'ALL') return true;
|
||||||
@@ -83,8 +108,12 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
body: JSON.stringify(shipForm),
|
body: JSON.stringify(shipForm),
|
||||||
});
|
});
|
||||||
setShipModalId(null);
|
setShipModalId(null);
|
||||||
const next = await request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders');
|
const next = await request<OrdersResponse>('PARTNER_H5', '/partner/orders');
|
||||||
setData(next);
|
setData({
|
||||||
|
list: Array.isArray(next.list) ? next.list : [],
|
||||||
|
hasWarehouseAccess: next.hasWarehouseAccess,
|
||||||
|
message: next.message,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.alert(err instanceof Error ? err.message : '发货失败');
|
window.alert(err instanceof Error ? err.message : '发货失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -97,6 +126,21 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|||||||
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
|
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loaded && !warehouseOk) {
|
||||||
|
return (
|
||||||
|
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
|
||||||
|
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
||||||
|
<div className="partner-warehouse-denied" style={{ margin: 20, padding: 32, textAlign: 'center' }}>
|
||||||
|
<span className="material-symbols-outlined text-primary" style={{ fontSize: 40 }}>warehouse</span>
|
||||||
|
<p className="headline-md" style={{ marginTop: 12 }}>未配置仓库管理权限</p>
|
||||||
|
<p className="body-md text-muted" style={{ marginTop: 8, lineHeight: 1.6 }}>
|
||||||
|
购酒订单由总部履约,不会推送到本账号。如需管仓发货,请联系总部配置仓库。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
|
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
|
||||||
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
|
|||||||
import type { PartnerBillDto } from '@dukang/shared-types';
|
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
|
|
||||||
type StatusFilter = 'all' | 'pending' | 'settled' | 'reviewing';
|
type StatusFilter = 'all' | 'pending' | 'settled' | 'reviewing' | 'rejected';
|
||||||
|
|
||||||
function fmtMoney(n: number) {
|
function fmtMoney(n: number) {
|
||||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
@@ -15,6 +15,7 @@ function billStatusLabel(status: string) {
|
|||||||
case 'DRAFT': return '待结算';
|
case 'DRAFT': return '待结算';
|
||||||
case 'CONFIRMED': return '审核中';
|
case 'CONFIRMED': return '审核中';
|
||||||
case 'PAID': return '已结算';
|
case 'PAID': return '已结算';
|
||||||
|
case 'REJECTED': return '已驳回';
|
||||||
default: return status;
|
default: return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ function billStatusClass(status: string) {
|
|||||||
case 'DRAFT': return 'partner-settlement-status--pending';
|
case 'DRAFT': return 'partner-settlement-status--pending';
|
||||||
case 'CONFIRMED': return 'partner-settlement-status--reviewing';
|
case 'CONFIRMED': return 'partner-settlement-status--reviewing';
|
||||||
case 'PAID': return 'partner-settlement-status--settled';
|
case 'PAID': return 'partner-settlement-status--settled';
|
||||||
|
case 'REJECTED': return 'partner-settlement-status--rejected';
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,9 +72,10 @@ export default function SettlementPage() {
|
|||||||
const matchMonth = d.getFullYear() === month.year && d.getMonth() + 1 === month.month;
|
const matchMonth = d.getFullYear() === month.year && d.getMonth() + 1 === month.month;
|
||||||
if (!matchMonth) return false;
|
if (!matchMonth) return false;
|
||||||
if (statusFilter === 'all') return true;
|
if (statusFilter === 'all') return true;
|
||||||
if (statusFilter === 'pending') return b.status === 'DRAFT';
|
if (statusFilter === 'pending') return b.status === 'DRAFT' || b.status === 'REJECTED';
|
||||||
if (statusFilter === 'settled') return b.status === 'PAID';
|
if (statusFilter === 'settled') return b.status === 'PAID';
|
||||||
if (statusFilter === 'reviewing') return b.status === 'CONFIRMED';
|
if (statusFilter === 'reviewing') return b.status === 'CONFIRMED';
|
||||||
|
if (statusFilter === 'rejected') return b.status === 'REJECTED';
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [bills, month, statusFilter]);
|
}, [bills, month, statusFilter]);
|
||||||
@@ -93,8 +96,9 @@ export default function SettlementPage() {
|
|||||||
const statusTabs: Array<{ key: StatusFilter; label: string }> = [
|
const statusTabs: Array<{ key: StatusFilter; label: string }> = [
|
||||||
{ key: 'all', label: '全部' },
|
{ key: 'all', label: '全部' },
|
||||||
{ key: 'pending', label: '待结算' },
|
{ key: 'pending', label: '待结算' },
|
||||||
{ key: 'settled', label: '已结算' },
|
|
||||||
{ key: 'reviewing', label: '审核中' },
|
{ key: 'reviewing', label: '审核中' },
|
||||||
|
{ key: 'rejected', label: '已驳回' },
|
||||||
|
{ key: 'settled', label: '已结算' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -188,7 +192,12 @@ export default function SettlementPage() {
|
|||||||
<span className="partner-settlement-dot" />
|
<span className="partner-settlement-dot" />
|
||||||
订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}
|
订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}
|
||||||
</p>
|
</p>
|
||||||
<p className={`partner-settlement-item-amount${bill.status === 'DRAFT' ? ' text-primary' : ''}`}>
|
{bill.status === 'REJECTED' && bill.rejectReason ? (
|
||||||
|
<p className="label-md text-primary" style={{ marginTop: 6 }}>
|
||||||
|
驳回:{bill.rejectReason}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className={`partner-settlement-item-amount${bill.status === 'DRAFT' || bill.status === 'REJECTED' ? ' text-primary' : ''}`}>
|
||||||
¥{fmtMoney(Number(bill.totalAmount || 0))}
|
¥{fmtMoney(Number(bill.totalAmount || 0))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2985,6 +2985,11 @@ nav.app-tabbar .app-tabbar-label {
|
|||||||
color: var(--color-success-green);
|
color: var(--color-success-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.partner-settlement-status--rejected {
|
||||||
|
background: rgba(166, 29, 36, 0.12);
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
}
|
||||||
|
|
||||||
.partner-settlement-item-meta {
|
.partner-settlement-item-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3273,8 +3278,17 @@ body {
|
|||||||
border-top: 1px dashed rgba(166, 29, 36, 0.15);
|
border-top: 1px dashed rgba(166, 29, 36, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.partner-home--flush-top .partner-home-body {
|
.partner-warehouse-denied {
|
||||||
padding-top: 20px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-warehouse-denied .material-symbols-outlined {
|
||||||
|
font-size: 36px;
|
||||||
|
font-variation-settings: 'FILL' 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.partner-home--flush-top.partner-store-page,
|
.partner-home--flush-top.partner-store-page,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const PARTNER_LOG_EVENT_CATEGORIES: Record<PartnerLogCategory, readonly s
|
|||||||
'partner_store_audit_rejected',
|
'partner_store_audit_rejected',
|
||||||
],
|
],
|
||||||
shipping: ['partner_order_ship', 'partner_delivery_advance'],
|
shipping: ['partner_order_ship', 'partner_delivery_advance'],
|
||||||
settlement: ['partner_bill_view', 'partner_bill_detail_view'],
|
settlement: ['partner_bill_view', 'partner_bill_detail_view', 'partner_bill_confirm'],
|
||||||
warehouse_ops: ['partner_warehouse_view', 'partner_warehouse_update'],
|
warehouse_ops: ['partner_warehouse_view', 'partner_warehouse_update'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export interface PartnerMe {
|
|||||||
/** 子账号联系主账号用 */
|
/** 子账号联系主账号用 */
|
||||||
primaryPhone?: string;
|
primaryPhone?: string;
|
||||||
primaryName?: string;
|
primaryName?: string;
|
||||||
|
/** 主账号绑定的管仓 ID(有则本城酒单推送至该仓) */
|
||||||
|
managedWarehouseId?: string | null;
|
||||||
|
/** 是否已配置仓库管理(无则不展示/不推送酒订单) */
|
||||||
|
hasWarehouseAccess?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdatePartnerMeRequest {
|
export interface UpdatePartnerMeRequest {
|
||||||
|
|||||||
@@ -7,21 +7,24 @@ export interface StorePayoutDto {
|
|||||||
paidAt?: string | null;
|
paidAt?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PartnerBillStatus = 'DRAFT' | 'CONFIRMED' | 'PAID' | 'REJECTED';
|
||||||
|
|
||||||
export interface PartnerBillDto {
|
export interface PartnerBillDto {
|
||||||
id: string;
|
id: string;
|
||||||
billNo: string;
|
billNo: string;
|
||||||
orderCommission: number;
|
orderCommission: number;
|
||||||
redeemCommission: number;
|
redeemCommission: number;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
status: 'DRAFT' | 'CONFIRMED' | 'PAID';
|
status: PartnerBillStatus;
|
||||||
periodStart: string;
|
periodStart: string;
|
||||||
periodEnd: string;
|
periodEnd: string;
|
||||||
|
confirmedAt?: string | null;
|
||||||
|
paidAt?: string | null;
|
||||||
|
rejectReason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PartnerBillDetailDto extends PartnerBillDto {
|
export interface PartnerBillDetailDto extends PartnerBillDto {
|
||||||
partnerId: string;
|
partnerId: string;
|
||||||
confirmedAt?: string | null;
|
|
||||||
paidAt?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 门店结算默认比例(核销金额 × 比例) */
|
/** 门店结算默认比例(核销金额 × 比例) */
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ enum PartnerBillStatus {
|
|||||||
DRAFT
|
DRAFT
|
||||||
CONFIRMED
|
CONFIRMED
|
||||||
PAID
|
PAID
|
||||||
|
REJECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StoreStatus {
|
enum StoreStatus {
|
||||||
@@ -623,6 +624,7 @@ model PartnerBill {
|
|||||||
status PartnerBillStatus @default(DRAFT)
|
status PartnerBillStatus @default(DRAFT)
|
||||||
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
||||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||||
|
rejectReason String? @map("reject_reason") @db.VarChar(500)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||||
|
|
||||||
partnerAccount PartnerAccount @relation(fields: [partnerAccountId], references: [id], onDelete: Restrict)
|
partnerAccount PartnerAccount @relation(fields: [partnerAccountId], references: [id], onDelete: Restrict)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const HqOperationAction = {
|
|||||||
PARTNER_BILL_GENERATE: 'PARTNER_BILL_GENERATE',
|
PARTNER_BILL_GENERATE: 'PARTNER_BILL_GENERATE',
|
||||||
PARTNER_BILL_CONFIRM: 'PARTNER_BILL_CONFIRM',
|
PARTNER_BILL_CONFIRM: 'PARTNER_BILL_CONFIRM',
|
||||||
PARTNER_BILL_MARK_PAID: 'PARTNER_BILL_MARK_PAID',
|
PARTNER_BILL_MARK_PAID: 'PARTNER_BILL_MARK_PAID',
|
||||||
|
PARTNER_BILL_REJECT: 'PARTNER_BILL_REJECT',
|
||||||
REDEEM_DEBUG_CREATE_TOKEN: 'REDEEM_DEBUG_CREATE_TOKEN',
|
REDEEM_DEBUG_CREATE_TOKEN: 'REDEEM_DEBUG_CREATE_TOKEN',
|
||||||
REDEEM_DEBUG_CONFIRM: 'REDEEM_DEBUG_CONFIRM',
|
REDEEM_DEBUG_CONFIRM: 'REDEEM_DEBUG_CONFIRM',
|
||||||
PROMO_CODE_CREATE: 'PROMO_CODE_CREATE',
|
PROMO_CODE_CREATE: 'PROMO_CODE_CREATE',
|
||||||
@@ -108,6 +109,7 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
|
|||||||
[HqOperationAction.PARTNER_BILL_GENERATE]: '生成合伙人账单',
|
[HqOperationAction.PARTNER_BILL_GENERATE]: '生成合伙人账单',
|
||||||
[HqOperationAction.PARTNER_BILL_CONFIRM]: '确认合伙人账单',
|
[HqOperationAction.PARTNER_BILL_CONFIRM]: '确认合伙人账单',
|
||||||
[HqOperationAction.PARTNER_BILL_MARK_PAID]: '合伙人账单结算',
|
[HqOperationAction.PARTNER_BILL_MARK_PAID]: '合伙人账单结算',
|
||||||
|
[HqOperationAction.PARTNER_BILL_REJECT]: '驳回合伙人打款申请',
|
||||||
[HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN]: '核销调试-生成码',
|
[HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN]: '核销调试-生成码',
|
||||||
[HqOperationAction.REDEEM_DEBUG_CONFIRM]: '核销调试-确认核销',
|
[HqOperationAction.REDEEM_DEBUG_CONFIRM]: '核销调试-确认核销',
|
||||||
[HqOperationAction.PROMO_CODE_CREATE]: '创建推广码',
|
[HqOperationAction.PROMO_CODE_CREATE]: '创建推广码',
|
||||||
|
|||||||
@@ -103,10 +103,16 @@ export class PartnerCityService {
|
|||||||
if (!primary.cityId) return { id: -1n };
|
if (!primary.cityId) return { id: -1n };
|
||||||
|
|
||||||
const warehouseIds = await this.resolveManagedWarehouseIds(primary.id);
|
const warehouseIds = await this.resolveManagedWarehouseIds(primary.id);
|
||||||
if (warehouseIds.length > 0) {
|
// 未配置管仓:不推送/不展示酒订单(同城无仓由总部履约)
|
||||||
return { fulfillmentWarehouseId: { in: warehouseIds } };
|
if (warehouseIds.length === 0) {
|
||||||
|
return { id: -1n };
|
||||||
}
|
}
|
||||||
return { cityId: primary.cityId };
|
return { fulfillmentWarehouseId: { in: warehouseIds } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasManagedWarehouse(partnerAccountId: bigint): Promise<boolean> {
|
||||||
|
const ids = await this.resolveManagedWarehouseIds(partnerAccountId);
|
||||||
|
return ids.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合伙人可管仓库:主账号 managedWarehouseId + 绑定为管仓合伙人的仓 */
|
/** 合伙人可管仓库:主账号 managedWarehouseId + 绑定为管仓合伙人的仓 */
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class AdminDashboardService {
|
|||||||
deliveriesTotal,
|
deliveriesTotal,
|
||||||
pendingPayouts,
|
pendingPayouts,
|
||||||
pendingBills,
|
pendingBills,
|
||||||
|
pendingPartnerDraftBills,
|
||||||
openTickets,
|
openTickets,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.prisma.user.count({ where: { status: 1, mergedIntoUserId: null } }),
|
this.prisma.user.count({ where: { status: 1, mergedIntoUserId: null } }),
|
||||||
@@ -42,7 +43,8 @@ export class AdminDashboardService {
|
|||||||
this.prisma.redeemRecord.count({ where: { createdAt: { gte: todayStart } } }),
|
this.prisma.redeemRecord.count({ where: { createdAt: { gte: todayStart } } }),
|
||||||
this.prisma.orderDelivery.count(),
|
this.prisma.orderDelivery.count(),
|
||||||
this.prisma.storePayout.count({ where: { status: 'PENDING' } }),
|
this.prisma.storePayout.count({ where: { status: 'PENDING' } }),
|
||||||
this.prisma.partnerBill.count({ where: { status: { in: ['DRAFT', 'CONFIRMED'] } } }),
|
this.prisma.partnerBill.count({ where: { status: 'CONFIRMED' } }),
|
||||||
|
this.prisma.partnerBill.count({ where: { status: 'DRAFT' } }),
|
||||||
this.prisma.commonTicket.count({ where: { status: { in: ['PENDING', 'OPEN'] } } }),
|
this.prisma.commonTicket.count({ where: { status: { in: ['PENDING', 'OPEN'] } } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -57,7 +59,9 @@ export class AdminDashboardService {
|
|||||||
redeemToday,
|
redeemToday,
|
||||||
deliveriesTotal,
|
deliveriesTotal,
|
||||||
pendingPayouts,
|
pendingPayouts,
|
||||||
|
/** 合伙人已确认、待总部审核打款 */
|
||||||
pendingBills,
|
pendingBills,
|
||||||
|
pendingPartnerDraftBills,
|
||||||
openTickets,
|
openTickets,
|
||||||
ordersByStatus: ordersByStatus.map((row) => ({
|
ordersByStatus: ordersByStatus.map((row) => ({
|
||||||
status: row.status,
|
status: row.status,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
|||||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
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 { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||||
|
|
||||||
class UpdatePartnerMeDto {
|
class UpdatePartnerMeDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -25,6 +26,11 @@ export class SettlementController {
|
|||||||
bills(@CurrentUser() user: AuthUser) {
|
bills(@CurrentUser() user: AuthUser) {
|
||||||
return this.settlementService.listPartnerBills(user.actorId);
|
return this.settlementService.listPartnerBills(user.actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('bills/:id/confirm')
|
||||||
|
confirm(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||||
|
return this.settlementService.partnerConfirmBill(user.actorId, BigInt(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('shop/payouts')
|
@Controller('shop/payouts')
|
||||||
@@ -193,6 +199,20 @@ export class AdminPartnerBillController {
|
|||||||
markPaid(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
|
markPaid(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
|
||||||
return this.settlementService.markPartnerBillPaid(BigInt(id), body);
|
return this.settlementService.markPartnerBillPaid(BigInt(id), body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':id/reject')
|
||||||
|
@HqOperation({
|
||||||
|
action: HqOperationAction.PARTNER_BILL_REJECT,
|
||||||
|
refType: 'PARTNER_BILL',
|
||||||
|
refIdParam: 'id',
|
||||||
|
includeBody: true,
|
||||||
|
})
|
||||||
|
reject(@Param('id') id: string, @Body() body: { reason?: string }) {
|
||||||
|
if (!body?.reason?.trim()) {
|
||||||
|
throw new BadRequestException('请填写驳回理由');
|
||||||
|
}
|
||||||
|
return this.settlementService.rejectPartnerBill(BigInt(id), body.reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('admin/winery-bills')
|
@Controller('admin/winery-bills')
|
||||||
@@ -224,7 +244,10 @@ export class AdminWineryBillController {
|
|||||||
@Controller('partner/me')
|
@Controller('partner/me')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class PartnerMeController {
|
export class PartnerMeController {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly partnerCityService: PartnerCityService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async me(@CurrentUser() user: AuthUser) {
|
async me(@CurrentUser() user: AuthUser) {
|
||||||
@@ -255,6 +278,7 @@ export class PartnerMeController {
|
|||||||
where: { id: account.parentAccountId },
|
where: { id: account.parentAccountId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
|
||||||
return {
|
return {
|
||||||
id: account.id.toString(),
|
id: account.id.toString(),
|
||||||
name: account.name,
|
name: account.name,
|
||||||
@@ -269,6 +293,8 @@ export class PartnerMeController {
|
|||||||
hasWechat: !!account.wxOpenId,
|
hasWechat: !!account.wxOpenId,
|
||||||
wxNickname: account.wxNickname ?? undefined,
|
wxNickname: account.wxNickname ?? undefined,
|
||||||
wxAvatarUrl: account.wxAvatarUrl ?? undefined,
|
wxAvatarUrl: account.wxAvatarUrl ?? undefined,
|
||||||
|
managedWarehouseId: primary.managedWarehouseId?.toString() ?? null,
|
||||||
|
hasWarehouseAccess,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -529,11 +529,70 @@ export class SettlementService {
|
|||||||
async confirmPartnerBill(id: bigint) {
|
async confirmPartnerBill(id: bigint) {
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
if (bill.status !== 'DRAFT') throw new BadRequestException('仅草稿可确认');
|
if (bill.status !== 'DRAFT' && bill.status !== 'REJECTED') {
|
||||||
|
throw new BadRequestException('仅待确认或已驳回账单可确认');
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
const updated = await this.prisma.partnerBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'CONFIRMED', confirmedAt: new Date() },
|
data: {
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
rejectReason: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return serializeBigInt(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 合伙人端:确认账单并申请打款(DRAFT/REJECTED → CONFIRMED) */
|
||||||
|
async partnerConfirmBill(partnerAccountId: bigint, billId: bigint) {
|
||||||
|
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||||
|
const bill = await this.prisma.partnerBill.findUnique({ where: { id: billId } });
|
||||||
|
if (!bill || bill.partnerAccountId !== primary.id) {
|
||||||
|
throw new NotFoundException('账单不存在');
|
||||||
|
}
|
||||||
|
if (bill.status !== 'DRAFT' && bill.status !== 'REJECTED') {
|
||||||
|
throw new BadRequestException('当前账单不可再次申请打款');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.prisma.partnerBill.update({
|
||||||
|
where: { id: billId },
|
||||||
|
data: {
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
rejectReason: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||||
|
partnerAccountId: primary.id,
|
||||||
|
eventName: 'partner_bill_confirm',
|
||||||
|
refType: 'PARTNER_BILL',
|
||||||
|
refId: billId,
|
||||||
|
extraJson: { billNo: bill.billNo },
|
||||||
|
});
|
||||||
|
|
||||||
|
return serializeBigInt(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectPartnerBill(id: bigint, reason: string) {
|
||||||
|
const rejectReason = reason.trim();
|
||||||
|
if (!rejectReason) throw new BadRequestException('请填写驳回理由');
|
||||||
|
if (rejectReason.length > 500) throw new BadRequestException('驳回理由不能超过 500 字');
|
||||||
|
|
||||||
|
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
||||||
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
|
if (bill.status !== 'CONFIRMED') {
|
||||||
|
throw new BadRequestException('仅待打款审核的账单可驳回');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.prisma.partnerBill.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
status: 'REJECTED',
|
||||||
|
rejectReason,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
@@ -542,11 +601,11 @@ export class SettlementService {
|
|||||||
async markPartnerBillPaid(id: bigint, dto: { paymentRef?: string }) {
|
async markPartnerBillPaid(id: bigint, dto: { paymentRef?: string }) {
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
if (bill.status !== 'CONFIRMED') throw new BadRequestException('仅已确认账单可标记打款');
|
if (bill.status !== 'CONFIRMED') throw new BadRequestException('仅待打款审核的账单可标记打款');
|
||||||
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
const updated = await this.prisma.partnerBill.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: 'PAID', paidAt: new Date() },
|
data: { status: 'PAID', paidAt: new Date(), rejectReason: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
@@ -577,6 +636,7 @@ export class SettlementService {
|
|||||||
'状态',
|
'状态',
|
||||||
'确认时间',
|
'确认时间',
|
||||||
'打款时间',
|
'打款时间',
|
||||||
|
'驳回理由',
|
||||||
].join(',');
|
].join(',');
|
||||||
const rows = bills.map((b) =>
|
const rows = bills.map((b) =>
|
||||||
[
|
[
|
||||||
@@ -591,6 +651,7 @@ export class SettlementService {
|
|||||||
b.status,
|
b.status,
|
||||||
b.confirmedAt ? b.confirmedAt.toISOString().slice(0, 10) : '',
|
b.confirmedAt ? b.confirmedAt.toISOString().slice(0, 10) : '',
|
||||||
b.paidAt ? b.paidAt.toISOString().slice(0, 10) : '',
|
b.paidAt ? b.paidAt.toISOString().slice(0, 10) : '',
|
||||||
|
csvEscape(b.rejectReason ?? ''),
|
||||||
].join(','),
|
].join(','),
|
||||||
);
|
);
|
||||||
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
|
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
|
||||||
|
|||||||
@@ -509,6 +509,7 @@ export class TradeService {
|
|||||||
|
|
||||||
async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) {
|
async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) {
|
||||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||||
|
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
|
||||||
const where = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
const where = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||||
const [list, total] = await Promise.all([
|
const [list, total] = await Promise.all([
|
||||||
this.prisma.order.findMany({
|
this.prisma.order.findMany({
|
||||||
@@ -520,7 +521,14 @@ export class TradeService {
|
|||||||
}),
|
}),
|
||||||
this.prisma.order.count({ where }),
|
this.prisma.order.count({ where }),
|
||||||
]);
|
]);
|
||||||
return { list: serializeBigInt(list.map(mapOrderCompat)), total, page, pageSize };
|
return {
|
||||||
|
list: serializeBigInt(list.map(mapOrderCompat)),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
hasWarehouseAccess,
|
||||||
|
message: hasWarehouseAccess ? undefined : '未配置仓库管理权限,购酒订单由总部履约',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) {
|
async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) {
|
||||||
|
|||||||
Reference in New Issue
Block a user