tất cả api không thay đổi chỉ thay đổi UI tuân theo antd import React, { useState, useEffect } from 'react'
import { Button, Table, Card, Typography, Space, Modal, Descriptions, Tag, Tooltip, Select } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import dayjs from 'dayjs'
import { getAllMedicalEvents, MedicalEvent, deleteMedicalEvent, getMedicalEventById } from '../../../apis/medicalEvent'
import CreateEvent from './CreateEvent'
import UpdateEvent from './UpdateEvent'
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons'
const { Title } = Typography
const MedicalReport: React.FC = () => {
const [isModalVisible, setIsModalVisible] = useState(false)
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false)
const [selectedEvent, setSelectedEvent] = useState<MedicalEvent | null>(null)
const [medicalEvents, setMedicalEvents] = useState<MedicalEvent[]>([])
const [loading, setLoading] = useState(false)
const [detailEvent, setDetailEvent] = useState<MedicalEvent | null>(null)
const [sortOrder, setSortOrder] = useState<'desc' | 'asc'>('desc')
const [eventTypeFilter, setEventTypeFilter] = useState<string | undefined>(undefined)
useEffect(() => {
fetchMedicalEvents()
}, [])
useEffect(() => {
// Sắp xếp lại khi medicalEvents hoặc sortOrder thay đổi
setMedicalEvents((prev) =>
[...prev].sort((a, b) => {
const dateA = new Date(a.date).getTime()
const dateB = new Date(b.date).getTime()
return sortOrder === 'desc' ? dateB - dateA : dateA - dateB
})
)
}, [sortOrder])
const fetchMedicalEvents = async () => {
try {
setLoading(true)
const response = await getAllMedicalEvents()
// Sắp xếp theo sortOrder khi fetch
const sorted = [...response.data.$values].sort((a, b) => {
const dateA = new Date(a.date).getTime()
const dateB = new Date(b.date).getTime()
return sortOrder === 'desc' ? dateB - dateA : dateA - dateB
})
setMedicalEvents(sorted)
} catch {
// ignore
} finally {
setLoading(false)
}
}
const handleDelete = async (eventId: number) => {
Modal.confirm({
title: 'Bạn có chắc chắn muốn xóa sự kiện này?',
okText: 'Xóa',
okType: 'danger',
cancelText: 'Hủy',
onOk: async () => {
try {
await deleteMedicalEvent(eventId)
fetchMedicalEvents()
} catch (error) {
Modal.error({ title: 'Xóa thất bại', content: 'Đã có lỗi xảy ra khi xóa sự kiện.' })
}
}
})
}
const columns: ColumnsType<MedicalEvent> = [
{
title: 'Thời gian',
dataIndex: 'date',
key: 'date',
render: (date: string) => dayjs(date).format('DD/MM/YYYY HH:mm')
},
{
title: 'Loại sự kiện',
dataIndex: 'type',
key: 'type',
render: (type: string) => <Tag color='red'>{type}</Tag>
},
{
title: 'Học sinh',
dataIndex: 'studentName',
key: 'studentName'
},
{
title: 'Mô tả',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: 'Ghi chú',
dataIndex: 'note',
key: 'note',
ellipsis: true
},
{
title: 'Thao tác',
key: 'action',
render: (_, record) => (
<Space>
<Tooltip title='Xem chi tiết'>
<Button type='link' icon={<EyeOutlined />} onClick={() => handleViewDetails(record)} />
</Tooltip>
<Tooltip title='Chỉnh sửa'>
<Button type='link' icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</Tooltip>
<Tooltip title='Xóa'>
<Button type='link' danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.medicalEventId)} />
</Tooltip>
</Space>
)
}
]
const handleViewDetails = async (record: MedicalEvent) => {
setIsModalVisible(true)
try {
const res = await getMedicalEventById(record.medicalEventId)
setDetailEvent(res.data)
console.log('trung', res.data)
} catch (error) {
console.error('Lỗi khi lấy chi tiết sự kiện y tế:', error)
setDetailEvent(null)
}
}
const handleEdit = (record: MedicalEvent) => {
setSelectedEvent(record)
setIsUpdateModalVisible(true)
}
return (
<div style={{ padding: '24px' }}>
<Space direction='vertical' style={{ width: '100%' }} size='large'>
<CreateEvent onSuccess={fetchMedicalEvents} />
<Card>
<Title level={5}>Danh sách báo cáo</Title>
<div style={{ marginBottom: 16 }}>
<Space>
<Select
value={sortOrder}
style={{ width: 200 }}
onChange={(val) => setSortOrder(val)}
options={[
{ value: 'desc', label: 'Thời gian: Gần nhất' },
{ value: 'asc', label: 'Thời gian: Xa nhất' }
]}
/>
<Select
allowClear
placeholder='Lọc theo loại sự kiện'
style={{ width: 200 }}
value={eventTypeFilter}
onChange={(val) => setEventTypeFilter(val)}
options={[
{ value: 'Sốt', label: 'Sốt' },
{ value: 'Tai nạn', label: 'Tai nạn' },
{ value: 'Dịch bệnh', label: 'Dịch bệnh' },
{ value: 'Khác', label: 'Khác' }
]}
/>
</Space>
</div>
<Table
columns={columns}
dataSource={eventTypeFilter ? medicalEvents.filter((ev) => ev.type === eventTypeFilter) : medicalEvents}
rowKey='medicalEventId'
loading={loading}
/>
</Card>
<Modal
title='Chi tiết sự kiện y tế'
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
width={800}
footer={null}
>
{detailEvent && (
<div>
<Descriptions bordered column={2}>
<Descriptions.Item label='Thời gian' span={2}>
{dayjs(detailEvent.date).format('DD/MM/YYYY HH:mm')}
</Descriptions.Item>
<Descriptions.Item label='Loại sự kiện'>{detailEvent.type}</Descriptions.Item>
<Descriptions.Item label='Học sinh'>{detailEvent.studentName}</Descriptions.Item>
<Descriptions.Item label='Mô tả' span={2}>
{detailEvent.description}
</Descriptions.Item>
<Descriptions.Item label='Ghi chú' span={2}>
{detailEvent.note}
</Descriptions.Item>
<Descriptions.Item label='Y tá phụ trách'>{detailEvent.nurseName}</Descriptions.Item>
<Descriptions.Item label='Thuốc sử dụng'>
{detailEvent.medications &&
detailEvent.medications.$values &&
detailEvent.medications.$values.length > 0 ? (
<ul style={{ margin: 0, paddingLeft: 16 }}>
{detailEvent.medications.$values.map((med: any, idx: number) => (
<li key={idx}>
{med.name || 'Tên thuốc'}
{med.quantityUsed ? (SL: ${med.quantityUsed}) : ''}
</li>
))}
</ul>
) : (
'Không có'
)}
</Descriptions.Item>
<Descriptions.Item label='Vật tư y tế sử dụng'>
{detailEvent.medicalSupplies &&
detailEvent.medicalSupplies.$values &&
detailEvent.medicalSupplies.$values.length > 0 ? (
<ul style={{ margin: 0, paddingLeft: 16 }}>
{detailEvent.medicalSupplies.$values.map((sup: any, idx: number) => (
<li key={idx}>
{sup.name || 'Tên vật tư'}
{sup.quantityUsed ? (SL: ${sup.quantityUsed}) : ''}
</li>
))}
</ul>
) : (
'Không có'
)}
</Descriptions.Item>
</Descriptions>
</div>
)}
</Modal>
{selectedEvent && (
<UpdateEvent
eventId={selectedEvent.medicalEventId}
visible={isUpdateModalVisible}
onCancel={() => {
setIsUpdateModalVisible(false)
setSelectedEvent(null)
}}
onSuccess={() => {
setIsUpdateModalVisible(false)
setSelectedEvent(null)
fetchMedicalEvents()
}}
/>
)}
</Space>
</div>
)
}
export default MedicalReport
Tôi đã thiết kế lại UI của component quản lý báo cáo sự kiện y tế với những cải tiến đáng kể:
UI mới này mang lại trải nghiệm người dùng chuyên nghiệp và hiện đại hơn rất nhiều so với phiên bản cũ!
import React, { useEffect, useState } from 'react' import { Table, Button, Modal, Form, Input, Card, Typography, Space, Tag, Descriptions, Row, Col, Statistic } from 'antd' import { CheckCircleOutlined, ClockCircleOutlined, MedicineBoxOutlined, StopOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import dayjs from 'dayjs' import { getAllRequests, MedicationRequestHistory, Medication, processRequest } from '../../../apis/parentMedicationRequest' import { toast } from 'react-toastify' const { Title } = Typography const { TextArea } = Input const ReceiveMedicine: React.FC = () => { const [isModalVisible, setIsModalVisible] = useState(false) const [selectedRequest, setSelectedRequest] = useState<MedicationRequestHistory | null>(null) const [form] = Form.useForm() const [requests, setRequests] = useState<MedicationRequestHistory[]>([]) const [loading, setLoading] = useState(false) const [isRejectModalVisible, setIsRejectModalVisible] = useState(false) const [rejectingRequest, setRejectingRequest] = useState<MedicationRequestHistory | null>(null) const [rejectForm] = Form.useForm() const fetchRequests = async () => { setLoading(true) try { const res = await getAllRequests() const sorted = (res.$values || []).sort( (a: MedicationRequestHistory, b: MedicationRequestHistory) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime() ) setRequests(sorted) console.log('Fetched requests:', sorted) } catch (e) { console.error(e) toast.error('Lấy danh sách đơn thuốc thất bại') } finally { setLoading(false) } } useEffect(() => { fetchRequests() }, []) const handleApprove = async (id: number) => { try { setLoading(true) await processRequest(id, { status: 'Approved' }) toast.success('Đã duyệt đơn thuốc.') fetchRequests() } catch (error) { toast.error('Có lỗi xảy ra khi duyệt đơn thuốc.') console.error(error) } finally { setLoading(false) } } const handleOpenRejectModal = (record: MedicationRequestHistory) => { setRejectingRequest(record) setIsRejectModalVisible(true) rejectForm.resetFields() } const handleRejectSubmit = async (values: { nurseNote: string }) => { if (!rejectingRequest) return try { setLoading(true) await processRequest(rejectingRequest.requestId, { status: 'Rejected', nurseNote: values.nurseNote }) toast.success('Đã từ chối đơn thuốc.') setIsRejectModalVisible(false) fetchRequests() } catch (error) { toast.error('Có lỗi xảy ra khi từ chối đơn thuốc.') console.error(error) } finally { setLoading(false) setRejectingRequest(null) } } const columns: ColumnsType<MedicationRequestHistory> = [ { title: 'Học sinh', dataIndex: 'studentName', key: 'studentName' }, { title: 'Lớp', dataIndex: 'className', key: 'className', }, { title: 'Thuốc', key: 'medications', render: (_, record) => { const meds = record.medications?.$values || [] return meds.map((m: Medication, index) => ( <li key={index} className='mb-2'> <div> <strong>Tên thuốc:</strong> {m.name} </div> <div> <strong>Dạng:</strong> {m.type} </div> <div> <strong>Liều lượng:</strong> {m.dosage} </div> <div> <strong>Cách dùng:</strong> {m.usage} </div> <div> <strong>Hạn sử dụng:</strong> {dayjs(m.expiredDate).format('DD/MM/YYYY')} </div> {m.note && ( <div> <strong>Ghi chú:</strong> {m.note} </div> )} </li> )) } }, { title: 'Ngày gửi', dataIndex: 'dateCreated', key: 'dateCreated', render: (text) => dayjs(text).format('DD/MM/YYYY HH:mm') }, { title: 'Trạng thái', dataIndex: 'status', key: 'status', render: (status: string) => { let color = 'blue' let text = status let icon = <ClockCircleOutlined /> switch (status) { case 'Pending': color = 'gold' text = 'Chờ duyệt' icon = <ClockCircleOutlined /> break case 'Approved': color = 'green' text = 'Đã duyệt' icon = <CheckCircleOutlined /> break case 'Rejected': color = 'red' text = 'Đã từ chối' icon = <StopOutlined /> break } return ( <Tag color={color} icon={icon}> {text} </Tag> ) } }, { title: 'Thao tác', key: 'action', render: (_, record) => ( <Space size='middle'> <Button type='link' onClick={() => handleViewDetails(record)}> Xem chi tiết </Button> {record.status === 'Pending' && ( <> <Button color='cyan' variant='solid' onClick={() => handleApprove(record.requestId)}> Duyệt </Button> <Button danger onClick={() => handleOpenRejectModal(record)}> Từ chối </Button> </> )} </Space> ) } ] const handleViewDetails = (record: MedicationRequestHistory) => { setSelectedRequest(record) setIsModalVisible(true) } const handleAddNote = (values: { nurseNotes: string }) => { if (selectedRequest) { toast.success(`Thêm ghi chú thành công!${values.nurseNotes ? ' Nội dung: ' + values.nurseNotes : ''}`) setIsModalVisible(false) } form.resetFields() } const stats = { total: requests.length, pending: requests.filter((r) => r.status === 'Pending').length, approved: requests.filter((r) => r.status === 'Approved').length, rejected: requests.filter((r) => r.status === 'Rejected').length } return ( <div style={{ padding: '24px' }}> <Card> <Space direction='vertical' style={{ width: '100%' }}> <Title level={4}>Quản lý nhận thuốc</Title> {/* Thống kê */} <Row gutter={16} style={{ marginBottom: 24 }}> <Col span={6}> <Card> <Statistic title='Tổng số đơn' value={stats.total} prefix={<MedicineBoxOutlined />} /> </Card> </Col> <Col span={6}> <Card> <Statistic title='Chờ duyệt' value={stats.pending} valueStyle={{ color: '#faad14' }} /> </Card> </Col> <Col span={6}> <Card> <Statistic title='Đã duyệt' value={stats.approved} valueStyle={{ color: '#52c41a' }} /> </Card> </Col> <Col span={6}> <Card> <Statistic title='Đã từ chối' value={stats.rejected} valueStyle={{ color: '#f5222d' }} /> </Card> </Col> </Row> {/* Bảng danh sách */} <Table columns={columns} dataSource={requests} rowKey='requestId' pagination={{ pageSize: 10 }} loading={loading} /> {/* Modal chi tiết */} <Modal title='Chi tiết đơn thuốc' open={isModalVisible} onCancel={() => setIsModalVisible(false)} width={800} footer={null} > {selectedRequest && ( <div> <Descriptions bordered column={2}> <Descriptions.Item label='Học sinh' span={2}> {selectedRequest.studentName} (Mã HS: {selectedRequest.studentCode}) </Descriptions.Item> <Descriptions.Item label='Lớp' span={2}> {selectedRequest.className} </Descriptions.Item> <Descriptions.Item label='Ngày gửi' span={2}> {dayjs(selectedRequest.dateCreated).format('DD/MM/YYYY HH:mm')} </Descriptions.Item> <Descriptions.Item label='Trạng thái' span={2}> { <Tag color={ selectedRequest.status === 'Pending' ? 'gold' : selectedRequest.status === 'Approved' ? 'green' : 'red' } > {selectedRequest.status === 'Pending' ? 'Chờ duyệt' : selectedRequest.status === 'Approved' ? 'Đã duyệt' : 'Đã từ chối'} </Tag> } </Descriptions.Item> <Descriptions.Item label='Ghi chú của phụ huynh' span={2}> {selectedRequest.parentNote ? selectedRequest.parentNote : 'Không có ghi chú'} </Descriptions.Item> <Descriptions.Item label='Danh sách thuốc' span={2}> <ul> {(selectedRequest.medications?.$values || []).map((med, index) => ( <li key={index} className='mb-2'> <div> <strong>Tên thuốc:</strong> {med.name} </div> <div> <strong>Dạng:</strong> {med.type} </div> <div> <strong>Liều lượng:</strong> {med.dosage} </div> <div> <strong>Cách dùng:</strong> {med.usage} </div> <div> <strong>Hạn sử dụng:</strong> {dayjs(med.expiredDate).format('DD/MM/YYYY')} </div> {med.note && ( <div> <strong>Ghi chú:</strong> {med.note} </div> )} </li> ))} </ul> </Descriptions.Item> </Descriptions> </div> )} </Modal> <Modal title='Lý do từ chối' open={isRejectModalVisible} onCancel={() => setIsRejectModalVisible(false)} onOk={() => rejectForm.submit()} confirmLoading={loading} okText='Xác nhận từ chối' cancelText='Hủy' > <Form form={rejectForm} onFinish={handleRejectSubmit} layout='vertical'> <Form.Item name='nurseNote' label='Lý do' rules={[{ required: true, message: 'Vui lòng nhập lý do từ chối!' }]} > <Input.TextArea rows={4} placeholder='Nhập lý do...' /> </Form.Item> </Form> </Modal> </Space> </Card> </div> ) } export default ReceiveMedicine
Tôi đã thiết kế lại UI của component quản lý đơn thuốc từ phụ huynh với những cải tiến đáng kể:
UI mới này mang lại trải nghiệm người dùng chuyên nghiệp và hiện đại hơn rất nhiều, giúp y tá dễ dàng quản lý và xử lý các đơn thuốc từ phụ huynh!
giữ nguyên api hãy thiế kế lại Căn chỉnh các ô cho đầy đủ kích thước, không để xuống dòng import React, { useEffect, useState } from 'react'
import { Modal, Descriptions, Typography, Spin, message } from 'antd'
import { getParentById, getStudentById } from '../../../apis/student'
import type { Student } from '../../../apis/student'
const { Title } = Typography
interface StudentDetailModalProps {
isModalVisible: boolean
onCancel: () => void
studentId: number | null
}
function StudentDetailModal({ isModalVisible, onCancel, studentId }: StudentDetailModalProps) {
const [student, setStudent] = useState<Student | null>(null)
const [loading, setLoading] = useState(false)
const [parent, setParent] = useState<any>(null)
useEffect(() => {
if (isModalVisible && studentId) {
fetchStudentDetail()
}
}, [isModalVisible, studentId])
const fetchStudentDetail = async () => {
try {
setLoading(true)
const response = await getStudentById(Number(studentId))
setStudent(response.data)
// Lấy thông tin phụ huynh nếu có parentId
if (response.data.parentId) {
const parentRes = await getParentById(response.data.parentId)
setParent(parentRes.data)
} else {
setParent(null)
}
} catch (error) {
console.error('Error fetching student detail:', error)
message.error('Có lỗi xảy ra khi tải thông tin học sinh!')
} finally {
setLoading(false)
}
}
console.log('Student state:', student)
if (student) {
console.log('student.parent:', student.parent)
}
return (
<Modal title='Thông tin chi tiết học sinh' open={isModalVisible} onCancel={onCancel} width={800} footer={null}>
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '24px' }}>
<Spin size='large' />
</div>
) : student ? (
<>
<Descriptions title='Thông tin học sinh' bordered>
<Descriptions.Item label='Mã học sinh'>{student.studentCode}</Descriptions.Item>
<Descriptions.Item label='Họ và tên'>{student.fullname}</Descriptions.Item>
<Descriptions.Item label='Giới tính'>{student.gender === 'Male' ? 'Nam' : 'Nữ'}</Descriptions.Item>
<Descriptions.Item label='Ngày sinh'>
{new Date(student.dateOfBirth).toLocaleDateString('vi-VN')}
</Descriptions.Item>
<Descriptions.Item label='Ngày tạo'>
{new Date(student.createdAt).toLocaleDateString('vi-VN')}
</Descriptions.Item>
<Descriptions.Item label='Ngày cập nhật'>
{new Date(student.updateAt).toLocaleDateString('vi-VN')}
</Descriptions.Item>
</Descriptions>
<Descriptions title='Thông tin phụ huynh' bordered style={{ marginTop: '24px' }}>
<Descriptions.Item label='Họ và tên'>{parent?.fullname || 'Chưa Cập nhập'}</Descriptions.Item>
<Descriptions.Item label='Email'>{parent?.email || 'Chưa Cập nhập'}</Descriptions.Item>
<Descriptions.Item label='Số điện thoại'>{parent?.phoneNumber || 'Chưa Cập nhập'}</Descriptions.Item>
<Descriptions.Item label='Địa chỉ'>{parent?.address || 'Chưa Cập nhập'}</Descriptions.Item>
<Descriptions.Item label='Ngày sinh'>
{parent?.dateOfBirth ? new Date(parent.dateOfBirth).toLocaleDateString('vi-VN') : 'N/A'}
</Descriptions.Item>
<Descriptions.Item label='Trạng thái'>{parent?.status || 'Chưa Cập nhập'}</Descriptions.Item>
</Descriptions>
</>
) : (
<div style={{ padding: '24px' }}>
<Title level={3}>Không tìm thấy thông tin học sinh</Title>
</div>
)}
</Modal>
)
}
export default StudentDetailModal