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:
2026-07-18 12:54:10 +00:00
parent 92d303ed01
commit 375c7ec60b
64 changed files with 5169 additions and 2404 deletions

View File

@@ -1,6 +1,27 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Row, Col, Card, Tag, Select, Statistic, Modal, Spin, Badge, Tooltip, DatePicker, Alert, Button } from 'antd';
import { HomeOutlined, UserOutlined, CalendarOutlined, BankOutlined, HistoryOutlined, ShopOutlined } from '@ant-design/icons';
import {
Row,
Col,
Card,
Tag,
Select,
Statistic,
Modal,
Spin,
Badge,
Tooltip,
DatePicker,
Alert,
Button,
} from 'antd';
import {
HomeOutlined,
UserOutlined,
CalendarOutlined,
BankOutlined,
HistoryOutlined,
ShopOutlined,
} from '@ant-design/icons';
import dayjs, { Dayjs } from 'dayjs';
import api from '../../api';
import { message } from '../../ui/app-message';
@@ -9,10 +30,14 @@ function getCardStyle(room: any): React.CSSProperties {
let base: React.CSSProperties;
if (room.status === 'maintenance') base = { background: '#f5f5f5', borderColor: '#d9d9d9' };
else if (room.currentCount === 0) base = { background: '#f6ffed', borderColor: '#b7eb8f' };
else if (room.currentCount >= room.capacity) base = { background: '#fff2f0', borderColor: '#ffccc7' };
else if (room.currentCount >= room.capacity)
base = { background: '#fff2f0', borderColor: '#ffccc7' };
else base = { background: '#e6f4ff', borderColor: '#91caff' };
if (room.organizationColor) {
return { ...base, background: `color-mix(in srgb, ${room.organizationColor} 15%, ${base.background || '#fff'} 85%)` };
return {
...base,
background: `color-mix(in srgb, ${room.organizationColor} 15%, ${base.background || '#fff'} 85%)`,
};
}
return base;
}
@@ -29,7 +54,10 @@ function getOrganizationTags(occupants: any[]) {
...new Map(
occupants
.filter((o: any) => o.organizationName)
.map((o: any) => [o.organizationId, { name: o.organizationName, color: o.organizationColor }]),
.map((o: any) => [
o.organizationId,
{ name: o.organizationName, color: o.organizationColor },
]),
).values(),
] as { name: string; color: string | null }[];
if (organizationList.length === 0) return null;
@@ -80,7 +108,8 @@ const RoomVisualPage: React.FC = () => {
const rooms = data.rooms.filter((r: any) => {
if (selectedBuilding !== 'all' && r.building !== selectedBuilding) return false;
if (selectedOrganization !== 'all' && !(r.organizationIds || []).includes(selectedOrganization)) return false;
if (selectedOrganization !== 'all' && !(r.organizationIds || []).includes(selectedOrganization))
return false;
return true;
});
@@ -138,7 +167,15 @@ const RoomVisualPage: React.FC = () => {
label: (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
{t.color && (
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: t.color, display: 'inline-block' }} />
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: t.color,
display: 'inline-block',
}}
/>
)}
{t.name}
</span>
@@ -175,17 +212,29 @@ const RoomVisualPage: React.FC = () => {
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic title="空闲房间" value={emptyRooms} styles={{ value: { color: '#34C759' } }} />
<Statistic
title="空闲房间"
value={emptyRooms}
styles={{ value: { color: '#34C759' } }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic title="可安排床位" value={availableBedsCount} styles={{ value: { color: '#007AFF' } }} />
<Statistic
title="可安排床位"
value={availableBedsCount}
styles={{ value: { color: '#007AFF' } }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic title="满员房间" value={fullRooms} styles={{ value: { color: '#FF3B30' } }} />
<Statistic
title="满员房间"
value={fullRooms}
styles={{ value: { color: '#FF3B30' } }}
/>
</Card>
</Col>
</Row>
@@ -214,13 +263,27 @@ const RoomVisualPage: React.FC = () => {
marginBottom: 8,
}}
>
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 16, fontWeight: 600, color: '#1d1d1f' }}>
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 16,
fontWeight: 600,
color: '#1d1d1f',
}}
>
{room.organizationColor && (
<span style={{
width: 10, height: 10, borderRadius: '50%',
backgroundColor: room.organizationColor, display: 'inline-block',
flexShrink: 0,
}} />
<span
style={{
width: 10,
height: 10,
borderRadius: '50%',
backgroundColor: room.organizationColor,
display: 'inline-block',
flexShrink: 0,
}}
/>
)}
{room.roomNumber}
</span>
@@ -231,7 +294,13 @@ const RoomVisualPage: React.FC = () => {
{room.floor && <span>{room.floor}F</span>}
</div>
{room.totalBeds > 0 && (
<div style={{ fontSize: 12, color: room.occupiedBeds >= room.totalBeds ? '#FF3B30' : '#34C759', marginBottom: 6 }}>
<div
style={{
fontSize: 12,
color: room.occupiedBeds >= room.totalBeds ? '#FF3B30' : '#34C759',
marginBottom: 6,
}}
>
: {room.occupiedBeds}/{room.totalBeds}
</div>
)}
@@ -259,10 +328,16 @@ const RoomVisualPage: React.FC = () => {
)}
{getOrganizationTags(room.occupants)}
{room.occupants.length > 0 && (
<div className="room-card-tag-wrapper" style={{ borderTop: '1px solid rgba(0,0,0,0.06)', paddingTop: 6 }}>
<div
className="room-card-tag-wrapper"
style={{ borderTop: '1px solid rgba(0,0,0,0.06)', paddingTop: 6 }}
>
{room.occupants.slice(0, 4).map((o: any) => (
<Tooltip key={o.studentId} title={`入住 ${o.days} 天 (${o.checkInDate} 起)`}>
<Tag style={{ margin: '0 4px 4px 0', fontSize: 12, maxWidth: '100%' }} icon={<UserOutlined />}>
<Tag
style={{ margin: '0 4px 4px 0', fontSize: 12, maxWidth: '100%' }}
icon={<UserOutlined />}
>
{o.studentName}
</Tag>
</Tooltip>