feat: bills expense type filter (backend join + frontend Select), sync stub docs

This commit is contained in:
2026-07-06 14:48:47 +08:00
parent 4b02012634
commit 20f3b79179
4 changed files with 20 additions and 9 deletions

View File

@@ -48,12 +48,16 @@ const BillsPage: React.FC = () => {
const [selectedRows, setSelectedRows] = useState<number[]>([]); const [selectedRows, setSelectedRows] = useState<number[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined); const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
const [filterExpenseType, setFilterExpenseType] = useState<string | undefined>(undefined);
const [generateForm] = Form.useForm(); const [generateForm] = Form.useForm();
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const res: any = await api.get('/bills'); const params: Record<string, string | undefined> = {};
if (filterStatus) params.status = filterStatus;
if (filterExpenseType) params.expenseType = filterExpenseType;
const res = await api.get('/bills', { params }) as unknown[];
setBills(res); setBills(res);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -63,7 +67,7 @@ const BillsPage: React.FC = () => {
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, []); }, [filterStatus, filterExpenseType]);
const filteredBills = useMemo(() => { const filteredBills = useMemo(() => {
return bills.filter((b: any) => { return bills.filter((b: any) => {
@@ -332,6 +336,8 @@ const BillsPage: React.FC = () => {
{ value: 'paid', label: '已支付' }, { value: 'paid', label: '已支付' },
]} ]}
/> />
<Select placeholder="费用类型" allowClear style={{ width: 120 }} value={filterExpenseType} onChange={setFilterExpenseType}
options={[{value:'water',label:'水费'},{value:'electricity',label:'电费'},{value:'cleaning',label:'保洁费'},{value:'rent',label:'租金'},{value:'other',label:'其他'}]} />
<PermissionButton <PermissionButton
permission="bill:confirm" permission="bill:confirm"
onClick={() => batchUpdateStatus('confirmed')} onClick={() => batchUpdateStatus('confirmed')}

View File

@@ -77,12 +77,12 @@ export class BillsController {
@Query('periodEnd') periodEnd?: string, @Query('periodEnd') periodEnd?: string,
@Query('studentId') studentId?: string, @Query('studentId') studentId?: string,
@Query('status') status?: string, @Query('status') status?: string,
@Query('expenseType') expenseType?: string,
) { ) {
return this.service.findAll({ return this.service.findAll({
periodStart, periodStart, periodEnd,
periodEnd,
studentId: studentId ? +studentId : undefined, studentId: studentId ? +studentId : undefined,
status, status, expenseType,
}); });
} }

View File

@@ -227,6 +227,7 @@ export class BillsService {
periodEnd?: string; periodEnd?: string;
studentId?: number; studentId?: number;
status?: string; status?: string;
expenseType?: string;
}) { }) {
const scopeIds = await this.scope.getScopeDepartmentIds(); const scopeIds = await this.scope.getScopeDepartmentIds();
const qb = this.billRepo const qb = this.billRepo
@@ -238,6 +239,9 @@ export class BillsService {
if (query?.periodEnd) qb.andWhere('b.periodEnd = :pe', { pe: query.periodEnd }); if (query?.periodEnd) qb.andWhere('b.periodEnd = :pe', { pe: query.periodEnd });
if (query?.studentId) qb.andWhere('b.studentId = :sid', { sid: query.studentId }); if (query?.studentId) qb.andWhere('b.studentId = :sid', { sid: query.studentId });
if (query?.status) qb.andWhere('b.status = :status', { status: query.status }); if (query?.status) qb.andWhere('b.status = :status', { status: query.status });
if (query?.expenseType) {
qb.innerJoin('b.items', 'bi', 'bi.expenseType = :et', { et: query.expenseType });
}
const bills = await qb.getMany(); const bills = await qb.getMany();
return this.attachDepositInfo(bills); return this.attachDepositInfo(bills);
} }

View File

@@ -157,10 +157,11 @@ export class SyncService {
this.logger.warn('DingTalk not configured (DINGTALK_APP_KEY missing), skipping sync'); this.logger.warn('DingTalk not configured (DINGTALK_APP_KEY missing), skipping sync');
return 0; return 0;
} }
// 接入指引:
this.logger.warn( // 1. 创建 apps/server/src/integration/dingtalk.service.ts实现 fetchDepartments()/fetchUsers()
'DingTalk integration service not yet implemented — add DingTalkService to SyncModule to enable real sync', // 2. 在 SyncModule 中注入 DingTalkService
); // 3. 取消以下注释并调用 this.dingTalkService.fetchDepartments()
this.logger.warn('DingTalk sync stub: create DingTalkService in src/integration/ to enable real sync');
return 0; return 0;
} }