65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ASP.NET Core (.NET 6) 中构建可视化医生预约排班系统

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2022年1月12日

Apache

6分钟阅读

viewsIcon

48750

downloadIcon

2162

使用 ASP.NET Core、Entity Framework、DayPilot 和原生 JavaScript 构建医生预约排班 Web 应用程序。

ASP.NET Core (.NET 6) Doctor Appointment Scheduling UI

预约排班用户界面

患者用户界面

医生预约排班项目包含一个专门的患者用户界面,患者可以在其中查看空闲预约时段。

Patient UI for scheduling an appointment

患者看到的是一个简化的日历,带有周视图。在左侧,应用程序显示一个日期选择器日历,显示三个月。这有助于在周之间快速导航。有空闲预约时段的日期使用粗体字体。这样,患者可以快速查看第一个可用日期,并可以选择他们的预约日期和时间。

患者用户界面的主要目的是让他们选择一个空闲的预约时段。其他患者的预约以及过去的预约时段都已隐藏。这些时段是只读的,不能移动。一旦患者选择了一个时段,颜色就会变为橙色,表示“等待”状态(有关预约时段状态的更多信息,请参阅下文)。预约请求需要得到确认。

医生用户界面

医生使用单独的区域管理预约 - 创建、移动和编辑预约时段。

Doctor Appointment Scheduling UI

医生用户界面更高级 - 它显示所有预约时段,医生对它们拥有完全控制权。他们可以编辑预约详情,确认患者发送的请求,使用拖放将时段移动到不同的时间,创建自定义时段并删除它们。

所有预约时段都需要提前定义 - 我们将在下一节中更详细地讨论逻辑。

基本组件

虽然您可以在 AngularReactVue 中使用 DayPilot 日历组件,但这次我们将使用不带任何框架的简单 JavaScript。

有关使用 DayPilot 的 JavaScript 日历组件的介绍,请参阅 HTML5/JavaScript 事件日历 [code.daypilot.org] 教程。

工作原理

处理带有公共接口的医生预约有两种主要方法。

  1. 您可以定义显示可用时间的工作时间。患者可以在这些工作时间内创建新预约。这样,应用程序需要应用额外的规则,例如预约开始时间和持续时间。当患者请求会议时,会创建预约数据库记录。
  2. 您可以提前定义独立时段,患者只能选择现有时段之一。不需要额外的规则,因为医生对时段拥有完全控制权。在生成时段时会创建数据库记录。当患者请求会议时,他们只会更改时段状态。

在此项目中,我们将使用方法 #2,即预定义时段

此工作流程要求提前定义时段。此应用程序使用半自动系统,可让您为特定范围生成时段(而不是逐个添加)。

ASP.NET Core 中的日、周和月日历视图

主要调度界面使用 DayPilot 的 JavaScript 日历组件创建。

它是您在 Google 日历或 Outlook 中熟悉的传统日/周/月视图。

在此应用程序中,我们扩展了日历应用程序的功能,增加了调度功能。

  • 事件/预约具有状态,该状态决定了时段的颜色。
  • 时段管理部分,可让您批量创建或删除时段。

ASP.NET Core Doctor Appointment Scheduler Day/Week/Month View

医生部分的前端部分在 Doctor.cshml ASP.NET Core 视图中定义。

该视图加载 DayPilot 调度 JavaScript 库

<script src="~/lib/daypilot/daypilot-all.min.js"></script>

并包含日历视图(日、周和月)的占位符。日历组件将在此处显示。切换视图时,我们将根据需要隐藏和显示组件。

<div id="day"></div>
<div id="week"></div>
<div id="month"></div>

现在我们需要初始化日历组件。每个组件都有自己的实例——日视图和周视图使用DayPilot.Calendar类,月视图使用DayPilot.Month类。

您会看到配置非常简单 - 我们依赖默认值。但是,必须添加定义行为的事件处理程序。

日视图

const day = new DayPilot.Calendar("day", {
    viewType: "Day",
    visible: false,
    eventDeleteHandling: "Update",
    onTimeRangeSelected: (args) => {
        app.createAppointmentSlot(args);
    },
    onEventMoved: (args) => {
        app.moveAppointmentSlot(args);
    },
    onEventResized: (args) => {
        app.moveAppointmentSlot(args);
    },
    onEventDeleted: (args) => {
        app.deleteAppointmentSlot(args);
    },
    onBeforeEventRender: (args) => {
        app.renderAppointmentSlot(args);
    },
    onEventClick: (args) => {
        app.editAppointmentSlot(args);
    }
});
day.init();

