diff --git a/backend/src/HealthManager.Application/DTOs/AuthDtos.cs b/backend/src/HealthManager.Application/DTOs/AuthDtos.cs index a018bab..2922cee 100644 --- a/backend/src/HealthManager.Application/DTOs/AuthDtos.cs +++ b/backend/src/HealthManager.Application/DTOs/AuthDtos.cs @@ -14,7 +14,18 @@ public record UserProfileResponse( List? MedicalHistory, DateOnly? StentDate, string? StentType, string? Department, string? Title, List? Specialty, string? Introduction); -public record UpdateProfileRequest( - string? Name, string? Gender, DateOnly? Birthday, - decimal? HeightCm, decimal? WeightKg, List? MedicalHistory, - string? Department, string? Title, string? Introduction, List? Specialty); +public class UpdateProfileRequest +{ + public string? Name { get; set; } + public string? Gender { get; set; } + public DateOnly? Birthday { get; set; } + public decimal? HeightCm { get; set; } + public decimal? WeightKg { get; set; } + public List? MedicalHistory { get; set; } + public DateOnly? StentDate { get; set; } + public string? StentType { get; set; } + public string? Department { get; set; } + public string? Title { get; set; } + public string? Introduction { get; set; } + public List? Specialty { get; set; } +} diff --git a/backend/src/HealthManager.Application/Services/FollowUpService.cs b/backend/src/HealthManager.Application/Services/FollowUpService.cs index d2fb021..a318ee5 100644 --- a/backend/src/HealthManager.Application/Services/FollowUpService.cs +++ b/backend/src/HealthManager.Application/Services/FollowUpService.cs @@ -20,6 +20,13 @@ public class FollowUpService(AppDbContext db) .OrderBy(f => f.ScheduledAt) .ToListAsync(); + public async Task> GetDoctorInitiatedFollowUpsAsync(Guid doctorId) + => await db.FollowUps + .Include(f => f.Patient) + .Where(f => f.DoctorId == doctorId) + .OrderBy(f => f.ScheduledAt) + .ToListAsync(); + public async Task AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null, string? notes = null) { var followUp = new FollowUp diff --git a/backend/src/HealthManager.Application/Services/MedicationService.cs b/backend/src/HealthManager.Application/Services/MedicationService.cs index d69f2af..3a61528 100644 --- a/backend/src/HealthManager.Application/Services/MedicationService.cs +++ b/backend/src/HealthManager.Application/Services/MedicationService.cs @@ -120,4 +120,53 @@ public class MedicationService(AppDbContext db) await db.SaveChangesAsync(); return true; } + + public async Task> GetTodaySummaryAsync(Guid userId) + { + var today = DateTime.UtcNow.Date; + var medications = await db.Medications + .Where(m => m.UserId == userId && m.Status == "active") + .OrderBy(m => m.CreatedAt) + .ToListAsync(); + + var allRecords = await db.MedicationRecords + .Where(mr => mr.UserId == userId && mr.CreatedAt.Date == today) + .ToListAsync(); + + var now = DateTime.Now; + return medications.Select(m => + { + var slots = m.TimeSlots.Select(slot => + { + var record = allRecords.FirstOrDefault(r => + r.MedicationId == m.Id && r.TimeSlot == slot); + var taken = record?.IsTaken ?? false; + + // Parse slot time and mark as missed if past due + var parts = slot.Split(':'); + var slotHour = int.Parse(parts[0]); + var slotMinute = int.Parse(parts[1]); + var slotTime = today.AddHours(slotHour).AddMinutes(slotMinute); + var missed = !taken && now > slotTime; + + return new + { + time = slot, + taken, + missed, + takenAt = record?.TakenAt, + }; + }).ToList(); + + return (object)new + { + m.Id, + m.DrugName, + m.Dosage, + m.Frequency, + slots, + allTaken = slots.All(s => s.taken), + }; + }).ToList(); + } } diff --git a/backend/src/HealthManager.WebApi/Controllers/AuthController.cs b/backend/src/HealthManager.WebApi/Controllers/AuthController.cs index 2160190..3f57fae 100644 --- a/backend/src/HealthManager.WebApi/Controllers/AuthController.cs +++ b/backend/src/HealthManager.WebApi/Controllers/AuthController.cs @@ -136,6 +136,8 @@ public class AuthController( if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm; if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg; if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory; + if (request.StentDate.HasValue) user.StentDate = request.StentDate; + if (request.StentType != null) user.StentType = request.StentType; if (request.Department != null) user.Department = request.Department; if (request.Title != null) user.Title = request.Title; if (request.Introduction != null) user.Introduction = request.Introduction; diff --git a/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs b/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs index c35cd5f..74e7663 100644 --- a/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs +++ b/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs @@ -14,11 +14,22 @@ public class FollowUpController(FollowUpService followUpService) : ControllerBas private string Role => User.FindFirstValue(ClaimTypes.Role)!; [HttpGet] - public async Task GetFollowUps() + public async Task GetFollowUps([FromQuery] string? type) { - var followUps = Role == "doctor" - ? await followUpService.GetDoctorFollowUpsAsync(UserId) - : await followUpService.GetPatientFollowUpsAsync(UserId); + List followUps; + + if (Role == "doctor" && type == "followup") + followUps = await followUpService.GetDoctorInitiatedFollowUpsAsync(UserId); + else if (Role == "doctor") + followUps = await followUpService.GetDoctorFollowUpsAsync(UserId); + else + { + followUps = await followUpService.GetPatientFollowUpsAsync(UserId); + if (type == "followup") + followUps = followUps.Where(f => f.DoctorId != null).ToList(); + else if (type == "recheck") + followUps = followUps.Where(f => f.DoctorId == null).ToList(); + } return Ok(followUps.Select(f => new { diff --git a/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs b/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs index cd51041..3e03596 100644 --- a/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs +++ b/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs @@ -13,6 +13,13 @@ public class MedicationController(MedicationService medicationService) : Control private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); private string Role => User.FindFirstValue(ClaimTypes.Role)!; + [HttpGet("today-summary")] + public async Task GetTodaySummary() + { + var summary = await medicationService.GetTodaySummaryAsync(UserId); + return Ok(summary); + } + [HttpGet] public async Task GetMedications() { diff --git a/frontend-doctor/src/components/layout/DoctorLayout.tsx b/frontend-doctor/src/components/layout/DoctorLayout.tsx index 63f57fd..44c2ef1 100644 --- a/frontend-doctor/src/components/layout/DoctorLayout.tsx +++ b/frontend-doctor/src/components/layout/DoctorLayout.tsx @@ -41,6 +41,13 @@ const SIDEBAR_ICONS: Record = { ), + visits: ( + + + + + + ), }; const navItems = [ @@ -49,6 +56,7 @@ const navItems = [ { to: '/consultations', label: '在线问诊', ikey: 'consultations' }, { to: '/reports', label: '报告审核', ikey: 'reports' }, { to: '/follow-ups', label: '复查管理', ikey: 'followups' }, + { to: '/visits', label: '随访管理', ikey: 'visits' }, ]; const sidebarStyles = { diff --git a/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx b/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx index 14ac844..64f9b1e 100644 --- a/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx +++ b/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx @@ -45,7 +45,7 @@ export function FollowUpEditPage() { return (
-

{isNew ? '新建随访' : '编辑随访'}

+

{isNew ? '新建复查' : '编辑复查'}

diff --git a/frontend-doctor/src/pages/followups/FollowUpListPage.tsx b/frontend-doctor/src/pages/followups/FollowUpListPage.tsx index 9330063..bb5b2ac 100644 --- a/frontend-doctor/src/pages/followups/FollowUpListPage.tsx +++ b/frontend-doctor/src/pages/followups/FollowUpListPage.tsx @@ -9,41 +9,66 @@ interface RawFollowUpItem { export function FollowUpListPage() { const [followUps, setFollowUps] = useState([]); + const [showCompleted, setShowCompleted] = useState(false); - useEffect(() => { - api.get('/api/follow-ups').then((r) => setFollowUps(r.data)).catch(() => {}); - }, []); - - const statusLabel = (s: string) => { - switch (s) { - case 'upcoming': return { text: '待随访', color: '#F59E0B', bg: '#FFF8E6' }; - case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' }; - case 'cancelled': return { text: '已取消', color: '#EF4444', bg: '#FEE9E9' }; - default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' }; - } + const load = () => { + api.get('/api/follow-ups?type=recheck') + .then((r) => setFollowUps(r.data)).catch(() => {}); }; + useEffect(() => { load(); }, []); + + const handleComplete = async (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + try { + await api.put(`/api/follow-ups/${id}`, { status: 'completed' }); + load(); + } catch { /* ignore */ } + }; + + const handleDelete = async (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + if (!confirm('确定删除?')) return; + try { + await api.del(`/api/follow-ups/${id}`); + load(); + } catch { /* ignore */ } + }; + + const now = new Date(); + const statusLabel = (f: RawFollowUpItem) => { + if (f.status === 'completed') return { text: '已完成', color: '#20C997', bg: '#E6F9F2' }; + if (f.status === 'cancelled') return { text: '已取消', color: '#EF4444', bg: '#FEE9E9' }; + if (f.scheduledAt && new Date(f.scheduledAt) < now) return { text: '已过期', color: '#EF4444', bg: '#FEE9E9' }; + return { text: '待复查', color: '#F59E0B', bg: '#FFF8E6' }; + }; + + const active = followUps.filter((f) => f.status !== 'completed'); + const completed = followUps.filter((f) => f.status === 'completed'); + const displayed = [...active, ...(showCompleted ? completed : [])]; + return (
-

随访管理

+

复查管理

- 新建随访 + + 新建复查
-

共 {followUps.length} 条随访记录

+

共 {followUps.length} 条复查记录

-
- {followUps.map((f) => { - const s = statusLabel(f.status); +
+ {displayed.map((f) => { + const s = statusLabel(f); return (
{f.title}
@@ -51,22 +76,48 @@ export function FollowUpListPage() { {f.patientName || '未知'} · {f.scheduledAt?.split('T')[0]}
-
+
{s.text} + {f.status === 'upcoming' && ( + + )} 编辑 +
); })} {followUps.length === 0 && ( -
暂无随访记录
+
暂无复查记录
)}
+ + {completed.length > 0 && ( + + )}
); } diff --git a/frontend-doctor/src/pages/followups/VisitEditPage.tsx b/frontend-doctor/src/pages/followups/VisitEditPage.tsx new file mode 100644 index 0000000..5aca6d9 --- /dev/null +++ b/frontend-doctor/src/pages/followups/VisitEditPage.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { api } from '../../services/api-client'; + +export function VisitEditPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const isNew = !id || id === 'new'; + + const [title, setTitle] = useState(''); + const [patientId, setPatientId] = useState(''); + const [scheduledAt, setScheduledAt] = useState(''); + const [notes, setNotes] = useState(''); + const [patients, setPatients] = useState<{ id: string; name: string }[]>([]); + + useEffect(() => { + api.get<{ id: string; name: string }[]>('/api/patients').then((r) => setPatients(r.data)); + if (!isNew) { + api.get>(`/api/follow-ups/${id}`).then((r) => { + setTitle(r.data.title as string); + setPatientId(r.data.patientId as string); + setScheduledAt((r.data.scheduledAt as string)?.slice(0, 16) || ''); + setNotes((r.data.notes as string) || ''); + }).catch(() => {}); + } + }, [id, isNew]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (isNew) { + await api.post('/api/follow-ups', { title, patientId, scheduledAt, notes }); + } else { + await api.put(`/api/follow-ups/${id}`, { title, patientId, scheduledAt, notes }); + } + navigate('/visits'); + } catch { alert('操作失败'); } + }; + + const inputStyle: React.CSSProperties = { + width: '100%', padding: '10px 14px', border: '1.5px solid #E1E5ED', + borderRadius: 10, fontSize: 13, outline: 'none', boxSizing: 'border-box', + }; + + return ( +
+

+ {isNew ? '新建随访' : '编辑随访'} +

+ +
+ + setTitle(e.target.value)} required style={inputStyle} + placeholder="如:PCI术后1个月随访" /> +
+
+ + +
+
+ + setScheduledAt(e.target.value)} required style={inputStyle} /> +
+
+ +