From e379c99e19a02522ab48ada2aed48e83fb685390 Mon Sep 17 00:00:00 2001 From: ponselvajeganathan <68591831+ponselvajeganathan@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:03:05 +0530 Subject: [PATCH] 1049019: Improved UI on Appointment planner --- .../AppointmentPlanner/Components/App.razor | 29 +- .../Components/Pages/About/About.razor | 142 +- .../Pages/Dashboard/Dashboard.razor | 1311 ++++++-- .../Components/Pages/Doctors/Doctors.razor | 398 ++- .../Pages/Doctors/DoctorsDetails.razor | 281 +- .../Components/Pages/Patients/Patients.razor | 159 +- .../Pages/Preference/Preference.razor | 143 +- .../Components/Pages/Schedule/Schedule.razor | 695 ++++- .../Data/AppointmentService.cs | 14 +- .../Models/AppointmentModels.cs | 327 +- .../Shared/MainLayout.razor | 55 +- .../wwwroot/css/appoinment/about.css | 213 -- .../wwwroot/css/appoinment/dashboard.css | 362 --- .../wwwroot/css/appoinment/doctordetails.css | 370 --- .../wwwroot/css/appoinment/preference.css | 63 - .../wwwroot/css/appointment/about.css | 402 +++ .../assets/.gitkeep | 0 .../assets/Icons/Doctors.svg | 0 .../assets/Icons/ThisWeek_Widget.svg | 0 .../assets/Icons/Today_Widget.svg | 0 .../assets/favicon.ico | Bin .../assets/fonts/icomoon.eot | Bin .../assets/fonts/icomoon.svg | 0 .../assets/fonts/icomoon.ttf | Bin .../assets/fonts/icomoon.woff | Bin .../assets/fonts/icons.eot | Bin .../assets/fonts/icons.svg | 0 .../assets/fonts/icons.ttf | Bin .../assets/fonts/icons.woff | Bin .../assets/images/Admin.png | Bin .../assets/images/AlexaRichardson.png | Bin .../assets/images/AmeliaEdwards.png | Bin .../assets/images/MollieCobb.png | Bin .../assets/images/NemboLukni.png | Bin .../assets/images/NoutGolstein.png | Bin .../assets/images/PaulWalker.png | Bin .../assets/images/YaraBarros.png | Bin .../assets/images/default.png | Bin .../assets/styles/bootstrap.css | 0 .../assets/styles/bootstrap.scss | 0 .../wwwroot/css/appointment}/calendar.css | 753 ++++- .../{appoinment => appointment}/common.css | 12 +- .../wwwroot/css/appointment/dashboard.css | 2700 +++++++++++++++++ .../{appoinment => appointment}/doctor.css | 468 ++- .../wwwroot/css/appointment/doctordetails.css | 345 +++ .../doctoreditdialog.css | 0 .../wwwroot/css/appointment/main.css | 340 +++ .../css/{appoinment => appointment}/mark.svg | 0 .../{appoinment => appointment}/patients.css | 163 +- .../wwwroot/css/appointment/preference.css | 245 ++ .../css/{appoinment/main.css => sidebar.css} | 89 +- .../AppointmentPlanner/wwwroot/css/site.css | 31 +- .../Pages/About/About.razor | 142 +- .../Pages/AppointmentModels.cs | 327 +- .../Pages/AppointmentService.cs | 14 +- .../Pages/Dashboard/Dashboard.razor | 1313 ++++++-- .../Pages/Doctors/Doctors.razor | 456 ++- .../Pages/Doctors/DoctorsDetails.razor | 281 +- .../Pages/Patients/Patients.razor | 167 +- .../Pages/Preference/Preference.razor | 145 +- .../Pages/Schedule/Schedule.razor | 704 ++++- .../Shared/MainLayout.razor | 57 +- .../AppointmentPlanner/Components/App.razor | 46 +- .../wwwroot/css/appoinment/about.css | 213 -- .../wwwroot/css/appoinment/dashboard.css | 362 --- .../wwwroot/css/appoinment/doctordetails.css | 370 --- .../wwwroot/css/appoinment/patients.css | 236 -- .../wwwroot/css/appoinment/preference.css | 63 - .../wwwroot/css/appointment/about.css | 402 +++ .../assets/.gitkeep | 0 .../assets/Icons/Doctors.svg | 0 .../assets/Icons/ThisWeek_Widget.svg | 0 .../assets/Icons/Today_Widget.svg | 0 .../assets/favicon.ico | Bin .../assets/fonts/icomoon.eot | Bin .../assets/fonts/icomoon.svg | 0 .../assets/fonts/icomoon.ttf | Bin .../assets/fonts/icomoon.woff | Bin .../assets/fonts/icons.eot | Bin .../assets/fonts/icons.svg | 0 .../assets/fonts/icons.ttf | Bin .../assets/fonts/icons.woff | Bin .../assets/images/Admin.png | Bin .../assets/images/AlexaRichardson.png | Bin .../assets/images/AmeliaEdwards.png | Bin .../assets/images/MollieCobb.png | Bin .../assets/images/NemboLukni.png | Bin .../assets/images/NoutGolstein.png | Bin .../assets/images/PaulWalker.png | Bin .../assets/images/YaraBarros.png | Bin .../assets/images/default.png | Bin .../assets/styles/bootstrap.css | 0 .../assets/styles/bootstrap.scss | 0 .../wwwroot/css/appointment}/calendar.css | 753 ++++- .../{appoinment => appointment}/common.css | 14 +- .../wwwroot/css/appointment/dashboard.css | 2700 +++++++++++++++++ .../{appoinment => appointment}/doctor.css | 491 ++- .../wwwroot/css/appointment/doctordetails.css | 345 +++ .../doctoreditdialog.css | 0 .../wwwroot/css/appointment/main.css | 340 +++ .../css/{appoinment => appointment}/mark.svg | 0 .../wwwroot/css/appointment/patients.css | 381 +++ .../wwwroot/css/appointment/preference.css | 245 ++ .../css/{appoinment/main.css => sidebar.css} | 361 +-- .../AppointmentPlanner/wwwroot/css/site.css | 35 +- 105 files changed, 17094 insertions(+), 3979 deletions(-) delete mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appoinment/about.css delete mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appoinment/dashboard.css delete mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appoinment/doctordetails.css delete mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appoinment/preference.css create mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appointment/about.css rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/.gitkeep (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/Icons/Doctors.svg (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/Icons/ThisWeek_Widget.svg (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/Icons/Today_Widget.svg (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/favicon.ico (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.eot (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.svg (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.ttf (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.woff (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.eot (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.svg (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.ttf (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.woff (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/Admin.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/AlexaRichardson.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/AmeliaEdwards.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/MollieCobb.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/NemboLukni.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/NoutGolstein.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/PaulWalker.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/YaraBarros.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/default.png (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/styles/bootstrap.css (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/styles/bootstrap.scss (100%) rename webapp/{wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appoinment => server/AppointmentPlanner/wwwroot/css/appointment}/calendar.css (67%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/common.css (86%) create mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appointment/dashboard.css rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/doctor.css (66%) create mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appointment/doctordetails.css rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/doctoreditdialog.css (100%) create mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appointment/main.css rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/mark.svg (100%) rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/patients.css (73%) create mode 100644 webapp/server/AppointmentPlanner/wwwroot/css/appointment/preference.css rename webapp/server/AppointmentPlanner/wwwroot/css/{appoinment/main.css => sidebar.css} (87%) delete mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appoinment/about.css delete mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appoinment/dashboard.css delete mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appoinment/doctordetails.css delete mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appoinment/patients.css delete mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appoinment/preference.css create mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment/about.css rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/.gitkeep (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/Icons/Doctors.svg (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/Icons/ThisWeek_Widget.svg (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/Icons/Today_Widget.svg (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/favicon.ico (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.eot (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.svg (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.ttf (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icomoon.woff (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.eot (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.svg (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.ttf (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/fonts/icons.woff (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/Admin.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/AlexaRichardson.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/AmeliaEdwards.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/MollieCobb.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/NemboLukni.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/NoutGolstein.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/PaulWalker.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/YaraBarros.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/images/default.png (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/styles/bootstrap.css (100%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/assets/styles/bootstrap.scss (100%) rename webapp/{server/AppointmentPlanner/wwwroot/css/appoinment => wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment}/calendar.css (67%) rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/common.css (85%) create mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment/dashboard.css rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/doctor.css (66%) create mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment/doctordetails.css rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/doctoreditdialog.css (100%) create mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment/main.css rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment => appointment}/mark.svg (100%) create mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment/patients.css create mode 100644 webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/appointment/preference.css rename webapp/wasm/AppointmentPlanner/AppointmentPlanner/wwwroot/css/{appoinment/main.css => sidebar.css} (59%) diff --git a/webapp/server/AppointmentPlanner/Components/App.razor b/webapp/server/AppointmentPlanner/Components/App.razor index 1047ffb..8be12ab 100644 --- a/webapp/server/AppointmentPlanner/Components/App.razor +++ b/webapp/server/AppointmentPlanner/Components/App.razor @@ -36,7 +36,8 @@ - Appointment Planner - Syncfusion Blazor Components Showcase App + Appointment Scheduling Dashboard | Syncfusion Blazor + @@ -45,17 +46,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -87,7 +91,6 @@ - diff --git a/webapp/server/AppointmentPlanner/Components/Pages/About/About.razor b/webapp/server/AppointmentPlanner/Components/Pages/About/About.razor index 709db50..dd112dc 100644 --- a/webapp/server/AppointmentPlanner/Components/Pages/About/About.razor +++ b/webapp/server/AppointmentPlanner/Components/Pages/About/About.razor @@ -3,91 +3,47 @@ @if (isDataLoaded) {
-
+
-
ABOUT
+

About Appointment Planner

+ @showcaseComponents.Count components
-

- This Example Clinic demo application showcases several Syncfusion Blazor UI components together in a real-world application scenario. - You can explore the source code of this application and use it as a - reference for integrating Syncfusion Blazor UI components in your applications. -

-
List of Syncfusion Blazor UI components used in this sample
-
-
- - Schedule -
-
- - Grid -
-
- - Chart -
-
- - TreeView -
-
- - ListView -
-
- - DropDownList -
-
- - DatePicker -
-
- - TimePicker -
-
- - Dialog -
-
- - Sidebar -
-
- - TextBox -
-
- - Button -
-
- - Toast -
-
- - CheckBox -
-
- - Skeleton -
-
- - AppBar -
- -
- - Message +
+
+ SYNCFUSION BLAZOR SHOWCASE +

Real clinic workflows, built with composable UI.

+

This example clinic brings scheduling, patient management, doctor availability, and operational insights together in one working application.

+
+ +
+
+

Components in this sample

See how each control supports a real workflow.

+
+
+ @foreach (var group in showcaseComponents.GroupBy(item => item.Category)) + { +
+

@group.Key

+
+ @foreach (var item in group) + { + + + +
@item.Name@item.Description
+ Docs +
+
+ } +
+
+ }
} @@ -120,19 +76,37 @@ else private bool isDataLoaded; private string aboutId; - private async void LoadData() + private readonly List showcaseComponents = new() + { + new("Scheduling", "Schedule", "sfimage-schedule", "Resource-based appointment planning.", "https://blazor.syncfusion.com/documentation/scheduler/getting-started-with-server-app"), + new("Data and analytics", "Grid", "sfimage-grid", "Searchable patient administration.", "https://blazor.syncfusion.com/documentation/datagrid/getting-started-with-server-app"), + new("Data and analytics", "Chart", "sfimage-chart", "Visual appointment trends.", "https://blazor.syncfusion.com/documentation/chart/getting-started"), + new("Forms and input", "DropDownList", "sfimage-dropdownlist", "Fast filtering and configuration.", "https://blazor.syncfusion.com/documentation/dropdown-list/getting-started-with-server-app"), + new("Forms and input", "DatePicker", "sfimage-datepicker", "Date-aware appointment forms.", "https://blazor.syncfusion.com/documentation/datepicker/getting-started-with-server-app"), + new("Forms and input", "TextBox", "sfimage-textboxes", "Validated patient and doctor fields.", "https://blazor.syncfusion.com/documentation/textbox/getting-started-with-server-app"), + new("Dialogs and feedback", "Dialog", "sfimage-dialog", "Focused add, edit, and review flows.", "https://blazor.syncfusion.com/documentation/dialog/getting-started-with-server-app"), + new("Dialogs and feedback", "Predefined Dialogs", "sf-icon-predefineddialogs", "Clear confirmation workflows.", "https://blazor.syncfusion.com/documentation/predefined-dialogs/getting-started-with-server-app"), + new("Dialogs and feedback", "Message", "sf-icon-message", "Inline status communication.", "https://blazor.syncfusion.com/documentation/message/getting-started-with-server-app"), + new("Navigation and layout", "Sidebar", "sfimage-sidebar", "Persistent application navigation.", "https://blazor.syncfusion.com/documentation/sidebar/getting-started"), + new("Navigation and layout", "Button", "sfimage-button", "Explicit workflow actions.", "https://blazor.syncfusion.com/documentation/button/getting-started-with-server-app"), + new("Loading", "Skeleton", "sf-icon-skeleton", "Comfortable loading transitions.", "https://blazor.syncfusion.com/documentation/skeleton/getting-started") + }; + + private async Task LoadData() { this.aboutId = "about"; await Task.Delay(100); isDataLoaded = true; - StateHasChanged(); + await InvokeAsync(StateHasChanged); } - protected override void OnAfterRender(bool firstRender) + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - LoadData(); + await LoadData(); } } + + private sealed record ShowcaseComponent(string Category, string Name, string Icon, string Description, string Url); } \ No newline at end of file diff --git a/webapp/server/AppointmentPlanner/Components/Pages/Dashboard/Dashboard.razor b/webapp/server/AppointmentPlanner/Components/Pages/Dashboard/Dashboard.razor index 41055c1..eabdcbd 100644 --- a/webapp/server/AppointmentPlanner/Components/Pages/Dashboard/Dashboard.razor +++ b/webapp/server/AppointmentPlanner/Components/Pages/Dashboard/Dashboard.razor @@ -4,153 +4,549 @@ @using AppointmentPlanner.Data; @using AppointmentPlanner.Models; @using System; +@using System.Linq; +@using System.Threading; +@implements IDisposable -@if(isDataLoaded){ -
-
-
-
-
-
-
- Total Appointments - Today -
- Today - @dayEventCount -
+@if (isDataLoaded) +{ +
+ + + + @if (CriticalPatients > 0) + { + + } + + + + + + + +
+
+ +
+ + + +
+ Active Patients + +
+ +
@totalPatients
+ +
0 ? "positive" : "neutral")" title="vs previous week" aria-label="@(totalPatientsTrend < 0 ? "Down" : totalPatientsTrend > 0 ? "Up" : "No change") @Math.Abs(totalPatientsTrend)% this week"> + @(totalPatientsTrend < 0 ? "↓" : totalPatientsTrend > 0 ? "↑" : "→") @Math.Abs(totalPatientsTrend)% this week +
+ + + + + + +
+
+ + + + +
+ Waiting + +
+ +
@WaitingCount
+ +
+ @TotalPatientsInPeriod in active view +
+ + + +
+
+ + + + +
+ Doctors Available + +
+ +
@ConsultationCount/@doctorCount
+ +
+ @doctorUtilization% utilisation now +
+ + + +
+
+ + + + +
+ Completed Visits + +
+ +
@CompletedCount/@TotalPatientsInPeriod
+ +
0 ? "positive" : "neutral")" title="vs previous week" aria-label="@(appointmentsTrend < 0 ? "Down" : appointmentsTrend > 0 ? "Up" : "No change") @Math.Abs(appointmentsTrend)% throughput"> + @(appointmentsTrend < 0 ? "↓" : appointmentsTrend > 0 ? "↑" : "→") @Math.Abs(appointmentsTrend)% throughput +
+ + + +
+
+
+ + +
+ +
+
+

Daily Appointment Stats

+
+ + + + + + + + + + + + + + + + + + + + + +
-
-
-
-
- Total Appointments - This Week -
- ThisWeek - @weekEventCount -
+ + +
+
+

Appointment Stats

+
+ + + + + + + + + + + + + + + + + + + + +
-
-
-
-
-
-
- Today's Appointments - Book Appointments - -
-
+ + +
+
+
+

Patient Overview

+
+ @if (dominantSegment != null) + { + + @dominantSegment.Category @dominantShare% + + } +
-
- - - - - - - - - - +
+
+ + + + + + + + +
+
@donutTotal
+
Patients
+ @foreach (var seg in donutData) + { + var idx = donutData.IndexOf(seg); + var segShare = donutTotal > 0 + ? (int)Math.Round(seg.Value * 100.0 / Math.Max(1, donutTotal)) + : 0; +
+ @seg.Value + @seg.Category + @segShare% +
+ }
-
-
-
-
- - - - - - - - - - - - - - - - - - - +
+ + +
+
+
+

Patients

+
+ @foreach (var t in patientTabs) + { + var tabName = t; + + } +
+
+
+ @if (filteredPatients.Count() == 0) + { +
+ No patients in this @activeTab view.
+ Try switching the period tabs or .
+ } + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+

Upcoming Appointments

+
+ + @currentMonthLabel +
+
+ @{ + var hasFutureDay = weekDays.Any(d => d.Date.Date >= Today.Date); + } + @if (!hasFutureDay) + { +
No upcoming days in this week.
+ } + else + { + @for (int i = 0; i < weekDays.Count; i++) + { + var dayIndex = i; + var d = weekDays[dayIndex]; + var isPast = d.Date.Date < Today.Date; + if (isPast) continue; + + } + } +
+
+ @if (upcomingAppointments.Count == 0) + { +
+ No appointments for this day. + Pick another day or use › to move to next week. +
+ } + else + { +
    + @for (var idx = 0; idx < upcomingAppointments.Count; idx++) + { + var a = upcomingAppointments[idx]; + var itemKey = idx; +
  • + +
    +
    @a.Name
    +
    @a.Time · @a.Dept
    +
    +
    + @if (a.IsCheckedIn) + { + ✓ Checked in + } + else + { + + } +
    +
  • + } +
+ } +
-
-
- - - - - - +
+ + @if (callingName != null) + { +
+ + Connecting call to @callingName… +
+ } + @if (showAI) + { + + -
+ } } -else{ -
-
-
-
-
- @for(int i=0; i < 10; i++) - { - -
- -

- } +else +{ +
+
+
+
+
+
} @@ -159,89 +555,652 @@ else{ [Inject] protected AppointmentService Service { get; set; } - private string dayEventCount { get; set; } - private string weekEventCount { get; set; } - private List appointments { get; set; } = new List(); + [Inject] + protected NavigationManager Urihelper { get; set; } + string lastUpdated => DateTime.Now.ToString("hh:mm:ss tt"); + private bool showAI; + + // ---- Today (live system date) ---- + private DateTime _today = DateTime.Today; + private DateTime Today + { + get => _today; + set + { + if (value < new DateTime(1900, 1, 1)) value = new DateTime(1900, 1, 1); + if (value > new DateTime(2999, 12, 1)) value = new DateTime(2999, 12, 1); + _today = value; + } + } + private string currentMonthShort => Today.ToString("MMM yyyy"); + private string currentMonthLabel => weekAnchor.ToString("MMMM"); + + public class TrendPoint + { + public int Value { get; set; } + } - private List activities { get; set; } - public List availableDoctors { get; set; } + private List patientSpark = + [ + new(){Value=10}, + new(){Value=14}, + new(){Value=8}, + new(){Value=16}, + new(){Value=18}, + new(){Value=22}, + new(){Value=27} + ]; - private List patients { get; set; } + private void CallUpcoming(Upcoming a) + { + if (a == null) return; + callingName = a.Name; + // Hide the toast after 3 seconds + _ = Task.Run(async () => { await Task.Delay(3000); callingName = null; await InvokeAsync(StateHasChanged); }); + StateHasChanged(); + } - private DateTime startDate { get; set; } = new DateTime(2020, 2, 5, 0, 0, 0, 0); + private string callingName; - private DateTime endDate { get; set; } + private void StartNextConsultation() + { + var next = allPatients + .Where(p => p.AppointmentDate.Date == Today.Date) + .Where(p => !checkedInUpcomingIds.Contains(p.HospitalId) && !completedUpcomingIds.Contains(p.HospitalId)) + .OrderBy(p => p.AppointmentDate) + .FirstOrDefault(); + + if (next != null) + { + CheckInPatient(next); + callingName = next.Name; // reuse the toast as a status confirmation + _ = Task.Run(async () => { await Task.Delay(3000); callingName = null; await InvokeAsync(StateHasChanged); }); + } + activeTab = "Daily"; + } - private DateTime firstDayOfWeek { get; set; } - private DateTime max { get; set; } + private int CriticalPatients => + Service?.Patients? + .Count(p => GetAge(p.DOB) >= 65) + ?? 0; - private List chartData { get; set; } - private List chartData1 { get; set; } - private List chartData2 { get; set; } - private bool isDataLoaded; + private int WaitingCount => + // Only patients who have not been checked in / completed are actually + filteredPatients.Count(p => !p.IsCheckedIn && !p.IsCompleted); + private int CriticalCount => + filteredPatients.Count(p => p.Age >= 60); - protected override void OnInitialized() + private int ConsultationCount => + Service?.Doctors?.Count(d => + d.Availability?.ToLower() == "available") ?? 0; + + private int CompletedCount => + // Visits actually marked completed in the active view. + filteredPatients.Count(p => p.IsCompleted); + + private void CheckInPatient(PatientRow row) + { + if (row == null || row.IsCheckedIn) return; + row.IsCheckedIn = true; + if (row.HospitalId > 0) checkedInUpcomingIds.Add(row.HospitalId); + RefreshPatientsPanels(); + } + + private void CompletePatient(PatientRow row) + { + if (row == null || row.IsCompleted) return; + row.IsCheckedIn = true; + row.IsCompleted = true; + if (row.HospitalId > 0) + { + checkedInUpcomingIds.Add(row.HospitalId); + completedUpcomingIds.Add(row.HospitalId); + } + RefreshPatientsPanels(); + } + + /// Re-syncs the day's upcoming list with shared state and refreshes both panels. + private void RefreshPatientsPanels() + { + SelectDay(selectedDayIndex); + grid?.Refresh(); + StateHasChanged(); + } + + /// Checks an upcoming-appointment patient in (list on the right). + private void CheckInUpcoming(Upcoming a) + { + if (a == null || a.IsCheckedIn) return; + a.IsCheckedIn = true; + if (a.HospitalId > 0) checkedInUpcomingIds.Add(a.HospitalId); + RefreshPatientsPanels(); + } + private SfGrid grid; + + + // ---- CTA strip actions ---- + private bool showScheduleCalendar { get; set; } + + private void ToggleScheduleCalendar() { - availableDoctors = Service.Doctors; + showScheduleCalendar = !showScheduleCalendar; + if (showScheduleCalendar) StateHasChanged(); + } + + private void OnScheduleDatePicked(Syncfusion.Blazor.Calendars.ChangedEventArgs args) + { + if (args?.Value == null) return; + showScheduleCalendar = false; + Today = args.Value.Value; + weekAnchor = Today.AddDays(-(int)Today.DayOfWeek + (int)DayOfWeek.Monday); + BuildWeek(); + BuildWeekAppointments(); + BuildPatients(); + BuildDailyAppointments(); + BuildAppointmentTrend(); + BuildDonut(); + activeTab = "Weekly"; + Urihelper.NavigateTo("schedule"); + } + // ---- Calendar bounds: past days are disabled — only Today and forward can be picked ---- + private DateTime calendarMin => Today.Date; + private int TotalPatientsInPeriod => filteredPatients.Count(); + private int waitingShare => + TotalPatientsInPeriod == 0 ? 0 : (int)Math.Round(WaitingCount * 100.0 / TotalPatientsInPeriod); + private int doctorUtilization => + doctorCount == 0 ? 0 : (int)Math.Round((double)ConsultationCount * 100 / doctorCount); + private int completionRate => + TotalPatientsInPeriod == 0 ? 0 : (int)Math.Round((double)CompletedCount * 100 / TotalPatientsInPeriod); - activities = Service.Activities; - patients = Service.Patients; - firstDayOfWeek = Service.GetWeekFirstDate(Service.StartDate); - max = Service.GetWeekFirstDate(Service.StartDate).AddDays(6); - endDate = Service.StartDate.AddDays(1); + private string BusiestDay => + dailyAppointments.Count == 0 ? "—" : + dailyAppointments.OrderByDescending(d => d.Clinic + d.Online).First().Day; + + private bool IsInCurrentWeek(DateTime date) + { + var weekStart = Today.AddDays(-(int)Today.DayOfWeek + (int)DayOfWeek.Monday).Date; + var weekEnd = weekStart.AddDays(7); + return date.Date >= weekStart && date.Date < weekEnd; } - private List GetAppoinment(List currentDayEvents) + private string severityFilter { get; set; } + + private void ToggleSeverity(string sev) + => severityFilter = severityFilter == sev ? null : sev; + + private DateTime calendarMax { - List apts = new List(); - foreach(var eventData in currentDayEvents) + get { - var filteredPatients = patients.Where(item => item.Id.Equals(eventData.PatientId)).FirstOrDefault(); - var filteredDoctors = availableDoctors.Where(item => item.Id.Equals(eventData.DoctorId)).FirstOrDefault(); - if (filteredPatients != null && filteredDoctors != null) + var safeYear = Math.Min(Today.Year, 2998); + var candidate = new DateTime(safeYear, Today.Month, 1).AddMonths(7).AddDays(-1); + if (candidate > new DateTime(2999, 12, 31)) candidate = new DateTime(2999, 12, 31); + return candidate; + } + } + + // ---- KPI: dynamic metrics derived from service data ---- + private int totalPatients => Service?.Patients?.Count ?? 0; + private int oldPatients => Service?.Patients?.Count(p => GetAge(p.DOB) >= 40) ?? 0; + private int newPatients => Service?.Patients?.Count(p => GetAge(p.DOB) < 25) ?? 0; + private int appointmentsCount => Service?.Hospitals?.Count ?? 0; + private int doctorCount => Service?.Doctors?.Count ?? 0; + + // Trend percentages – signed week-over-week change computed from real rows + private int totalPatientsTrend => PeriodChange(appointmentsThisWeek, appointmentsLastWeek); + private int oldPatientsTrend => PeriodChange(oldPatients, appointmentsCount); + private int newPatientsTrend => PeriodChange(newPatients, doctorCount); + private int appointmentsTrend => PeriodChange(appointmentsThisWeek, appointmentsLastWeek); + + private int PeriodChange(int current, int previous) + { + var cur = Math.Max(1, current); + var prev = Math.Max(1, previous); + return Math.Clamp((int)Math.Round((cur - prev) * 100.0 / prev), -99, 99); + } + + private bool IsInPreviousWeek(DateTime date) + { + var weekStart = Today.AddDays(-(int)Today.DayOfWeek + (int)DayOfWeek.Monday).AddDays(-7).Date; + var weekEnd = weekStart.AddDays(7); + return date.Date >= weekStart && date.Date < weekEnd; + } + + // ---- Daily Appointment Stats (Clinic vs Online) ---- + public class DayAppt { public string Day { get; set; } public double Clinic { get; set; } public double Online { get; set; } } + private List dailyAppointments = new(); + private double dailyYMax => NiceMax(dailyAppointments.Count == 0 ? 10 : dailyAppointments.Max(d => Math.Max(d.Clinic, d.Online))); + private double dailyYInterval => Math.Max(1, Math.Round(dailyYMax / 4, 0)); + + // ---- Appointment Trend (spline) ---- + public class DayPoint { public string Day { get; set; } public double Value { get; set; } } + private List appointmentTrend = new(); + private double trendYMax => NiceMax(appointmentTrend.Count == 0 ? 10 : appointmentTrend.Max(d => d.Value)); + private double trendYInterval => Math.Max(1, Math.Round(trendYMax / 5, 0)); + + // ---- Donut: Patient Overview (Child/Teen/Adult/Older) ---- + public class DonutSlice + { + public string Category { get; set; } + public double Value { get; set; } + public string Color { get; set; } + /// Human-readable age span this cohort covers. + public string AgeRange { get; set; } + /// Live-computed share of the total, 0-100. + public int Share { get; set; } + } + private List donutData = new(); + private int donutTotal => (int)donutData.Sum(d => d.Value); + private DonutSlice dominantSegment => donutTotal > 0 + ? donutData.OrderByDescending(d => d.Value).FirstOrDefault() + : null; + private int dominantShare => (int)Math.Round(dominantSegment.Value * 100.0 / Math.Max(1, donutTotal)); + + // ---- Upcoming appointments ---- + public class WeekDay { public string Short { get; set; } public int Number { get; set; } public DateTime Date { get; set; } } + public class Upcoming { public int HospitalId { get; set; } public string Name { get; set; } public string Time { get; set; } public string Dept { get; set; } public string StatusText { get; set; } public string StatusClass { get; set; } public string Initial { get; set; } public bool IsCheckedIn { get; set; } } + + private DateTime weekAnchor = DateTime.Today; + private List weekDays = new(); + private int selectedDayIndex = 4; + private List upcomingAppointments = new(); + + private Dictionary> upcomingByDate = new(); + + private void BuildWeekAppointments() + { + upcomingByDate = new Dictionary>(); + if (Service?.Hospitals == null) return; + + foreach (var h in Service.Hospitals) + { + var date = h.StartTime.Date; + if (!upcomingByDate.TryGetValue(date, out var list)) { - apts.Add(new Appointment(Service.GetFormatDate(eventData.StartTime, "hh:mm tt"), filteredPatients.Name, filteredDoctors.Name, eventData.Symptoms, filteredDoctors.Id)); + list = new List(); + upcomingByDate[date] = list; } + list.Add(h); } - return apts; } - private DateTime AddDate(DateTime date, int value) + private List GetUpcomingForDay(int idx) { - return date.AddDays(value); + if (Service?.Hospitals == null || weekDays.Count == 0) return new List(); + + var dayDate = weekDays[Math.Clamp(idx, 0, weekDays.Count - 1)].Date.Date; + var docsById = Service.Doctors?.ToDictionary(d => d.Id) ?? new Dictionary(); + + if (!upcomingByDate.TryGetValue(dayDate, out var matches) || matches == null) + return new List(); + + return matches + .OrderBy(h => h.StartTime) + .Select(h => + { + var doc = docsById.TryGetValue(h.DoctorId, out var d) ? d : null; + var status = (doc?.Availability ?? "busy").ToLowerInvariant(); + return new Upcoming + { + HospitalId = h.Id, + Name = h.Name, + Time = h.StartTime.ToString("h:mmtt").ToLower(), + Dept = h.DepartmentName, + StatusText = status == "available" ? "Online" : "Offline", + StatusClass = status == "available" ? "online" : "offline", + Initial = GetInitials(h.Name), + IsCheckedIn = checkedInUpcomingIds.Contains(h.Id) || completedUpcomingIds.Contains(h.Id) + }; + }).ToList(); } - private string getSpecializationText(string text) + private void BuildWeek() { - return Service.Specializations.Where(item => item.Id.Equals(text)).FirstOrDefault().Text.ToUpper(); + weekDays = Enumerable.Range(0, 7) + .Select(i => + { + var d = weekAnchor.AddDays(i); + return new WeekDay { Short = d.ToString("ddd").Substring(0, 3), Number = d.Day, Date = d }; + }) + .ToList(); } - private async void LoadData() + private void SelectDay(int idx) { - List currentDayEvents = Service.GetFilteredData(startDate, endDate); - this.appointments = this.GetAppoinment(currentDayEvents); - List currentViewEvents = Service.GetFilteredData(firstDayOfWeek, this.AddDate(startDate, 6)); - this.dayEventCount = currentDayEvents.Count().ToString(); - this.weekEventCount = currentViewEvents.Count().ToString(); - List diabetologyData = currentViewEvents.Where(item => item.DepartmentId == 5).ToList(); - List orthopaedicsData = currentViewEvents.Where(item => item.DepartmentId == 4).ToList(); - List cardiologyData = currentViewEvents.Where(item => item.DepartmentId == 6).ToList(); - chartData = Service.GetAllChartPoints(diabetologyData, firstDayOfWeek); - chartData1 = Service.GetAllChartPoints(orthopaedicsData, firstDayOfWeek); - chartData2 = Service.GetAllChartPoints(cardiologyData, firstDayOfWeek); - await Task.Delay(500); - isDataLoaded = true; - StateHasChanged(); + selectedDayIndex = idx; + upcomingAppointments = GetUpcomingForDay(idx); + } + + private void PrevWeek() + { + var candidate = weekAnchor.AddDays(-7); + if (candidate.AddDays(6).Date < Today.Date) return; + weekAnchor = candidate; + BuildWeek(); + BuildWeekAppointments(); + var firstFuture = weekDays.FindIndex(d => d.Date.Date >= Today.Date); + SelectDay(firstFuture < 0 ? 0 : firstFuture); + } + + private void NextWeek() + { + weekAnchor = weekAnchor.AddDays(7); + BuildWeek(); + BuildWeekAppointments(); + SelectDay(0); + } + + // ---- Patients table ---- + public class PatientRow + { + /// Id of the underlying appointment (Service.Hospitals) — links grid row to the upcoming list. + public int HospitalId { get; set; } + public string Name { get; set; } + public string Initial { get; set; } + public int Age { get; set; } + public DateTime AppointmentDate { get; set; } + public string DateTime { get; set; } + public string Reason { get; set; } + public bool IsCheckedIn { get; set; } + public bool IsCompleted { get; set; } + } + + private string[] patientTabs = new[] { "Daily", "Weekly", "Monthly", "Yearly" }; + private string activeTab = "Daily"; + private string searchText { get; set; } = string.Empty; + private bool monthPickerOpen { get; set; } + + private void ToggleMonthPicker() => monthPickerOpen = !monthPickerOpen; + + private void CloseMonthPicker() + { + if (monthPickerOpen) monthPickerOpen = false; + } + + private void OnMonthPicked(DateTime? picked) + { + if (!picked.HasValue) { monthPickerOpen = false; return; } + Today = picked.Value; + monthPickerOpen = false; + weekAnchor = Today.AddDays(-(int)Today.DayOfWeek + (int)DayOfWeek.Monday); + BuildWeek(); + BuildWeekAppointments(); + BuildPatients(); + BuildDailyAppointments(); + BuildAppointmentTrend(); + BuildDonut(); + var firstFuture = weekDays.FindIndex(d => d.Date.Date >= Today.Date); + SelectDay(firstFuture < 0 ? 0 : firstFuture); } - protected override void OnAfterRender(bool firstRender) + private List allPatients = new(); + private IEnumerable filteredPatients + { + get + { + IEnumerable q = activeTab switch + { + "Daily" => allPatients.Where(p => p.AppointmentDate.Date == Today.Date), + "Weekly" => allPatients.Where(p => IsInCurrentWeek(p.AppointmentDate)), + "Monthly" => allPatients.Where(p => p.AppointmentDate.Year == Today.Year && p.AppointmentDate.Month == Today.Month), + "Yearly" => allPatients.Where(p => p.AppointmentDate.Year == Today.Year), + _ => allPatients + }; + + if (!string.IsNullOrWhiteSpace(searchText)) + q = q.Where(p => p.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) || p.Reason.Contains(searchText, StringComparison.OrdinalIgnoreCase)); + + // Apply the shared check-in / completed state (tracked by appointment Id) + var result = q.OrderByDescending(p => p.AppointmentDate).ToList(); + result.ForEach(ApplyRowState); + return result; + } + } + + private int appointmentsThisWeek => + allPatients.Count(p => IsInCurrentWeek(p.AppointmentDate)); + private int appointmentsLastWeek => + allPatients.Count(p => IsInPreviousWeek(p.AppointmentDate)); + + // ---- Build dynamic datasets from service ---- + private void BuildPatients() + { + if (Service?.Hospitals == null) { allPatients = new List(); return; } + + var patientsById = Service.Patients? + .GroupBy(p => p.Id) + .ToDictionary(g => g.Key, g => g.First()) + ?? new Dictionary(); + + allPatients = Service.Hospitals + .OrderBy(h => h.StartTime) + .Select(h => + { + patientsById.TryGetValue(h.PatientId, out var patient); + var matched = patient ?? Service.Patients?.FirstOrDefault(p => p.Name == h.Name); + return new PatientRow + { + HospitalId = h.Id, + Name = h.Name, + Initial = GetInitials(h.Name), + Age = matched == null ? 0 : GetAge(matched.DOB), + AppointmentDate = h.StartTime, + DateTime = h.StartTime.ToString("d MMM yyyy · h:mm tt"), + Reason = h.Disease + }; + }) + .ToList(); + } + + /// Single source of truth for check-in / completed state per appointment Id. + private readonly HashSet checkedInUpcomingIds = new(); + private readonly HashSet completedUpcomingIds = new(); + + /// Applies tracked check-in / completed state to a grid row. + private void ApplyRowState(PatientRow row) + { + if (row == null) return; + row.IsCheckedIn = checkedInUpcomingIds.Contains(row.HospitalId); + row.IsCompleted = completedUpcomingIds.Contains(row.HospitalId); + } + + private void BuildDailyAppointments() + { + if (Service?.Hospitals == null) return; + var byDow = Service.Hospitals + .GroupBy(h => h.StartTime.DayOfWeek) + .ToDictionary(g => g.Key, g => g.Count()); + + var order = new[] + { + DayOfWeek.Saturday, DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, + DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday + }; + var labels = new[] { "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri" }; + + dailyAppointments = order.Select((dow, i) => + { + var total = byDow.TryGetValue(dow, out var c) ? c : 0; + var clinic = Math.Round(total * 0.6, 0); + var online = total - clinic; + return new DayAppt { Day = labels[i], Clinic = clinic, Online = online }; + }).ToList(); + } + + private void BuildAppointmentTrend() + { + if (Service?.Hospitals == null) return; + var byDow = Service.Hospitals + .GroupBy(h => h.StartTime.DayOfWeek) + .ToDictionary(g => g.Key, g => g.Count()); + + var order = new[] + { + DayOfWeek.Saturday, DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, + DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday + }; + var labels = new[] { "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri" }; + + appointmentTrend = order.Select((dow, i) => new DayPoint + { + Day = labels[i], + Value = byDow.TryGetValue(dow, out var c) ? c : 0 + }).ToList(); + } + + private void BuildDonut() + { + if (Service?.Patients == null) return; + var groups = Service.Patients + .GroupBy(p => GetAgeBucket(GetAge(p.DOB))) + .ToDictionary(g => g.Key, g => g.Count()); + + var palette = new (string Cat, string Color, string Rng)[] + { + ("Child", "#0a8a8a", "0–12 yrs"), + ("Teen", "#f59e0b", "13–19 yrs"), + ("Adult", "#ec4899", "20–39 yrs"), + ("Older", "#a855f7", "40+ yrs"), + }; + var temp = palette.Select(p => new DonutSlice + { + Category = p.Cat, + Color = p.Color, + AgeRange = p.Rng, + Value = groups.TryGetValue(p.Cat, out var c) ? c : 0 + }).ToList(); + + + var total = temp.Sum(d => d.Value); + foreach (var d in temp) d.Share = total > 0 ? (int)Math.Round(d.Value * 100.0 / total) : 0; + donutData = temp; + + if (donutTotal == 0) + { + donutData = palette.Select(p => new DonutSlice + { + Category = p.Cat, + Color = p.Color, + AgeRange = p.Rng, + Value = 1 + }).ToList(); + } + } + + // ---- Helpers ---- + private int GetAge(DateTime? dob) + { + if (!dob.HasValue) return 0; + var age = Today.Year - dob.Value.Year; + if (dob.Value.Date > Today.AddYears(-age)) age--; + return Math.Max(0, age); + } + + private string GetAgeBucket(int age) => age switch + { + <= 12 => "Child", + <= 19 => "Teen", + <= 39 => "Adult", + _ => "Older" + }; + + private string GetInitials(string name) + { + if (string.IsNullOrWhiteSpace(name)) return "?"; + var parts = name.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 1) return parts[0].Substring(0, Math.Min(2, parts[0].Length)).ToUpperInvariant(); + return (parts[0][0].ToString() + parts[1][0].ToString()).ToUpperInvariant(); + } + + private string FormatCount(int n) + { + if (n >= 1000) return (n / 1000.0).ToString("0.#") + "K"; + return n.ToString(); + } + + private double NiceMax(double v) + { + if (v <= 0) return 10; + var pow = Math.Pow(10, Math.Floor(Math.Log10(v))); + var n = v / pow; + double nice = n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10; + return nice * pow; + } + + private string GetStatus(PatientRow row) + { + if (row == null) + return "Waiting"; + + if (row.Age >= 65) + return "Critical"; + + if (row.Age >= 40) + return "Moderate"; + + return "Waiting"; + } + + private string GetStatusClass(PatientRow row) + { + if (row == null) + return "waiting-chip"; + + if (row.Age >= 65) + return "critical-chip"; + + if (row.Age >= 40) + return "warning-chip"; + + return "waiting-chip"; + } + + private string GetPriority(PatientRow row) + { + if (row == null) return "Normal"; + if (row.Age >= 65) return "Urgent"; + if (row.Age >= 40) return "High"; + return "Normal"; + } + + private string GetPriorityClass(PatientRow row) + { + if (row == null) return "priority-normal-chip"; + if (row.Age >= 65) return "priority-urgent-chip"; + if (row.Age >= 40) return "priority-high-chip"; + return "priority-normal-chip"; + } + + // ---- Lifecycle ---- + private bool isDataLoaded; + //private SfGrid grid; + + protected override void OnInitialized() + { + weekAnchor = Today.AddDays(-(int)Today.DayOfWeek + (int)DayOfWeek.Monday); + BuildWeek(); + BuildWeekAppointments(); + BuildPatients(); + BuildDailyAppointments(); + BuildAppointmentTrend(); + BuildDonut(); + var firstFuture = weekDays.FindIndex(d => d.Date.Date >= Today.Date); + selectedDayIndex = firstFuture < 0 ? 0 : firstFuture; + SelectDay(selectedDayIndex); + } + + protected async override Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - LoadData(); + await Task.Delay(120); + isDataLoaded = true; + StateHasChanged(); } } + + public void Dispose() { } } diff --git a/webapp/server/AppointmentPlanner/Components/Pages/Doctors/Doctors.razor b/webapp/server/AppointmentPlanner/Components/Pages/Doctors/Doctors.razor index 38ae097..ab7ffec 100644 --- a/webapp/server/AppointmentPlanner/Components/Pages/Doctors/Doctors.razor +++ b/webapp/server/AppointmentPlanner/Components/Pages/Doctors/Doctors.razor @@ -9,71 +9,237 @@
The Doctor's details have been deleted successfully.
-
-
-
-
DOCTORS LIST
-
-
-
-
-
Add New
+
+ +
-
-
- - - - - -
- - @((context as Specialization).Text) + + + Add Doctor + + +
+ +
+ + + +
+
+
+ Total Doctors
- - -
- - @((context as Specialization).Text) +
+ @TotalDoctors
- - - - Add New Doctor -
-
- @foreach (var data in filteredDoctors) - { -
-
-
- doctor - +
+ +
+
+ + + + + +
+
+
+ Available +
+
+ @AvailableDoctors
-
-
- Dr. @data.Name + +
+
+ + + + + +
+
+
+ Departments
-
- @(data.Education != null ? data.Education.ToUpper(): "") +
+ @specializationData.Count
-
-
- Designation - @data.Designation +
+ +
+
+ + + +
+
+ + + + + + + + + + + + + + +
+ +
+ + @foreach (var doctor in DisplayDoctors) + { +
+ +
+ + + + + + @GetDoctorStatus(doctor) + + + +
+ +
+ + Dr. @doctor.Name + +
+

+ Dr. @doctor.Name +

+
+ @Service.GetSpecializationText(doctor.Specialization) +
+
+ @doctor.Education
-
- Experience - @data.Experience +
+
+ + +
+ +
+ + + Today + + + + @GetTodayAppointmentCount(doctor.Id) Appointments + + +
+ + +
+ + + Next Slot + + + + @GetNextAvailableSlot(doctor) + + +
+
+ +
+ +
+ + Workload + + @GetWorkloadPercent(doctor)% + +
+ +
+ +
+
+
-
- } -
+ +
+ +
+ +
+ Experience +
+ +
+ @doctor.Experience +
+ +
+ +
+ +
+ Designation +
+ +
+ @doctor.Designation +
+ +
+ +
+ +
+ + + View + + +
+
+ + } +
+
@@ -208,13 +374,125 @@ else{ private List dutyTimings { get; set; } private bool isDataLoaded; + private string searchText = ""; - protected override void OnInitialized() + private int GetTodayAppointmentCount(int doctorId) { - this.specializationData = Service.Specializations; - this.experienceData = Service.Experience; + return Service.Hospitals? + .Count(x => + x.DoctorId == doctorId && + x.StartTime.Date == DateTime.Today) ?? 0; } + private IEnumerable DisplayDoctors => + filteredDoctors?.Where(x => + string.IsNullOrWhiteSpace(searchText) || + x.Name.Contains(searchText, + StringComparison.OrdinalIgnoreCase) || + x.Specialization.Contains(searchText, + StringComparison.OrdinalIgnoreCase)) + ?? Enumerable.Empty(); + + private int GetDoctorDailyCapacity(Doctor doctor) + { + if (string.IsNullOrEmpty(doctor.StartHour) || + string.IsNullOrEmpty(doctor.EndHour)) + { + return 16; + } + + TimeSpan start = TimeSpan.Parse(doctor.StartHour); + TimeSpan end = TimeSpan.Parse(doctor.EndHour); + + var hours = (end - start).TotalHours; + + return (int)(hours * 2); + } + private int GetWorkloadPercent(Doctor doctor) + { + int appointments = + GetTodayAppointmentCount(doctor.Id); + + int capacity = + GetDoctorDailyCapacity(doctor); + + if (capacity <= 0) + return 0; + + return Math.Min( + (appointments * 100) / capacity, + 100); + } + + private string GetDoctorStatus(Doctor doctor) + { + return doctor.Availability switch + { + "busy" => "Busy", + "away" => "Away", + _ => "Available" + }; + } + private string GetStatusClass(Doctor doctor) + { + return doctor.Availability; + } + + private string GetNextAvailableSlot(Doctor doctor) + { + var appointments = + Service.Hospitals + .Where(x => + x.DoctorId == doctor.Id && + x.StartTime.Date == DateTime.Today) + .OrderBy(x => x.StartTime) + .ToList(); + + if (!appointments.Any()) + { + return DateTime.Today + .Add(TimeSpan.Parse(doctor.StartHour)) + .ToString("hh:mm tt"); + } + + DateTime nextSlot = + appointments.Last().EndTime; + + DateTime doctorEnd = + DateTime.Today + .Add(TimeSpan.Parse(doctor.EndHour)); + + if (nextSlot >= doctorEnd) + return "Full"; + + return nextSlot.ToString("hh:mm tt"); + } + + private string GetWorkloadColor(int workload) + { + if (workload >= 90) + return "#dc2626"; + + if (workload >= 60) + return "#f59e0b"; + + return "#16a34a"; + } + private int TotalDoctors => + Service.Doctors.Count; + + private int AvailableDoctors => + Service.Doctors.Count(x => x.Availability == "available"); + + private int BusyDoctors => + Service.Doctors.Count(x => + x.Availability == "busy"); + + private int AwayDoctors => + Service.Doctors.Count(x => + x.Availability == "away"); + + private void onAddDoctor(Microsoft.AspNetCore.Components.Web.MouseEventArgs args) { doctorEditModel = new Doctor { Name = null, Gender = "Male", Mobile = null, Email = null, Education = null, Designation = null }; @@ -292,14 +570,18 @@ else{ } private async void LoadData() - { + { this.dutyTimings = Service.DutyTimings; this.filteredDoctors = Service.Doctors; await Task.Delay(500); isDataLoaded = true; StateHasChanged(); } - + protected override void OnInitialized() + { + this.specializationData = Service.Specializations; + this.experienceData = Service.Experience; + } protected override void OnAfterRender(bool firstRender) { if (firstRender) diff --git a/webapp/server/AppointmentPlanner/Components/Pages/Doctors/DoctorsDetails.razor b/webapp/server/AppointmentPlanner/Components/Pages/Doctors/DoctorsDetails.razor index 4c7e007..48e2c00 100644 --- a/webapp/server/AppointmentPlanner/Components/Pages/Doctors/DoctorsDetails.razor +++ b/webapp/server/AppointmentPlanner/Components/Pages/Doctors/DoctorsDetails.razor @@ -7,62 +7,244 @@ @inject NavigationManager UriHelper @implements IDisposable; - +
-
-
- -
-
DOCTOR DETAILS
-
+
+ +
+ + + +
+ +

+ Doctor Details +

+ +
+
+
-
- Delete - Edit + +
+ + + + Delete + + + + + + Edit + + +
+
- @if (activeData != null) { -
-
- doctor - -
-
-
-
Dr. @activeData.Name
-
@(activeData.Education != null ? activeData.Education.ToUpper(): "")
-
@activeData.Designation
-
-
-
Specialization
-
@getSpecializationText
-
-
-
Experience
-
@activeData.Experience
-
-
-
Availability
-
@getAvailability
-
-
-
Mobile
-
@activeData.Mobile
-
-
-
-
-
+ @if (activeData != null) { +
+ + + + + + + + +
+ +
+ + Doctor profile name + +
+ +
+ +

+ Dr. @activeData.Name +

+ +
+ @getSpecializationText +
+ +
+ @activeData.Education +
+ +
+ @activeData.Designation +
+ +
+ +
+ + + @activeData.Availability + + +
+ +
+ +
+ +
+ + + +
+ + + + +
+ @activeData.Experience +
+ +
+ Experience +
+ +
+
+ + + + +
+ @WorkingDaysCount +
+ +
+ Working Days +
+ +
+
+ + + + +
+ @activeData.Mobile +
+ +
+ Contact +
+ +
+
+ +
+ + + +
+ + + + + +

Professional Information

+ +
+ + @getSpecializationText +
+ +
+ + @activeData.Designation +
+ +
+ + @activeData.Experience +
+ +
+ + @activeData.Education +
+ +
+ +
+ + + + + +

Contact Information

+ +
+ + @activeData.Mobile +
+ +
+ + @activeData.Email +
+ +
+ +
+ +
+ + + + + + + +

+ Weekly Schedule +

+ @foreach (var data in activeData.WorkDays) { -
@(data.Day.Substring(0, 1).ToUpper())@data.Day.Substring(1)
-
@(getBreakDetails(data))
+
+ + + @(char.ToUpper(data.Day[0]) + data.Day.Substring(1)) + + + + @getBreakDetails(data) + + +
} -
-
-
-
+ + + + + + }
@@ -148,6 +330,7 @@ private Doctor activeData { get; set; } private string getSpecializationText { get; set; } private string getAvailability { get; set; } + private int WorkingDaysCount =>activeData?.AvailableDays?.Length ?? 0; protected override void OnInitialized() { @@ -206,7 +389,7 @@ } onBackIconClick(args); } - + private void onDoctorEdit(Microsoft.AspNetCore.Components.Web.MouseEventArgs args) { editVisible = true; diff --git a/webapp/server/AppointmentPlanner/Components/Pages/Patients/Patients.razor b/webapp/server/AppointmentPlanner/Components/Pages/Patients/Patients.razor index f5117ed..ac3a1b8 100644 --- a/webapp/server/AppointmentPlanner/Components/Pages/Patients/Patients.razor +++ b/webapp/server/AppointmentPlanner/Components/Pages/Patients/Patients.razor @@ -22,36 +22,113 @@
-
-
-
-