周视图

const week = new DayPilot.Calendar("week", {
    viewType: "Week",
    eventDeleteHandling: "Update",
    onTimeRangeSelected: (args) => {
        app.createAppointmentSlot(args);
    },
    onEventMoved: (args) => {
        app.moveAppointmentSlot(args);
    },
    onEventResized: (args) => {
        app.moveAppointmentSlot(args);
    },
    onEventDeleted: (args) => {
        app.deleteAppointmentSlot(args);
    },
    onBeforeEventRender: (args) => {
        app.renderAppointmentSlot(args);
    },
    onEventClick: (args) => {
        app.editAppointmentSlot(args);
    }
});
week.init();

月视图

const month = new DayPilot.Month("month", {
    visible: false,
    eventDeleteHandling: "Update",
    eventMoveHandling: "Disabled",
    eventResizeHandling: "Disabled",
    cellHeight: 150,
    onCellHeaderClick: args => {
        nav.selectMode = "Day";
        nav.select(args.start);
    },
    onEventDelete: args => {
        app.deleteAppointmentSlot(args);
    },
    onBeforeEventRender: args => {
        app.renderAppointmentSlot(args);

        // customize content
        const locale = DayPilot.Locale.find(month.locale);
        const start = new DayPilot.Date(args.data.start).toString(locale.timePattern);
        const name = DayPilot.Util.escapeHtml(args.data.patientName || "");
        args.data.html = `<span class='month-time'>${start}</span> ${name}`;
    },
    onTimeRangeSelected: async (args) => {
        const params = {
            start: args.start.toString(),
            end: args.end.toString(),
            weekends: true
        };

        args.control.clearSelection();

        const {data} = await DayPilot.Http.post("/api/appointments/create", params);
        app.loadEvents();
    },
    onEventClick: (args) => {
        app.editAppointmentSlot(args);
    }
});
month.init();

除了日历视图,我们还添加了一个日期选择器,让医生可以轻松更改日期。

占位符是一个空的 <div> 元素

<div id="nav"></div> 

我们需要使用 DayPilot.Navigator 类初始化日期选择器。

const nav = new DayPilot.Navigator("nav", {
    selectMode: "Week",
    showMonths: 3,
    skipMonths: 3,
    onTimeRangeSelected: (args) => {
        app.loadEvents(args.day);
    }
});
nav.init();

app.loadEvents() 方法很重要,因为它从服务器加载预约时段。它检查当前视图类型(使用 nav.selectMode)并加载数据。

const app = {
    async loadEvents(date) {
        const start = nav.visibleStart();
        const end = nav.visibleEnd();

        const {data} = await DayPilot.Http.get(`/api/appointments?start=${start}&end=${end}`);

        const options = {
            visible: true,
            events: data
        };

        if (date) {
            options.startDate = date;
        }

        day.hide();
        week.hide();
        month.hide();

        const active = app.active();
        active.update(options);

        nav.update({
            events: data
        });
    },
    // ...
};

请注意,它加载日期选择器中可见的完整日期范围(三个月)的预约数据。这样,日期选择器可以突出显示有预约的日期。此数据将用于当前可见的日历组件(日历日期范围始终是日期选择器中可见的完整范围的子集)。

服务器端部分是一个标准的 ASP.NET Core API 控制器,由 Entity Framework 模型类生成。在某些情况下,当我们需要执行特定操作(除了基本的 createupdatedeleteselect 操作)或需要额外参数时,会扩展控制器方法。

GetAppointments() 方法(GET /api/appointments 端点)如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;
using Project.Service;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AppointmentsController : ControllerBase
    {
        private readonly DoctorDbContext _context;

        public AppointmentsController(DoctorDbContext context)
        {
            _context = context;
        }

        // GET: api/Appointments
        [HttpGet]
        public async Task<ActionResult<IEnumerable<AppointmentSlot>>> 
               GetAppointments([FromQuery] DateTime start, [FromQuery] DateTime end)
        {
            return await _context.Appointments.Where(e => !((e.End <= start) || 
                                                    (e.Start >= end))).ToListAsync();
        }
        
        // ...        
    }
}

生成预约时段

在医生用户界面中的每个日历视图中,您可以使用“生成”按钮填充当前范围的预约时段。

Generate Appointment Slots Button

