feat: bills expense type filter (backend join + frontend Select), sync stub docs
This commit is contained in:
@@ -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')}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user