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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user