该按钮会打开一个模态对话框,您可以在其中选择是否也要为周末生成预约时段。

Generate Appointment Slots Modal Dialog

在月视图中,您还可以使用拖放功能选择任意范围。

Generate Appointment Slots using Drag and Drop

选择日期范围将为指定日期生成时段。

Automatically-Created Appointment Slots

它将跳过与现有预约时段冲突的时间。

时段生成逻辑在服务器端实现。AppointmentController 类的 PostAppointmentSlots() 方法使用 Timeline 类辅助工具生成时段日期。我们在此处不详细介绍 Timeline 类,但您可以在项目源代码中查看其实现。

PostAppointmentSlots() 方法还会为日期范围选择现有时段,并在 SQL Server 数据库中创建新的时段记录之前检查重叠情况。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;
using Project.Service;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AppointmentsController : ControllerBase
    {
        private readonly DoctorDbContext _context;

        public AppointmentsController(DoctorDbContext context)
        {
            _context = context;
        }

        [HttpPost("create")]
        public async Task<ActionResult<AppointmentSlot>> 
                     PostAppointmentSlots(AppointmentSlotRange range)
        {
            var existing = await _context.Appointments.Where(e => !((e.End <= range.Start) || 
                                                       (e.Start >= range.End))).ToListAsync();
            var slots = Timeline.GenerateSlots(range.Start, range.End, range.Weekends);
            slots.ForEach(slot =>
            {
                var overlaps = existing.Any(e => !((e.End <= slot.Start) || 
                                           (e.Start >= slot.End)));
                if (overlaps)
                {
                    return;
                }
                _context.Appointments.Add(slot);
            });

            await _context.SaveChangesAsync();

            return NoContent();
        }

        public class AppointmentSlotRange
        {
            public DateTime Start { get; set; }
            public DateTime End { get; set; }
            public bool Weekends { get; set; }
        }

        // ...
    }
}

预约时段状态

每个预约时段可以标记为“可用”、“等待”或“已确认”。

可用

Appointment Status: Available

等待确认

Appointment Status: Waiting

已确认

Appointment Status: Confirmed

时段状态存储在 AppointmentSlot 类的 Status 属性中。API 控制器将其作为预约数据对象的 status 字段发送到客户端。

using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;

namespace Project.Models
{
    public class AppointmentSlot
    {
        public int Id { get; set; }
        public DateTime Start { get; set; }
        public DateTime End { get; set; }

        public string? PatientName {  set; get; }

        public string? Text => PatientName;

        [JsonPropertyName("patient")]
        public string? PatientId { set; get; }

        public string Status { get; set; } = "free";
    }
}

DayPilot JavaScript 日历组件提供了一个 onBeforeEventRender 事件处理程序,允许您在渲染之前自定义预约外观。我们将使用它根据时段状态应用自定义颜色。

const week = new DayPilot.Calendar("week", {
    viewType: "Week",
    onBeforeEventRender: (args) => {
        switch (args.data.status) {
        case "free":
            args.data.backColor = app.colors.blue;
            args.data.barColor = app.colors.blueDarker;
            args.data.borderColor = "darker";
            args.data.fontColor = app.colors.text;
            args.data.text = "Available";
            break;
        case "waiting":
            args.data.backColor = app.colors.orange;
            args.data.barColor = app.colors.orangeDarker;
            args.data.borderColor = "darker";
            args.data.fontColor = app.colors.text;
            args.data.text = args.data.patientName;
            break;
        case "confirmed":
            args.data.backColor = app.colors.green;
            args.data.barColor = app.colors.greenDarker;
            args.data.borderColor = "darker";
            args.data.fontColor = app.colors.text;
            args.data.text = args.data.patientName;
            break;
        }
    },
    // ...
});

如何运行 ASP.NET Core 项目

所有内容都已包含

  • 服务器端依赖项 (Entity Framework) 由 NuGet 管理。Visual Studio 通常会在第一次构建时自动为您加载 NuGet 依赖项。
  • 客户端依赖项 (DayPilot) 位于 wwwroot/lib 文件夹中。
  • 数据库需要安装 SQL Server Express LocalDB 实例。如果需要,您可以修改 appsettings.json 文件中的数据库连接字符串。初始的 Entity Framework 迁移已包含(请参阅 Migrations 文件夹),但您需要在控制台中运行 Update-Database 以应用它们。

历史

  • 2022 年 1 月 12 日:首次发布
© . All rights reserved.