feat: refine admin forms, attendance and finance workflows
Squash merge PR #23. Included changes: - complete occupancy check-in required fields/default payload - improve responsive admin management pages - fix attendance edge cases and attendance period config - refine wallet/finance-related workflow handling Checks: - npm run typecheck -w apps/admin - npm run typecheck -w apps/server
This commit is contained in:
@@ -13,8 +13,18 @@ export class WalletsController {
|
||||
|
||||
@Get()
|
||||
@RequirePermission('wallet:view')
|
||||
findAll(@Query('keyword') keyword?: string, @Query('debtOnly') debtOnly?: string) {
|
||||
return this.service.findAll({ keyword, debtOnly: debtOnly === 'true' });
|
||||
findAll(
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('debtOnly') debtOnly?: string,
|
||||
@Query('roomType') roomType?: string,
|
||||
) {
|
||||
return this.service.findAll({ keyword, debtOnly: debtOnly === 'true', roomType });
|
||||
}
|
||||
|
||||
@Get('room-types')
|
||||
@RequirePermission('wallet:view')
|
||||
findRoomTypes() {
|
||||
return this.service.findRoomTypes();
|
||||
}
|
||||
|
||||
@Get('transactions')
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Bill } from '../entities/bill.entity';
|
||||
import { Student } from '../entities/student.entity';
|
||||
import { StudentWallet } from '../entities/student-wallet.entity';
|
||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
import { Room } from '../entities/room.entity';
|
||||
import { Occupancy } from '../entities/occupancy.entity';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { WalletsController } from './wallets.controller';
|
||||
import { WalletsService } from './wallets.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([StudentWallet, WalletTransaction, Student, Bill]), OperationLogsModule],
|
||||
imports: [TypeOrmModule.forFeature([StudentWallet, WalletTransaction, Student, Bill, Room, Occupancy]), OperationLogsModule],
|
||||
controllers: [WalletsController],
|
||||
providers: [WalletsService],
|
||||
exports: [WalletsService],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
import { In } from 'typeorm';
|
||||
import { BatchChangeWalletBalanceDto, ChangeWalletBalanceDto } from './dto/wallet.dto';
|
||||
import { FinancialOperationsService } from '../financial-operations/financial-operations.service';
|
||||
import { Room } from '../entities/room.entity';
|
||||
|
||||
const money = (value: number | string | null | undefined) => Number(Number(value || 0).toFixed(2));
|
||||
|
||||
@@ -21,21 +22,41 @@ export class WalletsService {
|
||||
private financialOperations?: FinancialOperationsService,
|
||||
) {}
|
||||
|
||||
async findAll(query?: { keyword?: string; debtOnly?: boolean }) {
|
||||
const students = await this.studentRepo
|
||||
async findAll(query?: { keyword?: string; debtOnly?: boolean; roomType?: string }) {
|
||||
const qb = this.studentRepo
|
||||
.createQueryBuilder('student')
|
||||
.where('student.status = :status', { status: 'active' })
|
||||
.andWhere(
|
||||
query?.keyword
|
||||
? '(student.name LIKE :keyword OR student.studentNo LIKE :keyword)'
|
||||
: '1 = 1',
|
||||
query?.keyword ? { keyword: `%${query.keyword}%` } : {},
|
||||
)
|
||||
.orderBy('student.name', 'ASC')
|
||||
.getMany();
|
||||
if (!students.length) return [];
|
||||
.leftJoin('student.occupancies', 'occupancy', 'occupancy.checkOutDate IS NULL')
|
||||
.leftJoin('occupancy.room', 'room')
|
||||
.where('student.status = :status', { status: 'active' });
|
||||
|
||||
const ids = students.map((student) => student.id);
|
||||
if (query?.keyword) {
|
||||
qb.andWhere('(student.name LIKE :keyword OR student.studentNo LIKE :keyword)', {
|
||||
keyword: `%${query.keyword}%`,
|
||||
});
|
||||
}
|
||||
if (query?.roomType) {
|
||||
qb.andWhere('room.roomType = :roomType', { roomType: query.roomType });
|
||||
}
|
||||
|
||||
const rows = await qb
|
||||
.select([
|
||||
'student.id AS studentId',
|
||||
'student.name AS studentName',
|
||||
'student.studentNo AS studentNo',
|
||||
'room.roomType AS roomType',
|
||||
'room.roomNumber AS roomNumber',
|
||||
])
|
||||
.orderBy('student.name', 'ASC')
|
||||
.getRawMany<{
|
||||
studentId: number;
|
||||
studentName: string;
|
||||
studentNo: string | null;
|
||||
roomType: string | null;
|
||||
roomNumber: string | null;
|
||||
}>();
|
||||
if (!rows.length) return [];
|
||||
|
||||
const ids = rows.map((row) => Number(row.studentId));
|
||||
const wallets = await this.walletRepo.find({ where: { studentId: In(ids) } });
|
||||
const bills = await this.dataSource.getRepository(Bill)
|
||||
.createQueryBuilder('bill')
|
||||
@@ -47,17 +68,34 @@ export class WalletsService {
|
||||
.getRawMany<{ studentId: number; outstandingAmount: string }>();
|
||||
const walletMap = new Map(wallets.map((wallet) => [wallet.studentId, wallet]));
|
||||
const debtMap = new Map(bills.map((bill) => [Number(bill.studentId), money(bill.outstandingAmount)]));
|
||||
return students
|
||||
.map((student) => ({
|
||||
studentId: student.id,
|
||||
studentName: student.name,
|
||||
studentNo: student.studentNo,
|
||||
balance: money(walletMap.get(student.id)?.balance),
|
||||
outstandingAmount: debtMap.get(student.id) || 0,
|
||||
return rows
|
||||
.map((row) => ({
|
||||
studentId: Number(row.studentId),
|
||||
studentName: row.studentName,
|
||||
studentNo: row.studentNo || undefined,
|
||||
roomType: row.roomType || undefined,
|
||||
roomNumber: row.roomNumber || undefined,
|
||||
balance: money(walletMap.get(Number(row.studentId))?.balance),
|
||||
outstandingAmount: debtMap.get(Number(row.studentId)) || 0,
|
||||
}))
|
||||
.filter((row) => !query?.debtOnly || row.outstandingAmount > 0);
|
||||
}
|
||||
|
||||
async findRoomTypes() {
|
||||
const rows = await this.dataSource
|
||||
.getRepository(Room)
|
||||
.createQueryBuilder('room')
|
||||
.innerJoin('room.occupancies', 'occupancy', 'occupancy.checkOutDate IS NULL')
|
||||
.innerJoin('occupancy.student', 'student', 'student.status = :status', { status: 'active' })
|
||||
.select('room.roomType', 'roomType')
|
||||
.where('room.roomType IS NOT NULL')
|
||||
.andWhere("room.roomType <> ''")
|
||||
.distinct(true)
|
||||
.orderBy('room.roomType', 'ASC')
|
||||
.getRawMany<{ roomType: string }>();
|
||||
return rows.map((row) => row.roomType);
|
||||
}
|
||||
|
||||
async findTransactions(studentId: number) {
|
||||
return this.transactionRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user