diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 000000000..e11125a3a --- /dev/null +++ b/.github/README.md @@ -0,0 +1,131 @@ +# GitHub Actions Workflows + +Данный репозиторий содержит набор GitHub Actions workflows для автоматизации CI/CD процессов для ASP.NET Core проекта клиники. + +## Доступные Workflows + +### 1. CI Pipeline (`ci.yml`) +**Триггеры:** Push в `main`/`develop`, Pull Request в `main`/`develop` + +**Функции:** +- Сборка .NET 9.0 проектов +- Восстановление NuGet зависимостей +- Запуск тестов (если есть) +- Кэширование NuGet пакетов для ускорения сборки + +**Использование:** +```bash +# Локальная сборка +dotnet restore Clinic/Clinic.sln +dotnet build Clinic/Clinic.sln --configuration Release +dotnet test Clinic/Clinic.sln --configuration Release +``` + +### 2. Docker Build and Deploy (`docker.yml`) +**Триггеры:** Push в `main`, теги `v*`, Pull Request в `main` + +**Функции:** +- Создание Docker образа для API +- Публикация образа в GitHub Container Registry +- Поддержка версионирования образов + +**Использование:** +```bash +# Сборка Docker образа +docker build -f Clinic/Clinic.Api/Dockerfile -t clinic-api:latest . + +# Запуск контейнера +docker run -p 8080:8080 clinic-api:latest +``` + +### 3. Azure Deploy (`azure-deploy.yml`) +**Триггеры:** Push в `main`, ручной запуск + +**Функции:** +- Публикация .NET проекта +- Деплой в Azure App Service + +**Необходимые Secrets:** +- `AZURE_WEBAPP_NAME` - имя Azure App Service +- `AZURE_WEBAPP_PUBLISH_PROFILE` - профиль публикации + +**Настройка Azure:** +1. Создайте Azure App Service +2. Скачайте профиль публикации +3. Добавьте Secrets в GitHub репозиторий + +## Структура файлов + +``` +.github/ +├── workflows/ +│ ├── ci.yml # Основной CI pipeline +│ ├── docker.yml # Docker сборка и деплой +│ ├── azure-deploy.yml # Azure App Service деплой +│ └── setup_pr.yml # Автоматическая настройка PR +├── ISSUE_TEMPLATE/ +│ └── вопрос-по-лабораторной.md +├── PULL_REQUEST_TEMPLATE.md +└── DISCUSSION_TEMPLATE/ + └── questions.yml + +Clinic/ +├── Clinic.Api/ +│ ├── Dockerfile # Docker образ для API +│ └── .dockerignore # Исключения для Docker сборки +└── ... +``` + +## Настройка проекта + +### Для локальной разработки: +```bash +# Восстановление зависимостей +dotnet restore Clinic/Clinic.sln + +# Сборка проекта +dotnet build Clinic/Clinic.sln + +# Запуск API +dotnet run --project Clinic/Clinic.Api + +# Запуск с настройками разработки +cd Clinic/Clinic.Api +dotnet run +``` + +### Для Docker: +```bash +# Сборка образа +docker build -f Clinic/Clinic.Api/Dockerfile -t clinic-api . + +# Запуск с базой данных (пример) +docker run -p 8080:8080 -e ConnectionStrings__DefaultConnection="Server=host.docker.internal;Database=ClinicDb;User=sa;Password=YourPassword;" clinic-api +``` + +## Переменные окружения + +Для работы проекта необходимо настроить следующие переменные окружения: + +### Development: +- `ASPNETCORE_ENVIRONMENT=Development` +- `ConnectionStrings__DefaultConnection` - строка подключения к БД + +### Production: +- `ASPNETCORE_ENVIRONMENT=Production` +- `ConnectionStrings__DefaultConnection` - строка подключения к продакшн БД + +## Дополнительные возможности + +### Добавление тестов: +1. Создайте тестовый проект: `dotnet new xunit -n Clinic.Tests` +2. Добавьте ссылку на основной проект +3. CI pipeline автоматически запустит тесты + +### Мониторинг: +- Логи Azure App Service доступны через Azure Portal +- Docker контейнеры можно мониторить через Docker Desktop или Azure Container Instances + +### Безопасность: +- Используйте GitHub Secrets для хранения чувствительных данных +- Не коммитьте ключи подключения к БД в репозиторий \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..8b3f0f53c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: Build Check + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore Clinic/Clinic.sln + + - name: Build solution + run: dotnet build Clinic/Clinic.sln --configuration Release --no-restore + + - name: Test solution + run: dotnet test ./Clinic/Clinic.Tests/Clinic.Tests.csproj --configuration Release --no-build --verbosity normal + diff --git a/Clinic/Clinic.Api/Clinic.Api.csproj b/Clinic/Clinic.Api/Clinic.Api.csproj new file mode 100644 index 000000000..87340686c --- /dev/null +++ b/Clinic/Clinic.Api/Clinic.Api.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + + diff --git a/Clinic/Clinic.Api/Controllers/AnalyticsControllers.cs b/Clinic/Clinic.Api/Controllers/AnalyticsControllers.cs new file mode 100644 index 000000000..76c1d928a --- /dev/null +++ b/Clinic/Clinic.Api/Controllers/AnalyticsControllers.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Mvc; +using Clinic.Application.Services; +using Clinic.Application.DTOs.Doctor; +using Clinic.Application.DTOs.Patient; +using Clinic.Application.DTOs.Appointment; + +namespace Clinic.Api.Controllers; + +/// +/// Controller for analytics and business logic queries about doctors, patients, and appointments. +/// +[ApiController] +[Route("api/analytics")] +public class AnalyticsController(AnalyticsServices testServices) : ControllerBase +{ + /// + /// Retrieves a list of doctors with 10 or more years of experience. + /// Returns a 200 OK with a list of GetDoctorDto objects. + /// + [HttpGet("doctors/experience")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public ActionResult> GetDoctorsWithExperience() + { + return Ok(testServices.GetDoctorsWithExperience10YearsOrMore()); + } + + /// + /// Fetches patients assigned to a specific doctor (by doctorId), ordered by their full name. + /// Returns 200 OK with a list of GetPatientDto objects if the doctor exists; otherwise, 404 Not Found with a message. + /// + [HttpGet("patients/doctor")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetPatientsByDoctor(int doctorId) + { + var result = testServices.GetPatientsByDoctorOrderedByFullName(doctorId); + if (result == null) + { + return NotFound("Doctor not found."); + } + return Ok(result); + } + + /// + /// Returns the count of return visits (as an integer) that occurred in the last month. + /// Responds with 200 OK. + /// + [HttpGet("patients/return-visits")] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + public ActionResult GetReturnVisitsCountLastMonth() + { + return Ok(testServices.GetReturnVisitsCountLastMonth()); + } + + /// + /// Gets a list of patients over 30 years old who are associated with multiple doctors, ordered by birth date. + /// Returns 200 OK with a list of GetPatientDto objects. + /// + [HttpGet("patients/over-30")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public ActionResult> GetPatientsOver30WithMultipleDoctors() + { + return Ok(testServices.GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate()); + } + + /// + /// Retrieves appointments scheduled in a specific room (roomNumber) for the current month. + /// Returns 200 OK with a list of GetAppointmentDto objects. + /// + [HttpGet("appointments/room")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public ActionResult> GetAppointmentsInRoomForCurrentMonth(int roomNumber) + { + return Ok(testServices.GetAppointmentsInRoomForCurrentMonth(roomNumber)); + } +} \ No newline at end of file diff --git a/Clinic/Clinic.Api/Controllers/AppointmentControllers.cs b/Clinic/Clinic.Api/Controllers/AppointmentControllers.cs new file mode 100644 index 000000000..65512ce50 --- /dev/null +++ b/Clinic/Clinic.Api/Controllers/AppointmentControllers.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using Clinic.Application.Interfaces.Services; +using Clinic.Application.DTOs.Appointment; + +namespace Clinic.Api.Controllers; + +/// +/// Controller for handling appointment-related operations such as retrieving, creating, updating, +/// and deleting appointments in the clinic. +/// +[ApiController] +[Route("api/appointments")] +public class AppointmentControllers(IAppointmentServices appointmentServices) : BaseControllers(appointmentServices) +{ + /// + /// Gets all appointments for a specific doctor. + /// + /// The doctor's id. + /// ActionResult containing appointments or NotFound if doctor not found. + [HttpGet("doctor/{doctorId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetByDoctor(int doctorId) + { + var appointments = appointmentServices.GetAppointmentsByDoctor(doctorId); + if (appointments == null) + { + return NotFound("Doctor not found."); + } + return Ok(appointments); + } + + /// + /// Gets all appointments for a specific patient. + /// + /// The patient's id. + /// ActionResult containing appointments or NotFound if patient not found. + [HttpGet("patient/{patientId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetByPatient(int patientId) + { + var appointments = appointmentServices.GetAppointmentsByPatient(patientId); + if (appointments == null) + { + return NotFound("Patient not found."); + } + return Ok(appointments); + } +} diff --git a/Clinic/Clinic.Api/Controllers/BaseControllers.cs b/Clinic/Clinic.Api/Controllers/BaseControllers.cs new file mode 100644 index 000000000..739f20baa --- /dev/null +++ b/Clinic/Clinic.Api/Controllers/BaseControllers.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Mvc; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Api.Controllers; + +/// +/// Base controller class that provides common CRUD operations for all controllers. +/// +/// DTO type for retrieving entity data. +/// DTO type for create and update operations. +public class BaseControllers(IBaseServices Service) : ControllerBase + where TGetDto : class + where TSaveDto : class +{ + + /// + /// Gets all entities. + /// + /// ActionResult containing a list of all entities. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public virtual ActionResult> GetAll() + { + var entities = Service.GetAll(); + return Ok(entities); + } + + /// + /// Gets a specific entity by its id. + /// + /// The id of the entity. + /// ActionResult containing the entity or NotFound if not found. + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public virtual ActionResult Get(int id) + { + var entity = Service.Get(id); + if (entity == null) + { + return NotFound(GetEntityName() + " not found."); + } + return Ok(entity); + } + + /// + /// Creates a new entity. + /// + /// The creation data. + /// ActionResult containing the created entity or BadRequest if creation fails. + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public virtual ActionResult Create(TSaveDto dto) + { + var entity = Service.Create(dto); + if (entity == null) + { + return BadRequest("Could not create " + GetEntityName() + "."); + } + var id = GetEntityId(entity); + return CreatedAtAction(nameof(Get), new { id }, entity); + } + + /// + /// Updates an existing entity. + /// + /// The id of the entity to update. + /// The update data. + /// ActionResult containing the updated entity or NotFound if not found. + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public virtual ActionResult Update(int id, TSaveDto dto) + { + var entity = Service.Update(id, dto); + if (entity == null) + { + return NotFound(GetEntityName() + " not found."); + } + return Ok(entity); + } + + /// + /// Deletes an entity by its id. + /// + /// The id of the entity to delete. + /// ActionResult indicating success or NotFound if not found. + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public virtual ActionResult Delete(int id) + { + var result = Service.Delete(id); + if (!result) + { + return NotFound(GetEntityName() + " not found."); + } + return Ok(GetEntityName() + " deleted successfully."); + } + + /// + /// Gets the entity name for error messages. + /// + /// The entity name. + protected virtual string GetEntityName() + { + var name = typeof(TGetDto).Name; + return name.Replace("Get", "").Replace("Dto", ""); + } + + /// + /// Gets the entity ID using reflection. + /// + /// The entity DTO. + /// The entity ID. + protected virtual int GetEntityId(TGetDto entity) + { + var idProperty = typeof(TGetDto).GetProperty("Id"); + if (idProperty != null && idProperty.GetValue(entity) is int id) + { + return id; + } + return 0; + } +} diff --git a/Clinic/Clinic.Api/Controllers/DoctorControllers.cs b/Clinic/Clinic.Api/Controllers/DoctorControllers.cs new file mode 100644 index 000000000..51a0d018a --- /dev/null +++ b/Clinic/Clinic.Api/Controllers/DoctorControllers.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Clinic.Application.DTOs.Doctor; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Api.Controllers; + +/// +/// Controller for handling doctor-related operations such as retrieving, +/// creating, updating, and deleting doctors in the clinic. +/// +[ApiController] +[Route("api/doctors")] +public class DoctorControllers(IDoctorServices doctorServices) : BaseControllers(doctorServices); diff --git a/Clinic/Clinic.Api/Controllers/PatientControllers.cs b/Clinic/Clinic.Api/Controllers/PatientControllers.cs new file mode 100644 index 000000000..75180d8f8 --- /dev/null +++ b/Clinic/Clinic.Api/Controllers/PatientControllers.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Clinic.Application.DTOs.Patient; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Api.Controllers; + +/// +/// Controller for managing patient operations such as retrieving, +/// creating, updating, and deleting patient records. +/// +[ApiController] +[Route("api/patients")] +public class PatientControllers(IPatientServices patientServices) : BaseControllers(patientServices); diff --git a/Clinic/Clinic.Api/Controllers/SpecializationControllers.cs b/Clinic/Clinic.Api/Controllers/SpecializationControllers.cs new file mode 100644 index 000000000..7909a33cb --- /dev/null +++ b/Clinic/Clinic.Api/Controllers/SpecializationControllers.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Clinic.Application.DTOs.Specialization; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Api.Controllers; + +/// +/// Controller for managing specialization operations such as retrieving, +/// creating, and deleting specializations. +/// +[ApiController] +[Route("api/specializations")] +public class SpecializationControllers(ISpecializationServices specializationServices) : BaseControllers(specializationServices); diff --git a/Clinic/Clinic.Api/Converter/DataConverter.cs b/Clinic/Clinic.Api/Converter/DataConverter.cs new file mode 100644 index 000000000..ac63b6a78 --- /dev/null +++ b/Clinic/Clinic.Api/Converter/DataConverter.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Clinic.Api.Converter; + +/// +/// Custom JSON converter for DateOnly type, handling serialization and deserialization using the "yyyy-MM-dd" format. +/// +public class DateConverter : JsonConverter +{ + private const string Format = "yyyy-MM-dd"; + + /// + /// Deserializes a JSON string in "yyyy-MM-dd" format to a DateOnly object. + /// + /// The UTF-8 JSON reader. + /// The type to convert (DateOnly). + /// The serializer options. + /// The parsed DateOnly value. + public override DateOnly Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) + => DateOnly.ParseExact(reader.GetString()!, Format); + + /// + /// Serializes a DateOnly object to a JSON string in "yyyy-MM-dd" format. + /// + /// The UTF-8 JSON writer. + /// The DateOnly value to serialize. + /// The serializer options. + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(Format)); +} \ No newline at end of file diff --git a/Clinic/Clinic.Api/Grpc/ContractIngestService.cs b/Clinic/Clinic.Api/Grpc/ContractIngestService.cs new file mode 100644 index 000000000..13a875a8a --- /dev/null +++ b/Clinic/Clinic.Api/Grpc/ContractIngestService.cs @@ -0,0 +1,140 @@ +using System.Globalization; +using Clinic.Application.Ports; +using Clinic.Contracts; +using Clinic.Models.Entities; +using Grpc.Core; + +namespace Clinic.Api.Grpc; + +/// +/// gRPC service that ingests appointment contracts from a client stream +/// and persists mapped appointments to storage. +/// +public class ContractIngestService : ContractIngestor.ContractIngestorBase +{ + private readonly IAppointmentRepository _appointments; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Appointment repository used to persist mapped contracts. + /// Logger for ingest diagnostics and persistence errors. + public ContractIngestService(IAppointmentRepository appointments, ILogger logger) + { + _appointments = appointments; + _logger = logger; + } + + /// + /// Reads contracts from the incoming gRPC stream, validates and maps them to appointments, + /// and returns aggregated ingest statistics. + /// + /// Incoming stream of contracts. + /// Server call context for cancellation and metadata. + /// Ingest result with total received, saved, failed and error details. + public override async Task IngestContracts(IAsyncStreamReader requestStream, ServerCallContext context) + { + var received = 0; + var saved = 0; + var failed = 0; + var errors = new List(); + + await foreach (var contract in requestStream.ReadAllAsync(context.CancellationToken)) + { + received++; + if (!TryMap(contract, out var appointment, out var error)) + { + failed++; + errors.Add($"#{received}: {error}"); + continue; + } + + try + { + if (_appointments.AddAppointment(appointment)) + { + saved++; + } + else + { + failed++; + errors.Add($"#{received}: Appointment already exists."); + } + } + catch (Exception ex) + { + failed++; + errors.Add($"#{received}: {ex.Message}"); + _logger.LogWarning(ex, "Failed to persist contract #{Index}", received); + } + } + + var result = new IngestResult + { + Received = received, + Saved = saved, + Failed = failed + }; + result.Errors.AddRange(errors); + + return result; + } + + /// + /// Validates contract payload and converts it to an . + /// + /// Source contract. + /// Mapped appointment if conversion succeeds. + /// Validation or conversion error message. + /// true when contract is valid and mapped; otherwise, false. + private static bool TryMap(Contract contract, out Appointment appointment, out string error) + { + appointment = null!; + error = string.Empty; + + if (contract.PatientId <= 0) + { + error = "PatientId must be positive."; + return false; + } + + if (contract.DoctorId <= 0) + { + error = "DoctorId must be positive."; + return false; + } + + if (string.IsNullOrWhiteSpace(contract.PatientFullName)) + { + error = "PatientFullName is required."; + return false; + } + + if (string.IsNullOrWhiteSpace(contract.DoctorFullName)) + { + error = "DoctorFullName is required."; + return false; + } + + if (!DateTimeOffset.TryParse(contract.AppointmentTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset)) + { + error = "AppointmentTime must be ISO-8601."; + return false; + } + + appointment = new Appointment + { + Id = 0, + PatientId = contract.PatientId, + PatientFullName = contract.PatientFullName, + DoctorId = contract.DoctorId, + DoctorFullName = contract.DoctorFullName, + DateTime = dateTimeOffset.UtcDateTime, + RoomNumber = contract.RoomNumber, + IsReturnVisit = contract.IsReturnVisit + }; + + return true; + } +} diff --git a/Clinic/Clinic.Api/Program.cs b/Clinic/Clinic.Api/Program.cs new file mode 100644 index 000000000..74d7d7ddb --- /dev/null +++ b/Clinic/Clinic.Api/Program.cs @@ -0,0 +1,70 @@ +using Clinic.DataBase; +using Clinic.Application.Ports; +using Clinic.DataBase.EntityFramework; +using Clinic.Application.Services.Mapping; +using Microsoft.Extensions.Hosting; +using Clinic.Application.Services; +using Clinic.Api.Converter; +using Clinic.Application.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using Clinic.Api.Grpc; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new DateConverter()); + options.JsonSerializerOptions.PropertyNamingPolicy = null; + }); + +builder.Services.AddGrpc(); + +var connectionString = builder.Configuration.GetConnectionString("ClinicDb") + ?? throw new InvalidOperationException("Connection string 'ClinicDb' is not configured."); + +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddAutoMapper(cfg => cfg.AddProfile()); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Clinic API V1"); + options.RoutePrefix = "swagger"; + }); +} + +app.MapGrpcService(); +app.MapControllers(); +app.Run(); diff --git a/Clinic/Clinic.Api/Properties/launchSettings.json b/Clinic/Clinic.Api/Properties/launchSettings.json new file mode 100644 index 000000000..13035bd70 --- /dev/null +++ b/Clinic/Clinic.Api/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5042", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7015;http://localhost:5042", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Clinic/Clinic.Api/appsettings.Development.json b/Clinic/Clinic.Api/appsettings.Development.json new file mode 100644 index 000000000..3c7c27b74 --- /dev/null +++ b/Clinic/Clinic.Api/appsettings.Development.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "ConnectionStrings": { + "ClinicDb": "Host=localhost;Database=ClinicDb;Username=clinic_app;Password=clinic123;Include Error Detail=true" + } +} diff --git a/Clinic/Clinic.Api/appsettings.json b/Clinic/Clinic.Api/appsettings.json new file mode 100644 index 000000000..4e606cea4 --- /dev/null +++ b/Clinic/Clinic.Api/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "ClinicDb": "Host=localhost;Database=ClinicDb;Username=clinic_app;Password=clinic123" + } +} diff --git a/Clinic/Clinic.AppHost/AppHost.cs b/Clinic/Clinic.AppHost/AppHost.cs new file mode 100644 index 000000000..68e73bbb4 --- /dev/null +++ b/Clinic/Clinic.AppHost/AppHost.cs @@ -0,0 +1,26 @@ +if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL")) && + string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"))) +{ + Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", "http://127.0.0.1:4317"); + Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL", "http://127.0.0.1:4318"); +} + +if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ASPIRE_ALLOW_UNSECURED_TRANSPORT"))) +{ + Environment.SetEnvironmentVariable("ASPIRE_ALLOW_UNSECURED_TRANSPORT", "true"); +} + +var builder = DistributedApplication.CreateBuilder(args); + +var postgresql = builder.AddPostgres("postgres") + .AddDatabase("ClinicDb"); + +var api = builder.AddProject("clinic-api", "../Clinic.Api/Clinic.Api.csproj") + .WithReference(postgresql) + .WithExternalHttpEndpoints(); + +var appointmentGenerator = builder.AddProject("clinic-appointment-generator", "../Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj") + .WithReference(postgresql) + .WithEnvironment("Grpc__Endpoint", api.GetEndpoint("https")); + +builder.Build().Run(); diff --git a/Clinic/Clinic.AppHost/Clinic.AppHost.csproj b/Clinic/Clinic.AppHost/Clinic.AppHost.csproj new file mode 100644 index 000000000..40a19d8f4 --- /dev/null +++ b/Clinic/Clinic.AppHost/Clinic.AppHost.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + 1fced4c2-e45b-4353-84db-0376db0b16fc + + + + + + + + + + + + diff --git a/Clinic/Clinic.AppHost/Properties/launchSettings.json b/Clinic/Clinic.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..29cc2b1ee --- /dev/null +++ b/Clinic/Clinic.AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17260;http://localhost:15245", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21029", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23235", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22009" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15245", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19118", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18229", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20170" + } + } + } +} \ No newline at end of file diff --git a/Clinic/Clinic.AppHost/appsettings.Development.json b/Clinic/Clinic.AppHost/appsettings.Development.json new file mode 100644 index 000000000..ff66ba6b2 --- /dev/null +++ b/Clinic/Clinic.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Clinic/Clinic.AppHost/appsettings.json b/Clinic/Clinic.AppHost/appsettings.json new file mode 100644 index 000000000..2185f9551 --- /dev/null +++ b/Clinic/Clinic.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/Clinic/Clinic.Application.Services/Clinic.Application.Services.csproj b/Clinic/Clinic.Application.Services/Clinic.Application.Services.csproj new file mode 100644 index 000000000..8f0424312 --- /dev/null +++ b/Clinic/Clinic.Application.Services/Clinic.Application.Services.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Clinic/Clinic.Application.Services/Mapping/MappingProfile.cs b/Clinic/Clinic.Application.Services/Mapping/MappingProfile.cs new file mode 100644 index 000000000..d19f3edee --- /dev/null +++ b/Clinic/Clinic.Application.Services/Mapping/MappingProfile.cs @@ -0,0 +1,46 @@ +using AutoMapper; +using Clinic.Application.DTOs.Patient; +using Clinic.Application.DTOs.Doctor; +using Clinic.Application.DTOs.Specialization; +using Clinic.Application.DTOs.Appointment; +using Clinic.Models.Enums; +using Clinic.Models.Entities; + +namespace Clinic.Application.Services.Mapping; + +/// +/// AutoMapper profile for mapping between DTOs and entity models in the Clinic API. +/// Defines the mapping rules for Patient, Doctor, Specialization, and Appointment objects, +/// including custom enum conversions and member mapping settings. +/// +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap() + .ForMember(dest => dest.Gender, opt => opt.MapFrom(src => Enum.Parse(src.Gender))) + .ForMember(dest => dest.BloodGroup, opt => opt.MapFrom(src => Enum.Parse(src.BloodGroup))) + .ForMember(dest => dest.RhesusFactor, opt => opt.MapFrom(src => Enum.Parse(src.RhesusFactor))); + + CreateMap() + .ForMember(dest => dest.Gender, opt => opt.MapFrom(src => src.Gender.ToString())) + .ForMember(dest => dest.BloodGroup, opt => opt.MapFrom(src => src.BloodGroup.ToString())) + .ForMember(dest => dest.RhesusFactor, opt => opt.MapFrom(src => src.RhesusFactor.ToString())); + + CreateMap() + .ForMember(dest => dest.Gender, opt => opt.MapFrom(src => Enum.Parse(src.Gender))) + .ForMember(dest => dest.Specializations, opt => opt.MapFrom(src => src.Specializations)); + + CreateMap() + .ForMember(dest => dest.Specializations, opt => opt.MapFrom(src => src.Specializations.Select(s => s.Name.ToString()).ToList())); + + CreateMap() + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name.ToString())); + + CreateMap(); + + CreateMap(); + + CreateMap(); + } +} diff --git a/Clinic/Clinic.Application.Services/Services/AnalyticsServices.cs b/Clinic/Clinic.Application.Services/Services/AnalyticsServices.cs new file mode 100644 index 000000000..94712e0c0 --- /dev/null +++ b/Clinic/Clinic.Application.Services/Services/AnalyticsServices.cs @@ -0,0 +1,115 @@ +using AutoMapper; +using Clinic.Application.Ports; +using Clinic.Application.DTOs.Patient; +using Clinic.Application.DTOs.Doctor; +using Clinic.Application.DTOs.Appointment; + +namespace Clinic.Application.Services; + +public class AnalyticsServices +{ + private readonly IPatientRepository _patients; + private readonly IDoctorRepository _doctors; + private readonly IAppointmentRepository _appointments; + private readonly IMapper _mapper; + + public AnalyticsServices( + IPatientRepository patients, + IDoctorRepository doctors, + IAppointmentRepository appointments, + IMapper mapper) + { + _patients = patients; + _doctors = doctors; + _appointments = appointments; + _mapper = mapper; + } + + /// + /// Display information about all doctors with at least 10 years of experience. + /// + public IReadOnlyList GetDoctorsWithExperience10YearsOrMore(){ + var doctors = _doctors.GetAllDoctors() + .Where(d => d.ExperienceYears >= 10) + .ToList(); + return _mapper.Map>(doctors); + } + + /// + /// Display information about all patients scheduled to see a specified doctor, sorted by full name. + /// + public IReadOnlyList? GetPatientsByDoctorOrderedByFullName(int doctorId){ + var doctor = _doctors.GetDoctor(doctorId); + if (doctor == null) + { + return null; + } + + var appointments = _appointments.GetAppointmentsByDoctor(doctorId); + var patients = appointments + .Where(a => a.DoctorId == doctorId) + .Select(a => _patients.GetPatient(a.PatientId)) + .Distinct() + .ToList(); + + return _mapper.Map>(patients); + } + + /// + /// Display information about the number of repeat patient visits in the last month. + /// + public int GetReturnVisitsCountLastMonth(){ + var lastMonth = DateTime.Now.AddMonths(-1); + var startOfLastMonth = new DateTime(lastMonth.Year, lastMonth.Month, 1); + var startOfCurrentMonth = startOfLastMonth.AddMonths(1); + + var appointments = _appointments.GetAllAppointments() + .Where(a => a.DateTime >= startOfLastMonth && + a.DateTime < startOfCurrentMonth && + a.IsReturnVisit) + .Count(); + + return appointments; + } + + /// + /// Display information about patients over 30 years old who have appointments with multiple doctors, sorted by birth date. + /// + public IReadOnlyList GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate(){ + var thirtyYearsAgo = DateOnly.FromDateTime(DateTime.Now.AddYears(-30)); + + var appointments = _appointments.GetAllAppointments(); + + var patientsWithMultipleDoctors = appointments + .GroupBy(a => a.PatientId) + .Where(g => g.Select(a => a.DoctorId).Distinct().Count() > 1) + .Select(g => g.Key) + .ToList(); + + var patients = patientsWithMultipleDoctors + .Select(id => _patients.GetPatient(id)) + .Where(p => p != null && p.BirthDate < thirtyYearsAgo) + .OrderBy(p => p!.BirthDate) + .ToList(); + + return _mapper.Map>(patients); + } + + /// + /// Display information about appointments for the current month that are held in the selected room. + /// + public IReadOnlyList GetAppointmentsInRoomForCurrentMonth(int roomNumber){ + var now = DateTime.Now; + var startOfMonth = new DateTime(now.Year, now.Month, 1); + var startOfNextMonth = startOfMonth.AddMonths(1); + + var appointments = _appointments.GetAllAppointments() + .Where(a => a.RoomNumber == roomNumber && + a.DateTime >= startOfMonth && + a.DateTime < startOfNextMonth) + .OrderBy(a => a.DateTime) + .ToList(); + + return _mapper.Map>(appointments); + } +} \ No newline at end of file diff --git a/Clinic/Clinic.Application.Services/Services/AppointmentServices.cs b/Clinic/Clinic.Application.Services/Services/AppointmentServices.cs new file mode 100644 index 000000000..6b7f02cd4 --- /dev/null +++ b/Clinic/Clinic.Application.Services/Services/AppointmentServices.cs @@ -0,0 +1,149 @@ +using AutoMapper; +using Clinic.Application.Ports; +using Clinic.Application.DTOs.Appointment; +using Clinic.Models.Entities; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Application.Services; + +/// +/// Service layer for managing appointments in the clinic. +/// Provides methods for creating, updating, retrieving, and deleting appointments. +/// Handles mapping between DTOs and entity models, and interacts with the appointment database. +/// +public class AppointmentServices : IAppointmentServices +{ + private readonly IAppointmentRepository _appointments; + private readonly IPatientRepository _patients; + private readonly IDoctorRepository _doctors; + private readonly IMapper _mapper; + private int _appointmentId; + + /// + /// Initializes a new instance of the class. + /// + /// The appointment database interface. + /// The patient database interface. + /// The doctor database interface. + /// The AutoMapper interface for DTO and entity mapping. + public AppointmentServices( + IAppointmentRepository appointments, + IPatientRepository patients, + IDoctorRepository doctors, + IMapper mapper) + { + _appointments = appointments; + _patients = patients; + _doctors = doctors; + _mapper = mapper; + _appointmentId = _appointments.AppointmentCount() + 1; + } + + /// + /// Retrieves all appointments from the database. + /// + /// A read-only collection of appointment DTOs. + public IReadOnlyCollection GetAll() + { + var appointments = _appointments.GetAllAppointments(); + return _mapper.Map>(appointments); + } + + /// + /// Retrieves all appointments for a specific doctor. + /// + /// The identifier of the doctor. + /// A collection of appointment DTOs if the doctor exists; otherwise, null. + public IReadOnlyCollection? GetAppointmentsByDoctor(int doctorId) + { + var doctor = _doctors.GetDoctor(doctorId); + if (doctor == null) + { + return null; + } + var appointments = _appointments.GetAppointmentsByDoctor(doctorId); + return _mapper.Map>(appointments); + } + + /// + /// Retrieves all appointments for a specific patient. + /// + /// The identifier of the patient. + /// A collection of appointment DTOs if the patient exists; otherwise, null. + public IReadOnlyCollection? GetAppointmentsByPatient(int patientId) + { + var patient = _patients.GetPatient(patientId); + if (patient == null) + { + return null; + } + var appointments = _appointments.GetAppointmentsByPatient(patientId); + return _mapper.Map>(appointments); + } + + /// + /// Retrieves a single appointment by its identifier. + /// + /// The identifier of the appointment to retrieve. + /// The appointment as a DTO if found; otherwise, null. + public GetAppointmentDto? Get(int id) + { + var appointment = _appointments.GetAppointment(id); + return appointment == null ? null : _mapper.Map(appointment); + } + + /// + /// Creates a new appointment entity in the database. + /// + /// The DTO containing appointment creation data. + /// The created appointment as a DTO if successful; otherwise, null. + public GetAppointmentDto? Create(CreateUpdateAppointmentDto dto) + { + var appointment = _mapper.Map(dto); + appointment.Id = _appointmentId; + + if (!_appointments.AddAppointment(appointment)) + { + return null; + } + + _appointmentId++; + return _mapper.Map(appointment); + } + + /// + /// Updates an existing appointment with the given identifier. + /// + /// The identifier of the appointment to update. + /// The DTO containing updated appointment data. + /// The updated appointment as a DTO if successful; otherwise, null. + public GetAppointmentDto? Update(int id, CreateUpdateAppointmentDto dto) + { + var appointment = _appointments.GetAppointment(id); + if (appointment == null) + { + return null; + } + + _mapper.Map(dto, appointment); + _appointments.UpdateAppointment(appointment); + + return _mapper.Map(appointment); + } + + /// + /// Deletes an appointment from the database by its identifier. + /// + /// The identifier of the appointment to delete. + /// True if the appointment was successfully deleted; otherwise, false. + public bool Delete(int id) + { + if (!_appointments.RemoveAppointment(id)) + { + return false; + } + + _appointmentId--; + return true; + } +} diff --git a/Clinic/Clinic.Application.Services/Services/DoctorServices.cs b/Clinic/Clinic.Application.Services/Services/DoctorServices.cs new file mode 100644 index 000000000..27dcac25a --- /dev/null +++ b/Clinic/Clinic.Application.Services/Services/DoctorServices.cs @@ -0,0 +1,111 @@ +using AutoMapper; +using Clinic.Application.Ports; +using Clinic.Application.DTOs.Doctor; +using Clinic.Models.Entities; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Application.Services; + +/// +/// Service class for managing doctor-related operations within the Clinic API. +/// Provides methods to create, retrieve, update, and delete doctors, +/// as well as functions to get all doctors from the underlying database. +/// Uses AutoMapper for mapping between entity and DTO objects. +/// +public class DoctorServices : IDoctorServices +{ + private readonly IDoctorRepository _db; + private readonly IMapper _mapper; + private int _doctorId; + + /// + /// Initializes a new instance of the class. + /// Sets the initial doctor identifier based on the count in the database. + /// + /// The database service for doctor operations. + /// The AutoMapper instance used for object mapping. + public DoctorServices(IDoctorRepository db, IMapper mapper) + { + _db = db; + _mapper = mapper; + _doctorId = _db.DoctorCount() + 1; + } + + /// + /// Retrieves all doctors from the database and maps them to DTOs. + /// + /// A collection of representing doctors. + public IReadOnlyCollection GetAll() + { + var doctors = _db.GetAllDoctors(); + var doctorsDto = _mapper.Map>(doctors); + return doctorsDto; + } + + /// + /// Creates a new doctor entity in the database. + /// + /// The DTO containing doctor creation data. + /// The created doctor as a if successful; otherwise, null. + public GetDoctorDto? Create(CreateUpdateDoctorDto createDoctorDto) + { + var doctor = _mapper.Map(createDoctorDto); + doctor.Id = _doctorId; + if (!_db.AddDoctor(doctor)) + { + return null; + } + return _mapper.Map(doctor); + } + + /// + /// Retrieves a single doctor by ID. + /// + /// The doctor identifier. + /// A if found; otherwise, null. + public GetDoctorDto? Get(int id) + { + var doctor = _db.GetDoctor(id); + if (doctor == null) + { + return null; + } + var doctorGetDto = _mapper.Map(doctor); + return doctorGetDto; + } + + /// + /// Updates an existing doctor's information. + /// + /// The doctor identifier. + /// DTO with updated doctor details. + /// The updated doctor as a if successful; otherwise, null. + public GetDoctorDto? Update(int id, CreateUpdateDoctorDto updateDoctorDto) + { + var doctor = _db.GetDoctor(id); + if (doctor == null) + { + return null; + } + _mapper.Map(updateDoctorDto, doctor); + _db.UpdateDoctor(doctor); + + var doctorGetDto = _mapper.Map(doctor); + return doctorGetDto; + } + + /// + /// Deletes a doctor from the database by ID. + /// + /// The doctor identifier to delete. + /// True if the doctor was successfully deleted; otherwise, false. + public bool Delete(int id) + { + if (!_db.RemoveDoctor(id)) + { + return false; + } + _doctorId--; + return true; + } +} diff --git a/Clinic/Clinic.Application.Services/Services/PatientServices.cs b/Clinic/Clinic.Application.Services/Services/PatientServices.cs new file mode 100644 index 000000000..8d908acdb --- /dev/null +++ b/Clinic/Clinic.Application.Services/Services/PatientServices.cs @@ -0,0 +1,112 @@ +using AutoMapper; +using Clinic.Application.Ports; +using Clinic.Application.DTOs.Patient; +using Clinic.Models.Entities; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Application.Services; + +/// +/// Service class for managing patient-related operations in the Clinic API. +/// Provides methods for creating, retrieving, updating, and deleting patients, +/// as well as listing all patients. Uses AutoMapper for entity-DTO mapping. +/// +public class PatientServices : IPatientServices +{ + private readonly IPatientRepository _db; + private readonly IMapper _mapper; + private int _patientId; + + /// + /// Initializes a new instance of the class. + /// Sets the initial patient identifier based on the count in the database. + /// + /// The database service for patient operations. + /// The AutoMapper instance for mapping objects. + public PatientServices(IPatientRepository db, IMapper mapper) + { + _db = db; + _mapper = mapper; + _patientId = _db.PatientCount() + 1; + } + + /// + /// Retrieves all patients from the database and maps them to DTOs. + /// + /// A collection of representing all patients. + public IReadOnlyCollection GetAll() + { + var patients = _db.GetAllPatients(); + var patientsDto = _mapper.Map>(patients); + return patientsDto; + } + + /// + /// Creates a new patient entity in the database. + /// + /// The DTO containing patient creation data. + /// The created patient as a if successful; otherwise, null. + public GetPatientDto? Create(CreateUpdatePatientDto patientCreateDto) + { + var patient = _mapper.Map(patientCreateDto); + patient.Id = _patientId; + if (!_db.AddPatient(patient)) + { + return null; + } + _patientId++; + var patientGetDto = _mapper.Map(patient); + return patientGetDto; + } + + /// + /// Updates an existing patient with the given id using the provided update DTO. + /// + /// The identifier of the patient to update. + /// The DTO containing updated patient data. + /// The updated patient as a if successful; otherwise, null. + public GetPatientDto? Update(int id, CreateUpdatePatientDto patientUpdateDto) + { + var patient = _db.GetPatient(id); + if (patient == null) + { + return null; + } + _mapper.Map(patientUpdateDto, patient); + _db.UpdatePatient(patient); + + var patientGetDto = _mapper.Map(patient); + return patientGetDto; + } + + /// + /// Retrieves a patient with the specified id. + /// + /// The identifier of the patient to retrieve. + /// The patient as a if found; otherwise, null. + public GetPatientDto? Get(int id) + { + var patient = _db.GetPatient(id); + if (patient == null) + { + return null; + } + var patientGetDto = _mapper.Map(patient); + return patientGetDto; + } + + /// + /// Deletes the patient with the given id. + /// + /// The identifier of the patient to delete. + /// True if the patient was deleted; otherwise, false. + public bool Delete(int id) + { + if (!_db.RemovePatient(id)) + { + return false; + } + _patientId--; + return true; + } +} diff --git a/Clinic/Clinic.Application.Services/Services/SpecializationServices.cs b/Clinic/Clinic.Application.Services/Services/SpecializationServices.cs new file mode 100644 index 000000000..6f0a5fc75 --- /dev/null +++ b/Clinic/Clinic.Application.Services/Services/SpecializationServices.cs @@ -0,0 +1,107 @@ +using AutoMapper; +using Clinic.Models.Entities; +using Clinic.Application.Ports; +using Clinic.Application.DTOs.Specialization; +using Clinic.Application.Interfaces.Services; + +namespace Clinic.Application.Services; + +/// +/// Service class for managing specialization-related operations in the Clinic API. +/// Provides methods to create, retrieve, and delete specializations, and maps entity objects to DTOs. +/// +public class SpecializationServices : ISpecializationServices +{ + private readonly ISpecializationRepository _db; + private readonly IMapper _mapper; + private int _specializationId; + + /// + /// Initializes a new instance of the class. + /// Sets the initial specialization identifier based on the count in the database. + /// + /// The database service for specialization operations. + /// The AutoMapper instance used for object mapping. + public SpecializationServices(ISpecializationRepository db, IMapper mapper) + { + _db = db; + _mapper = mapper; + _specializationId = _db.SpecializationCount() + 1; + } + + /// + /// Retrieves all specializations from the database and maps them to DTOs. + /// + /// A collection of representing specializations. + public IReadOnlyCollection GetAll() + { + var specializations = _db.GetAllSpecializations(); + var specializationDtos = _mapper.Map>(specializations); + return specializationDtos; + } + + /// + /// Updates an existing specialization with the given identifier. + /// + /// The identifier of the specialization to update. + /// The DTO containing updated specialization data. + /// The updated specialization as a DTO if successful; otherwise, null. + public GetSpecializationDto? Update(int id, CreateUpdateSpecializationDto updateSpecializationDto) + { + var specialization = _mapper.Map(updateSpecializationDto); + var updatedSpecialization = _db.UpdateSpecialization(id, specialization); + if (updatedSpecialization == null) + { + return null; + } + return _mapper.Map(updatedSpecialization); + } + + /// + /// Creates a new specialization entity in the database. + /// + /// The DTO containing specialization creation data. + /// The created specialization as a if successful; otherwise, null. + public GetSpecializationDto? Create(CreateUpdateSpecializationDto createSpecializationDto) + { + var specialization = _mapper.Map(createSpecializationDto); + specialization.Id = _specializationId; + + if (!_db.AddSpecialization(specialization)) + { + return null; + } + + var specializationDto = _mapper.Map(specialization); + specializationDto.Id = _specializationId; + + _specializationId++; + + return specializationDto; + } + + /// + /// Retrieves a single specialization by ID. + /// + /// The specialization identifier. + /// A if found; otherwise, null. + public GetSpecializationDto? Get(int id) + { + var specialization = _db.GetSpecialization(id); + if (specialization == null) + { + return null; + } + return _mapper.Map(specialization); + } + + /// + /// Deletes a specialization by ID. + /// + /// The specialization identifier. + /// True if the specialization was deleted; otherwise, false. + public bool Delete(int id) + { + return _db.RemoveSpecialization(id); + } +} diff --git a/Clinic/Clinic.Application/Clinic.Application.csproj b/Clinic/Clinic.Application/Clinic.Application.csproj new file mode 100644 index 000000000..18a1b78a8 --- /dev/null +++ b/Clinic/Clinic.Application/Clinic.Application.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Clinic/Clinic.Application/DTOs/Appointment/CreateUpdateAppointmentDto.cs b/Clinic/Clinic.Application/DTOs/Appointment/CreateUpdateAppointmentDto.cs new file mode 100644 index 000000000..27b2a73f7 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Appointment/CreateUpdateAppointmentDto.cs @@ -0,0 +1,33 @@ +namespace Clinic.Application.DTOs.Appointment; + +/// +/// DTO for creating a new appointment, including required patient/doctor information, +/// scheduled date/time, room number, and return visit flag. +/// +public class CreateUpdateAppointmentDto +{ + /// + /// The full name of the patient for the appointment. + /// + public required string PatientFullName { get; set; } + + /// + /// The full name of the doctor for the appointment. + /// + public required string DoctorFullName { get; set; } + + /// + /// The date and time of the appointment. + /// + public required DateTime DateTime { get; set; } + + /// + /// The room number where the appointment will take place. + /// + public required int RoomNumber { get; set; } + + /// + /// Indicates whether the appointment is a return visit. + /// + public required bool IsReturnVisit { get; set; } +} diff --git a/Clinic/Clinic.Application/DTOs/Appointment/GetAppointmentDto.cs b/Clinic/Clinic.Application/DTOs/Appointment/GetAppointmentDto.cs new file mode 100644 index 000000000..974efe0a8 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Appointment/GetAppointmentDto.cs @@ -0,0 +1,49 @@ +namespace Clinic.Application.DTOs.Appointment; + +/// +/// DTO for retrieving detailed information about an appointment, +/// including patient and doctor IDs/names, appointment date, room number, and return visit flag. +/// + +public class GetAppointmentDto +{ + /// + /// The unique identifier of the appointment. + /// + public int Id { get; set; } + + /// + /// The ID of the patient associated with the appointment. + /// + public int PatientId { get; set; } + + /// + /// The ID of the doctor associated with the appointment. + /// + public int DoctorId { get; set; } + + /// + /// The full name of the patient. + /// + public string PatientFullName { get; set; } = null!; + + /// + /// The full name of the doctor. + /// + public string DoctorFullName { get; set; } = null!; + + /// + /// The date and time of the appointment. + /// + public DateTime DateTime { get; set; } + + /// + /// The room number where the appointment takes place. + /// + public int RoomNumber { get; set; } + + /// + /// Indicates whether this is a return visit for the patient. + /// + public bool IsReturnVisit { get; set; } +} diff --git a/Clinic/Clinic.Application/DTOs/Doctor/CreateUpdateDoctorDto.cs b/Clinic/Clinic.Application/DTOs/Doctor/CreateUpdateDoctorDto.cs new file mode 100644 index 000000000..bdca43a40 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Doctor/CreateUpdateDoctorDto.cs @@ -0,0 +1,55 @@ +using Clinic.Application.DTOs.Specialization; + +namespace Clinic.Application.DTOs.Doctor; + +/// +/// DTO for creating a new doctor, including required personal information, +/// specialization list, and experience years. +/// +public class CreateUpdateDoctorDto +{ + /// + /// Required: The passport number of the doctor. + /// + public required string PassportNumber { get; set; } + + /// + /// Required: The birth date of the doctor. + /// + public required DateOnly BirthDate { get; set; } + + /// + /// Required: The last name of the doctor. + /// + public required string LastName { get; set; } + + /// + /// Required: The first name of the doctor. + /// + public required string FirstName { get; set; } + + /// + /// Required: The phone number of the doctor. + /// + public required string PhoneNumber { get; set; } + + /// + /// Optional: The patronymic (middle name) of the doctor. + /// + public string? Patronymic { get; set; } + + /// + /// Required: The gender of the doctor. + /// + public required String Gender { get; set; } + + /// + /// Required: The list of specializations for the doctor. + /// + public required List Specializations { get; set; } + + /// + /// Required: The number of years of experience for the doctor. + /// + public required int ExperienceYears { get; set; } +} \ No newline at end of file diff --git a/Clinic/Clinic.Application/DTOs/Doctor/GetDoctorDto.cs b/Clinic/Clinic.Application/DTOs/Doctor/GetDoctorDto.cs new file mode 100644 index 000000000..354a20167 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Doctor/GetDoctorDto.cs @@ -0,0 +1,53 @@ +namespace Clinic.Application.DTOs.Doctor; + +/// +/// DTO for retrieving detailed information about a doctor, +/// including personal details, specialization list, and experience years. +/// +public class GetDoctorDto +{ + /// + /// The unique identifier of the doctor. + /// + public int Id { get; set; } + + /// + /// The passport number of the doctor. + /// + public string PassportNumber { get; set; } = string.Empty; + + /// + /// The birth date of the doctor. + /// + public DateOnly BirthDate { get; set; } + + /// + /// The last name of the doctor. + /// + public string LastName { get; set; } = string.Empty; + + /// + /// The first name of the doctor. + /// + public string FirstName { get; set; } = string.Empty; + + /// + /// Optional: The patronymic (middle name) of the doctor. + /// + public string? Patronymic { get; set; } + + /// + /// The gender of the doctor. + /// + public String Gender { get; set; } = null!; + + /// + /// The list of specializations for the doctor. + /// + public List Specializations { get; set; } = new(); + + /// + /// The number of years of experience for the doctor. + /// + public int ExperienceYears { get; set; } +} \ No newline at end of file diff --git a/Clinic/Clinic.Application/DTOs/Patient/CreateUpdatePatientDto.cs b/Clinic/Clinic.Application/DTOs/Patient/CreateUpdatePatientDto.cs new file mode 100644 index 000000000..380d87779 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Patient/CreateUpdatePatientDto.cs @@ -0,0 +1,58 @@ +namespace Clinic.Application.DTOs.Patient; + +/// +/// DTO for creating a new patient, including required personal information, +/// medical history, and contact details. +/// +public class CreateUpdatePatientDto +{ + /// + /// The patient's first name. + /// + public required string FirstName { get; set; } + + /// + /// The patient's last name. + /// + public required string LastName { get; set; } + + /// + /// Optional patronymic (middle name) of the patient. + /// + public string? Patronymic { get; set; } + + /// + /// The patient's passport number. + /// + public required string PassportNumber { get; set; } + + /// + /// The patient's birth date. + /// + public required DateOnly BirthDate { get; set; } + + /// + /// The patient's gender as a string (e.g. "Male", "Female"). + /// + public required String Gender { get; set; } + + /// + /// The patient's residential address. + /// + public required string Address { get; set; } + + /// + /// The patient's contact phone number. + /// + public required string PhoneNumber { get; set; } + + /// + /// The patient's blood group as a string (e.g. "A", "B", "AB", "O"). + /// + public required String BloodGroup { get; set; } + + /// + /// The patient's rhesus factor as a string (e.g. "Positive", "Negative"). + /// + public required String RhesusFactor { get; set; } +} \ No newline at end of file diff --git a/Clinic/Clinic.Application/DTOs/Patient/GetPatientDto.cs b/Clinic/Clinic.Application/DTOs/Patient/GetPatientDto.cs new file mode 100644 index 000000000..c6fb978e7 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Patient/GetPatientDto.cs @@ -0,0 +1,48 @@ +namespace Clinic.Application.DTOs.Patient; + +/// +/// DTO for retrieving detailed information about a patient, +/// including personal details, medical history, and contact details. +/// +public class GetPatientDto +{ + /// + /// The unique identifier of the patient. + /// + public int Id { get; set; } + + /// + /// The patient's first name. + /// + public string FirstName { get; set; } = null!; + + /// + /// The patient's last name. + /// + public string LastName { get; set; } = null!; + + /// + /// Optional patronymic (middle name) of the patient. + /// + public string? Patronymic { get; set; } + + /// + /// The patient's birth date. + /// + public DateOnly BirthDate { get; set; } + + /// + /// The patient's gender as a string. + /// + public String Gender { get; set; } = null!; + + /// + /// The patient's blood group as a string (A, B, AB, O). + /// + public String BloodGroup { get; set; } = null!; + + /// + /// The patient's rhesus factor as a string (Positive/Negative). + /// + public String RhesusFactor { get; set; } = null!; +} \ No newline at end of file diff --git a/Clinic/Clinic.Application/DTOs/Specialization/CreateUpdateSpecializationDto.cs b/Clinic/Clinic.Application/DTOs/Specialization/CreateUpdateSpecializationDto.cs new file mode 100644 index 000000000..ae9d97c90 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Specialization/CreateUpdateSpecializationDto.cs @@ -0,0 +1,12 @@ +namespace Clinic.Application.DTOs.Specialization; + +/// +/// DTO for creating a new specialization, including required name. +/// +public class CreateUpdateSpecializationDto +{ + /// + /// The name of the specialization to create. + /// + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Clinic/Clinic.Application/DTOs/Specialization/GetSpecializationDto.cs b/Clinic/Clinic.Application/DTOs/Specialization/GetSpecializationDto.cs new file mode 100644 index 000000000..afb1ff261 --- /dev/null +++ b/Clinic/Clinic.Application/DTOs/Specialization/GetSpecializationDto.cs @@ -0,0 +1,18 @@ +namespace Clinic.Application.DTOs.Specialization; + +/// +/// DTO for retrieving detailed information about a specialization, +/// including its unique identifier and name. +/// +public class GetSpecializationDto +{ + /// + /// The unique identifier of the specialization. + /// + public int Id { get; set; } + + /// + /// The name of the specialization. + /// + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/Clinic/Clinic.Application/Interfaces/Services/IAppointmentService.cs b/Clinic/Clinic.Application/Interfaces/Services/IAppointmentService.cs new file mode 100644 index 000000000..bf5df0f6e --- /dev/null +++ b/Clinic/Clinic.Application/Interfaces/Services/IAppointmentService.cs @@ -0,0 +1,24 @@ +using Clinic.Application.DTOs.Appointment; + +namespace Clinic.Application.Interfaces.Services; + +/// +/// Interface for appointment service operations. +/// Provides methods for managing appointments in the clinic system. +/// +public interface IAppointmentServices : IBaseServices +{ + /// + /// Retrieves all appointments for a specific doctor. + /// + /// The identifier of the doctor. + /// A collection of appointment DTOs if the doctor exists; otherwise, null. + public IReadOnlyCollection? GetAppointmentsByDoctor(int doctorId); + + /// + /// Retrieves all appointments for a specific patient. + /// + /// The identifier of the patient. + /// A collection of appointment DTOs if the patient exists; otherwise, null. + public IReadOnlyCollection? GetAppointmentsByPatient(int patientId); +} diff --git a/Clinic/Clinic.Application/Interfaces/Services/IBaseService.cs b/Clinic/Clinic.Application/Interfaces/Services/IBaseService.cs new file mode 100644 index 000000000..ba4fb107b --- /dev/null +++ b/Clinic/Clinic.Application/Interfaces/Services/IBaseService.cs @@ -0,0 +1,48 @@ +namespace Clinic.Application.Interfaces.Services; + +/// +/// Generic interface for application services that provides CRUD operations. +/// This interface can be used to type all service functions with specific DTOs and entities. +/// +/// The entity type used in the database layer. +/// The DTO type for retrieving entities. +/// The DTO type for create and update operations. +public interface IBaseServices + where TGetDto : class + where TSaveDto : class +{ + /// + /// Retrieves all entities from the database and maps them to DTOs. + /// + /// A read-only collection of DTOs representing all entities. + public IReadOnlyCollection GetAll(); + + /// + /// Retrieves a single entity by its identifier. + /// + /// The identifier of the entity to retrieve. + /// The entity as a DTO if found; otherwise, null. + public TGetDto? Get(int id); + + /// + /// Creates a new entity in the database. + /// + /// The DTO containing entity creation data. + /// The created entity as a DTO if successful; otherwise, null. + public TGetDto? Create(TSaveDto createDto); + + /// + /// Updates an existing entity with the given identifier. + /// + /// The identifier of the entity to update. + /// The DTO containing updated entity data. + /// The updated entity as a DTO if successful; otherwise, null. + public TGetDto? Update(int id, TSaveDto updateDto); + + /// + /// Deletes an entity from the database by its identifier. + /// + /// The identifier of the entity to delete. + /// True if the entity was successfully deleted; otherwise, false. + public bool Delete(int id); +} diff --git a/Clinic/Clinic.Application/Interfaces/Services/IDoctorService.cs b/Clinic/Clinic.Application/Interfaces/Services/IDoctorService.cs new file mode 100644 index 000000000..b342c5a2f --- /dev/null +++ b/Clinic/Clinic.Application/Interfaces/Services/IDoctorService.cs @@ -0,0 +1,9 @@ +using Clinic.Application.DTOs.Doctor; + +namespace Clinic.Application.Interfaces.Services; + +/// +/// Interface for doctor service operations. +/// Provides methods for managing doctors in the clinic system. +/// +public interface IDoctorServices : IBaseServices; diff --git a/Clinic/Clinic.Application/Interfaces/Services/IPatientService.cs b/Clinic/Clinic.Application/Interfaces/Services/IPatientService.cs new file mode 100644 index 000000000..df7aaa9ad --- /dev/null +++ b/Clinic/Clinic.Application/Interfaces/Services/IPatientService.cs @@ -0,0 +1,9 @@ +using Clinic.Application.DTOs.Patient; + +namespace Clinic.Application.Interfaces.Services; + +/// +/// Interface for patient service operations. +/// Provides methods for managing patients in the clinic system. +/// +public interface IPatientServices : IBaseServices; diff --git a/Clinic/Clinic.Application/Interfaces/Services/ISpecializationService.cs b/Clinic/Clinic.Application/Interfaces/Services/ISpecializationService.cs new file mode 100644 index 000000000..008b08c0e --- /dev/null +++ b/Clinic/Clinic.Application/Interfaces/Services/ISpecializationService.cs @@ -0,0 +1,10 @@ +using Clinic.Application.DTOs.Specialization; + +namespace Clinic.Application.Interfaces.Services; + +/// +/// Interface for specialization service operations. +/// Provides methods for managing specializations in the clinic system. +/// Note: Specializations do not support update operations. +/// +public interface ISpecializationServices : IBaseServices; diff --git a/Clinic/Clinic.Application/Ports/IAppointmentRepository.cs b/Clinic/Clinic.Application/Ports/IAppointmentRepository.cs new file mode 100644 index 000000000..c8f281194 --- /dev/null +++ b/Clinic/Clinic.Application/Ports/IAppointmentRepository.cs @@ -0,0 +1,52 @@ +using Clinic.Models.Entities; + +namespace Clinic.Application.Ports; + +/// +/// Abstraction for appointment persistence operations. +/// Provides methods to query and modify appointments in the data store. +/// +public interface IAppointmentRepository +{ + /// + /// Retrieves an appointment by identifier. + /// + public Appointment? GetAppointment(int id); + + /// + /// Returns all appointments. + /// + public IReadOnlyCollection GetAllAppointments(); + + /// + /// Returns appointments for the specified doctor. + /// + public IReadOnlyCollection GetAppointmentsByDoctor(int doctorId); + + /// + /// Returns appointments for the specified patient. + /// + public IReadOnlyCollection GetAppointmentsByPatient(int patientId); + + /// + /// Adds a new appointment. Returns true on success. + /// + public bool AddAppointment(Appointment appointment); + + /// + /// Updates an appointment. Returns true if updated. + /// + public bool UpdateAppointment(Appointment appointment); + + /// + /// Removes an appointment by id. Returns true if removed. + /// + public bool RemoveAppointment(int id); + + /// + /// Returns total number of appointments. + /// + public int AppointmentCount(); +} + + diff --git a/Clinic/Clinic.Application/Ports/IDoctorRepository.cs b/Clinic/Clinic.Application/Ports/IDoctorRepository.cs new file mode 100644 index 000000000..4a7367c7e --- /dev/null +++ b/Clinic/Clinic.Application/Ports/IDoctorRepository.cs @@ -0,0 +1,42 @@ +using Clinic.Models.Entities; + +namespace Clinic.Application.Ports; + +/// +/// Abstraction for doctor persistence operations. +/// Implementations should provide CRUD operations for doctor entities. +/// +public interface IDoctorRepository +{ + /// + /// Retrieves a doctor by identifier, or null if not found. + /// + public Doctor? GetDoctor(int id); + + /// + /// Returns all doctors. + /// + public IReadOnlyCollection GetAllDoctors(); + + /// + /// Adds a new doctor. Returns true on success. + /// + public bool AddDoctor(Doctor doctor); + + /// + /// Updates an existing doctor. Returns true if updated. + /// + public bool UpdateDoctor(Doctor doctor); + + /// + /// Removes a doctor by id. Returns true if removed. + /// + public bool RemoveDoctor(int id); + + /// + /// Returns the total count of doctors. + /// + public int DoctorCount(); +} + + diff --git a/Clinic/Clinic.Application/Ports/IPatientRepository.cs b/Clinic/Clinic.Application/Ports/IPatientRepository.cs new file mode 100644 index 000000000..539f0f571 --- /dev/null +++ b/Clinic/Clinic.Application/Ports/IPatientRepository.cs @@ -0,0 +1,42 @@ +using Clinic.Models.Entities; + +namespace Clinic.Application.Ports; + +/// +/// Abstraction for patient persistence operations. +/// Implementations should provide methods to get, add, update and remove patients. +/// +public interface IPatientRepository +{ + /// + /// Retrieves a patient by identifier. + /// + public Patient? GetPatient(int id); + + /// + /// Returns all patients. + /// + public IReadOnlyCollection GetAllPatients(); + + /// + /// Adds a new patient. Returns true on success. + /// + public bool AddPatient(Patient patient); + + /// + /// Updates an existing patient. Returns true if updated. + /// + public bool UpdatePatient(Patient patient); + + /// + /// Removes a patient by id. Returns true if removed. + /// + public bool RemovePatient(int id); + + /// + /// Returns the total count of patients. + /// + public int PatientCount(); +} + + diff --git a/Clinic/Clinic.Application/Ports/ISpecializationRepository.cs b/Clinic/Clinic.Application/Ports/ISpecializationRepository.cs new file mode 100644 index 000000000..d07b612c0 --- /dev/null +++ b/Clinic/Clinic.Application/Ports/ISpecializationRepository.cs @@ -0,0 +1,42 @@ +using Clinic.Models.Entities; + +namespace Clinic.Application.Ports; + +/// +/// Abstraction for specialization persistence operations. +/// Provides methods to query and modify specializations. +/// +public interface ISpecializationRepository +{ + /// + /// Returns all specializations. + /// + public IReadOnlyCollection GetAllSpecializations(); + + /// + /// Adds a new specialization. Returns true on success. + /// + public bool AddSpecialization(Specialization specialization); + + /// + /// Updates an existing specialization and returns the updated entity, or null if not found. + /// + public Specialization? UpdateSpecialization(int id, Specialization specialization); + + /// + /// Retrieves a specialization by identifier. + /// + public Specialization? GetSpecialization(int id); + + /// + /// Removes a specialization by id. Returns true if removed. + /// + public bool RemoveSpecialization(int id); + + /// + /// Returns total number of specializations. + /// + public int SpecializationCount(); +} + + diff --git a/Clinic/Clinic.Application/Properties/launchSettings.json b/Clinic/Clinic.Application/Properties/launchSettings.json new file mode 100644 index 000000000..95a174177 --- /dev/null +++ b/Clinic/Clinic.Application/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5233", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7077;http://localhost:5233", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Clinic/Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj b/Clinic/Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj new file mode 100644 index 000000000..4cef0fdc8 --- /dev/null +++ b/Clinic/Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj @@ -0,0 +1,27 @@ + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/Clinic/Clinic.AppointmentGenerator/Program.cs b/Clinic/Clinic.AppointmentGenerator/Program.cs new file mode 100644 index 000000000..3bf620f21 --- /dev/null +++ b/Clinic/Clinic.AppointmentGenerator/Program.cs @@ -0,0 +1,180 @@ +using System.Globalization; +using Clinic.Contracts; +using Grpc.Net.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +using var loggerFactory = LoggerFactory.Create(builder => +{ + builder + .SetMinimumLevel(LogLevel.Information) + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss "; + }); +}); +var logger = loggerFactory.CreateLogger("Clinic.AppointmentGenerator"); + +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + +var endpoint = configuration["Grpc:Endpoint"]; +if (string.IsNullOrWhiteSpace(endpoint)) +{ + logger.LogError("Missing Grpc:Endpoint configuration."); + return 1; +} + +var aspireHttpsEndpoint = Environment.GetEnvironmentVariable("services__clinic-api__https__0"); +var aspireHttpEndpoint = Environment.GetEnvironmentVariable("services__clinic-api__http__0"); +if (endpoint.Contains("clinic-api", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = aspireHttpsEndpoint ?? aspireHttpEndpoint ?? endpoint; +} + +if (endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) +{ + logger.LogWarning("Using insecure gRPC endpoint {Endpoint}. Enabling Http2UnencryptedSupport.", endpoint); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); +} + +var count = configuration.GetValue("Generator:Count", 10); +var roomNumbers = configuration.GetSection("Generator:RoomNumbers").Get() ?? [101, 102, 201, 202]; +var patients = configuration.GetSection("Generator:Patients").Get() ?? +[ + new PersonSeed(1, "Иванов Иван Иванович"), + new PersonSeed(2, "Петров Петр Петрович"), + new PersonSeed(3, "Сидорова Мария Алексеевна") +]; +var doctors = configuration.GetSection("Generator:Doctors").Get() ?? +[ + new PersonSeed(1, "Смирнов Алексей Николаевич"), + new PersonSeed(2, "Кузнецова Елена Сергеевна"), + new PersonSeed(3, "Волков Дмитрий Олегович") +]; + +var knownVisitPairs = new HashSet<(int PatientId, int DoctorId)>(); + +if (count <= 0) +{ + logger.LogError("Generator:Count must be greater than 0."); + return 1; +} + +if (roomNumbers.Length == 0) +{ + logger.LogError("Generator:RoomNumbers is empty."); + return 1; +} + +if (patients.Length == 0 || doctors.Length == 0) +{ + logger.LogError("Generator:Patients and Generator:Doctors must contain at least one item."); + return 1; +} + +logger.LogInformation( + "Generator started. Endpoint={Endpoint}; Count={Count}; Patients={PatientsCount}; Doctors={DoctorsCount}.", + endpoint, + count, + patients.Length, + doctors.Length); + +//Create gRPC channel and client, then send generated contracts with retry logic for endpoint availability. +const int grpcAttempts = 10; +for (var attempt = 1; attempt <= grpcAttempts; attempt++) +{ + try + { + using var channel = GrpcChannel.ForAddress(endpoint); + var client = new ContractIngestor.ContractIngestorClient(channel); + + using var call = client.IngestContracts(); + var random = new Random(); + + for (var i = 0; i < count; i++) + { + var patient = patients[random.Next(patients.Length)]; + var doctor = doctors[random.Next(doctors.Length)]; + var contract = BuildRandomContract(patient, doctor, roomNumbers, random, knownVisitPairs); + + await call.RequestStream.WriteAsync(contract); + logger.LogInformation( + "Sent contract {Index}/{Total}: PatientId={PatientId}; DoctorId={DoctorId}; Room={Room}; IsReturnVisit={IsReturnVisit}; Time={Time}.", + i + 1, + count, + contract.PatientId, + contract.DoctorId, + contract.RoomNumber, + contract.IsReturnVisit, + contract.AppointmentTime); + } + + await call.RequestStream.CompleteAsync(); + + var result = await call; + logger.LogInformation("Ingest result: Received={Received}; Saved={Saved}; Failed={Failed}.", result.Received, result.Saved, result.Failed); + foreach (var error in result.Errors) + { + logger.LogWarning("Ingest error: {Error}", error); + } + + return 0; + } + catch (Grpc.Core.RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.Unavailable && attempt < grpcAttempts) + { + logger.LogWarning("gRPC endpoint is not ready yet (attempt {Attempt}/{MaxAttempts}). Retrying...", attempt, grpcAttempts); + await Task.Delay(TimeSpan.FromSeconds(2)); + } +} + +logger.LogError("Failed to send contracts to gRPC endpoint after retries."); +return 1; + +/// +/// Creates a randomized appointment contract for gRPC streaming. +/// +/// Source patient identity tuple. +/// Source doctor identity tuple. +/// Available room numbers. +/// Random generator instance. +/// Known patient-doctor pairs that already had visits. +/// Prepared with randomized date and flags. +static Contract BuildRandomContract( + PersonSeed patient, + PersonSeed doctor, + int[] roomNumbers, + Random random, + HashSet<(int PatientId, int DoctorId)> knownVisitPairs) +{ + var pair = (patient.Id, doctor.Id); + var isReturnVisit = knownVisitPairs.Contains(pair); + knownVisitPairs.Add(pair); + + var appointmentTime = DateTimeOffset.UtcNow + .AddDays(random.Next(0, 30)) + .AddHours(random.Next(8, 18)) + .AddMinutes(random.Next(0, 60)) + .ToString("O", CultureInfo.InvariantCulture); + + return new Contract + { + PatientId = patient.Id, + PatientFullName = patient.FullName, + DoctorId = doctor.Id, + DoctorFullName = doctor.FullName, + AppointmentTime = appointmentTime, + RoomNumber = roomNumbers[random.Next(roomNumbers.Length)], + IsReturnVisit = isReturnVisit + }; +} + +/// +/// Seed entity used by the autonomous generator to avoid direct database dependency. +/// +/// Domain identifier used in generated contracts. +/// Full name included in generated contracts. +readonly record struct PersonSeed(int Id, string FullName); diff --git a/Clinic/Clinic.AppointmentGenerator/appsettings.json b/Clinic/Clinic.AppointmentGenerator/appsettings.json new file mode 100644 index 000000000..da85bfcb1 --- /dev/null +++ b/Clinic/Clinic.AppointmentGenerator/appsettings.json @@ -0,0 +1,37 @@ +{ + "Grpc": { + "Endpoint": "https://clinic-api" + }, + "Generator": { + "Count": 10, + "RoomNumbers": [101, 102, 201, 202], + "Patients": [ + { + "Id": 1, + "FullName": "Иванов Иван Иванович" + }, + { + "Id": 2, + "FullName": "Петров Петр Петрович" + }, + { + "Id": 3, + "FullName": "Сидорова Мария Алексеевна" + } + ], + "Doctors": [ + { + "Id": 1, + "FullName": "Смирнов Алексей Николаевич" + }, + { + "Id": 2, + "FullName": "Кузнецова Елена Сергеевна" + }, + { + "Id": 3, + "FullName": "Волков Дмитрий Олегович" + } + ] + } +} diff --git a/Clinic/Clinic.Contracts/Clinic.Contracts.csproj b/Clinic/Clinic.Contracts/Clinic.Contracts.csproj new file mode 100644 index 000000000..f4556af88 --- /dev/null +++ b/Clinic/Clinic.Contracts/Clinic.Contracts.csproj @@ -0,0 +1,19 @@ + + + net8.0 + enable + enable + + + + + + + all + + + + + + + diff --git a/Clinic/Clinic.Contracts/Proto/appointment.proto b/Clinic/Clinic.Contracts/Proto/appointment.proto new file mode 100644 index 000000000..98d156a91 --- /dev/null +++ b/Clinic/Clinic.Contracts/Proto/appointment.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package clinic.contracts; + +option csharp_namespace = "Clinic.Contracts"; + +message Contract { + int32 patient_id = 1; + string patient_full_name = 2; + int32 doctor_id = 3; + string doctor_full_name = 4; + string appointment_time = 5; // ISO-8601 string + int32 room_number = 6; + bool is_return_visit = 7; +} + +message IngestResult { + int32 received = 1; + int32 saved = 2; + int32 failed = 3; + repeated string errors = 4; +} + +service ContractIngestor { + rpc IngestContracts (stream Contract) returns (IngestResult); +} diff --git a/Clinic/Clinic.DataBase/Clinic.DataBase.csproj b/Clinic/Clinic.DataBase/Clinic.DataBase.csproj new file mode 100644 index 000000000..84ced4c32 --- /dev/null +++ b/Clinic/Clinic.DataBase/Clinic.DataBase.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/Clinic/Clinic.DataBase/ClinicDbContext.cs b/Clinic/Clinic.DataBase/ClinicDbContext.cs new file mode 100644 index 000000000..f3548b319 --- /dev/null +++ b/Clinic/Clinic.DataBase/ClinicDbContext.cs @@ -0,0 +1,108 @@ +using Clinic.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Clinic.DataBase; + +/// +/// EF Core for the Clinic application. Configures entities +/// and relationships for patients, doctors, specializations and appointments. +/// +public class ClinicDbContext : DbContext +{ + public ClinicDbContext(DbContextOptions options) : base(options){} + + /// + /// DbSet of patients. + /// + public DbSet Patients => Set(); + + /// + /// DbSet of doctors. + /// + public DbSet Doctors => Set(); + + /// + /// DbSet of specializations. + /// + public DbSet Specializations => Set(); + + /// + /// DbSet of appointments. + /// + public DbSet Appointments => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Configure Patient entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.PassportNumber).IsRequired().HasMaxLength(50); + entity.Property(e => e.BirthDate).IsRequired(); + entity.Property(e => e.LastName).IsRequired().HasMaxLength(100); + entity.Property(e => e.FirstName).IsRequired().HasMaxLength(100); + entity.Property(e => e.Patronymic).HasMaxLength(100); + entity.Property(e => e.PhoneNumber).IsRequired().HasMaxLength(20); + entity.Property(e => e.Gender).IsRequired().HasConversion(); + entity.Property(e => e.Address).IsRequired().HasMaxLength(200); + entity.Property(e => e.BloodGroup).IsRequired().HasConversion(); + entity.Property(e => e.RhesusFactor).IsRequired().HasConversion(); + }); + + // Configure Doctor entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.PassportNumber).IsRequired().HasMaxLength(50); + entity.Property(e => e.BirthDate).IsRequired(); + entity.Property(e => e.LastName).IsRequired().HasMaxLength(100); + entity.Property(e => e.FirstName).IsRequired().HasMaxLength(100); + entity.Property(e => e.Patronymic).HasMaxLength(100); + entity.Property(e => e.PhoneNumber).IsRequired().HasMaxLength(20); + entity.Property(e => e.Gender).IsRequired().HasConversion(); + entity.Property(e => e.ExperienceYears).IsRequired(); + + // Configure many-to-many relationship with Specialization + entity.HasMany(d => d.Specializations) + .WithMany() + .UsingEntity>( + "DoctorSpecialization", + j => j.HasOne().WithMany().HasForeignKey("SpecializationId"), + j => j.HasOne().WithMany().HasForeignKey("DoctorId"), + j => j.HasKey("DoctorId", "SpecializationId")); + }); + + // Configure Specialization entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(100); + }); + + // Configure Appointment entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.PatientId).IsRequired(); + entity.Property(e => e.PatientFullName).IsRequired().HasMaxLength(300); + entity.Property(e => e.DoctorId).IsRequired(); + entity.Property(e => e.DoctorFullName).IsRequired().HasMaxLength(300); + entity.Property(e => e.DateTime).IsRequired(); + entity.Property(e => e.RoomNumber).IsRequired(); + entity.Property(e => e.IsReturnVisit).IsRequired(); + + // Configure foreign key relationships + entity.HasOne() + .WithMany() + .HasForeignKey(a => a.PatientId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne() + .WithMany() + .HasForeignKey(a => a.DoctorId) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} + + diff --git a/Clinic/Clinic.DataBase/ClinicDbFactory.cs b/Clinic/Clinic.DataBase/ClinicDbFactory.cs new file mode 100644 index 000000000..d251bd5ac --- /dev/null +++ b/Clinic/Clinic.DataBase/ClinicDbFactory.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace Clinic.DataBase; + +/// +/// Factory used at design-time to create instances of . +/// This is used by EF Core tools (migrations, scaffolding) to create a context. +/// +public class ClinicDbFactory : IDesignTimeDbContextFactory +{ + + /// + /// Creates a new for design-time operations. + /// + /// Command-line arguments (unused). + /// A configured instance. + public ClinicDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + var connectionString = configuration.GetConnectionString("ClinicDb"); + + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("Connection string 'ClinicDb' not found."); + } + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql( + connectionString + ); + + return new ClinicDbContext(optionsBuilder.Options); + } + +} \ No newline at end of file diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfAppointmentRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfAppointmentRepository.cs new file mode 100644 index 000000000..d26efb17b --- /dev/null +++ b/Clinic/Clinic.DataBase/EntityFramework/EfAppointmentRepository.cs @@ -0,0 +1,106 @@ +using Clinic.Models.Entities; +using Clinic.Application.Ports; +using Microsoft.EntityFrameworkCore; + +namespace Clinic.DataBase.EntityFramework; + +/// +/// Entity Framework implementation of that manages +/// appointment entities using a . +/// +public sealed class EfAppointmentRepository : IAppointmentRepository +{ + private readonly ClinicDbContext _context; + + /// + /// Initializes a new instance of the class. + /// + /// The database context used for data access. + public EfAppointmentRepository(ClinicDbContext context) + { + _context = context; + } + + /// + /// Retrieves an appointment by identifier. + /// + public Appointment? GetAppointment(int id) => + _context.Appointments.Find(id); + + /// + /// Returns all appointments as a read-only collection. + /// + public IReadOnlyCollection GetAllAppointments() => + _context.Appointments.AsNoTracking().ToList(); + + /// + /// Returns appointments for the specified doctor. + /// + public IReadOnlyCollection GetAppointmentsByDoctor(int doctorId) => + _context.Appointments + .Where(a => a.DoctorId == doctorId) + .AsNoTracking() + .ToList(); + + /// + /// Returns appointments for the specified patient. + /// + public IReadOnlyCollection GetAppointmentsByPatient(int patientId) => + _context.Appointments + .Where(a => a.PatientId == patientId) + .AsNoTracking() + .ToList(); + + /// + /// Adds a new appointment. Returns true on success. + /// + public bool AddAppointment(Appointment appointment) + { + if (_context.Appointments.Any(a => a.Id == appointment.Id)) + { + return false; + } + + _context.Appointments.Add(appointment); + _context.SaveChanges(); + return true; + } + + /// + /// Updates an existing appointment. Returns true if updated. + /// + public bool UpdateAppointment(Appointment appointment) + { + if (!_context.Appointments.Any(a => a.Id == appointment.Id)) + { + return false; + } + + _context.Appointments.Update(appointment); + _context.SaveChanges(); + return true; + } + + /// + /// Removes an appointment by id. Returns true if removed. + /// + public bool RemoveAppointment(int id) + { + var entity = _context.Appointments.Find(id); + if (entity is null) + { + return false; + } + + _context.Appointments.Remove(entity); + _context.SaveChanges(); + return true; + } + + /// + /// Returns total number of appointments. + /// + public int AppointmentCount() => _context.Appointments.Count(); +} + + diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfDoctorRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfDoctorRepository.cs new file mode 100644 index 000000000..e3e8d198b --- /dev/null +++ b/Clinic/Clinic.DataBase/EntityFramework/EfDoctorRepository.cs @@ -0,0 +1,93 @@ +using Clinic.Models.Entities; +using Clinic.Application.Ports; +using Microsoft.EntityFrameworkCore; + +namespace Clinic.DataBase.EntityFramework; + +/// +/// Entity Framework implementation of that manages +/// doctor entities via . +/// +public sealed class EfDoctorRepository : IDoctorRepository +{ + private readonly ClinicDbContext _context; + + /// + /// Initializes a new instance of the class. + /// + /// The database context used for data access. + public EfDoctorRepository(ClinicDbContext context) + { + _context = context; + } + + /// + /// Retrieves a doctor by identifier, including specializations. + /// + public Doctor? GetDoctor(int id) => + _context.Doctors + .Include(d => d.Specializations) + .FirstOrDefault(d => d.Id == id); + + /// + /// Returns all doctors including their specializations. + /// + public IReadOnlyCollection GetAllDoctors() => + _context.Doctors + .Include(d => d.Specializations) + .AsNoTracking() + .ToList(); + + /// + /// Adds a new doctor. Returns true on success. + /// + public bool AddDoctor(Doctor doctor) + { + if (_context.Doctors.Any(d => d.Id == doctor.Id)) + { + return false; + } + + _context.Doctors.Add(doctor); + _context.SaveChanges(); + return true; + } + + /// + /// Updates an existing doctor. Returns true if updated. + /// + public bool UpdateDoctor(Doctor doctor) + { + if (!_context.Doctors.Any(d => d.Id == doctor.Id)) + { + return false; + } + + _context.Doctors.Update(doctor); + _context.SaveChanges(); + return true; + } + + /// + /// Removes a doctor by id. Returns true if removed. + /// + public bool RemoveDoctor(int id) + { + var entity = _context.Doctors.Find(id); + if (entity is null) + { + return false; + } + + _context.Doctors.Remove(entity); + _context.SaveChanges(); + return true; + } + + /// + /// Returns total count of doctors. + /// + public int DoctorCount() => _context.Doctors.Count(); +} + + diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfPatientRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfPatientRepository.cs new file mode 100644 index 000000000..086f183a8 --- /dev/null +++ b/Clinic/Clinic.DataBase/EntityFramework/EfPatientRepository.cs @@ -0,0 +1,87 @@ +using Clinic.Models.Entities; +using Clinic.Application.Ports; +using Microsoft.EntityFrameworkCore; + +namespace Clinic.DataBase.EntityFramework; + +/// +/// Entity Framework implementation of that manages +/// patient entities using a . +/// +public sealed class EfPatientRepository : IPatientRepository +{ + private readonly ClinicDbContext _context; + + /// + /// Initializes a new instance of the class. + /// + /// The database context used for data access. + public EfPatientRepository(ClinicDbContext context) + { + _context = context; + } + + /// + /// Retrieves a patient by identifier. + /// + public Patient? GetPatient(int id) => _context.Patients.Find(id); + + /// + /// Returns all patients from the database as a read-only collection. + /// + public IReadOnlyCollection GetAllPatients() => + _context.Patients.AsNoTracking().ToList(); + + /// + /// Adds a new patient to the database. Returns true on success. + /// + public bool AddPatient(Patient patient) + { + if (_context.Patients.Any(p => p.Id == patient.Id)) + { + return false; + } + + _context.Patients.Add(patient); + _context.SaveChanges(); + return true; + } + + /// + /// Updates an existing patient. Returns true if the patient existed and was updated. + /// + public bool UpdatePatient(Patient patient) + { + if (!_context.Patients.Any(p => p.Id == patient.Id)) + { + return false; + } + + _context.Patients.Update(patient); + _context.SaveChanges(); + return true; + } + + /// + /// Removes a patient by identifier. Returns true if removed. + /// + public bool RemovePatient(int id) + { + var entity = _context.Patients.Find(id); + if (entity is null) + { + return false; + } + + _context.Patients.Remove(entity); + _context.SaveChanges(); + return true; + } + + /// + /// Returns total number of patients in the database. + /// + public int PatientCount() => _context.Patients.Count(); +} + + diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfSpecializationRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfSpecializationRepository.cs new file mode 100644 index 000000000..fa2b69d8d --- /dev/null +++ b/Clinic/Clinic.DataBase/EntityFramework/EfSpecializationRepository.cs @@ -0,0 +1,90 @@ +using Clinic.Models.Entities; +using Clinic.Application.Ports; +using Microsoft.EntityFrameworkCore; + +namespace Clinic.DataBase.EntityFramework; + +/// +/// Entity Framework implementation of that manages +/// specialization entities using . +/// +public sealed class EfSpecializationRepository : ISpecializationRepository +{ + private readonly ClinicDbContext _context; + + /// + /// Initializes a new instance of the class. + /// + /// The database context used for data access. + public EfSpecializationRepository(ClinicDbContext context) + { + _context = context; + } + + /// + /// Returns all specializations as a read-only collection. + /// + public IReadOnlyCollection GetAllSpecializations() => + _context.Specializations.AsNoTracking().ToList(); + + /// + /// Adds a new specialization. Returns true on success. + /// + public bool AddSpecialization(Specialization specialization) + { + if (_context.Specializations.Any(s => s.Id == specialization.Id)) + { + return false; + } + + _context.Specializations.Add(specialization); + _context.SaveChanges(); + return true; + } + + /// + /// Updates a specialization by id and returns the updated entity, or null if not found. + /// + public Specialization? UpdateSpecialization(int id, Specialization specialization) + { + var existing = _context.Specializations.Find(id); + if (existing is null) + { + return null; + } + + existing.Name = specialization.Name; + _context.SaveChanges(); + return existing; + } + + + /// + /// Retrieves a specialization by identifier. + /// + public Specialization? GetSpecialization(int id) => + _context.Specializations.Find(id); + + /// + /// Removes a specialization by id. Returns true if removed. + /// + public bool RemoveSpecialization(int id) + { + var entity = _context.Specializations.Find(id); + if (entity is null) + { + return false; + } + + _context.Specializations.Remove(entity); + _context.SaveChanges(); + return true; + } + + /// + /// Returns total number of specializations. + /// + public int SpecializationCount() => _context.Specializations.Count(); +} + + diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.Designer.cs b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.Designer.cs new file mode 100644 index 000000000..557d05685 --- /dev/null +++ b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.Designer.cs @@ -0,0 +1,235 @@ +// +using System; +using Clinic.DataBase; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Clinic.DataBase.Migrations +{ + [DbContext(typeof(ClinicDbContext))] + [Migration("20251222091310_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Clinic.Models.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DoctorFullName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("IsReturnVisit") + .HasColumnType("boolean"); + + b.Property("PatientFullName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("PatientId") + .HasColumnType("integer"); + + b.Property("RoomNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DoctorId"); + + b.HasIndex("PatientId"); + + b.ToTable("Appointments"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Doctor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("ExperienceYears") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.ToTable("Doctors"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("BloodGroup") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RhesusFactor") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Patients"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Specialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.ToTable("Specializations"); + }); + + modelBuilder.Entity("DoctorSpecialization", b => + { + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("SpecializationId") + .HasColumnType("integer"); + + b.HasKey("DoctorId", "SpecializationId"); + + b.HasIndex("SpecializationId"); + + b.ToTable("DoctorSpecialization"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Appointment", b => + { + b.HasOne("Clinic.Models.Entities.Doctor", null) + .WithMany() + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Clinic.Models.Entities.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("DoctorSpecialization", b => + { + b.HasOne("Clinic.Models.Entities.Doctor", null) + .WithMany() + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Clinic.Models.Entities.Specialization", null) + .WithMany() + .HasForeignKey("SpecializationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.cs b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.cs new file mode 100644 index 000000000..c14857f80 --- /dev/null +++ b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.cs @@ -0,0 +1,160 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Clinic.DataBase.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Doctors", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ExperienceYears = table.Column(type: "integer", nullable: false), + PassportNumber = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + BirthDate = table.Column(type: "date", nullable: false), + LastName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + FirstName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Patronymic = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + PhoneNumber = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Gender = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Doctors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Patients", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Address = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + BloodGroup = table.Column(type: "integer", nullable: false), + RhesusFactor = table.Column(type: "integer", nullable: false), + PassportNumber = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + BirthDate = table.Column(type: "date", nullable: false), + LastName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + FirstName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Patronymic = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + PhoneNumber = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Gender = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Patients", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Specializations", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Specializations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Appointments", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PatientId = table.Column(type: "integer", nullable: false), + PatientFullName = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + DoctorId = table.Column(type: "integer", nullable: false), + DoctorFullName = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + DateTime = table.Column(type: "timestamp with time zone", nullable: false), + RoomNumber = table.Column(type: "integer", nullable: false), + IsReturnVisit = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Appointments", x => x.Id); + table.ForeignKey( + name: "FK_Appointments_Doctors_DoctorId", + column: x => x.DoctorId, + principalTable: "Doctors", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Appointments_Patients_PatientId", + column: x => x.PatientId, + principalTable: "Patients", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "DoctorSpecialization", + columns: table => new + { + DoctorId = table.Column(type: "integer", nullable: false), + SpecializationId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DoctorSpecialization", x => new { x.DoctorId, x.SpecializationId }); + table.ForeignKey( + name: "FK_DoctorSpecialization_Doctors_DoctorId", + column: x => x.DoctorId, + principalTable: "Doctors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DoctorSpecialization_Specializations_SpecializationId", + column: x => x.SpecializationId, + principalTable: "Specializations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Appointments_DoctorId", + table: "Appointments", + column: "DoctorId"); + + migrationBuilder.CreateIndex( + name: "IX_Appointments_PatientId", + table: "Appointments", + column: "PatientId"); + + migrationBuilder.CreateIndex( + name: "IX_DoctorSpecialization_SpecializationId", + table: "DoctorSpecialization", + column: "SpecializationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Appointments"); + + migrationBuilder.DropTable( + name: "DoctorSpecialization"); + + migrationBuilder.DropTable( + name: "Patients"); + + migrationBuilder.DropTable( + name: "Doctors"); + + migrationBuilder.DropTable( + name: "Specializations"); + } + } +} diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.Designer.cs b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.Designer.cs new file mode 100644 index 000000000..6181cd4b4 --- /dev/null +++ b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.Designer.cs @@ -0,0 +1,235 @@ +// +using System; +using Clinic.DataBase; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Clinic.DataBase.Migrations +{ + [DbContext(typeof(ClinicDbContext))] + [Migration("20251222091340_SeedData")] + partial class SeedData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Clinic.Models.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DoctorFullName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("IsReturnVisit") + .HasColumnType("boolean"); + + b.Property("PatientFullName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("PatientId") + .HasColumnType("integer"); + + b.Property("RoomNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DoctorId"); + + b.HasIndex("PatientId"); + + b.ToTable("Appointments"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Doctor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("ExperienceYears") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.ToTable("Doctors"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("BloodGroup") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RhesusFactor") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Patients"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Specialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.ToTable("Specializations"); + }); + + modelBuilder.Entity("DoctorSpecialization", b => + { + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("SpecializationId") + .HasColumnType("integer"); + + b.HasKey("DoctorId", "SpecializationId"); + + b.HasIndex("SpecializationId"); + + b.ToTable("DoctorSpecialization"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Appointment", b => + { + b.HasOne("Clinic.Models.Entities.Doctor", null) + .WithMany() + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Clinic.Models.Entities.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("DoctorSpecialization", b => + { + b.HasOne("Clinic.Models.Entities.Doctor", null) + .WithMany() + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Clinic.Models.Entities.Specialization", null) + .WithMany() + .HasForeignKey("SpecializationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.cs b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.cs new file mode 100644 index 000000000..972142329 --- /dev/null +++ b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.cs @@ -0,0 +1,163 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Clinic.DataBase.Migrations +{ + /// + public partial class SeedData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Seed Specializations + migrationBuilder.InsertData( + table: "Specializations", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Терапевт" }, + { 2, "Хирург" }, + { 3, "Кардиолог" }, + { 4, "Невролог" }, + { 5, "Офтальмолог" }, + { 6, "Отоларинголог" }, + { 7, "Дерматолог" }, + { 8, "Педиатр" }, + { 9, "Гинеколог" }, + { 10, "Уролог" } + }); + + // Seed Doctors (10 doctors) + migrationBuilder.InsertData( + table: "Doctors", + columns: new[] { "Id", "PassportNumber", "BirthDate", "LastName", "FirstName", "Patronymic", "PhoneNumber", "Gender", "ExperienceYears" }, + values: new object[,] + { + { 1, "AB1234567", new DateOnly(1975, 5, 15), "Иванов", "Иван", "Иванович", "+7-900-123-45-67", 0, 15 }, + { 2, "CD2345678", new DateOnly(1980, 8, 22), "Петрова", "Мария", "Сергеевна", "+7-900-234-56-78", 1, 12 }, + { 3, "EF3456789", new DateOnly(1972, 3, 10), "Сидоров", "Александр", "Петрович", "+7-900-345-67-89", 0, 20 }, + { 4, "GH4567890", new DateOnly(1985, 11, 5), "Козлова", "Елена", "Владимировна", "+7-900-456-78-90", 1, 8 }, + { 5, "IJ5678901", new DateOnly(1978, 7, 30), "Морозов", "Дмитрий", "Александрович", "+7-900-567-89-01", 0, 14 }, + { 6, "KL6789012", new DateOnly(1982, 4, 18), "Волкова", "Анна", "Дмитриевна", "+7-900-678-90-12", 1, 10 }, + { 7, "MN7890123", new DateOnly(1976, 9, 25), "Лебедев", "Сергей", "Викторович", "+7-900-789-01-23", 0, 16 }, + { 8, "OP8901234", new DateOnly(1988, 1, 12), "Новикова", "Ольга", "Андреевна", "+7-900-890-12-34", 1, 6 }, + { 9, "QR9012345", new DateOnly(1974, 6, 8), "Федоров", "Максим", "Сергеевич", "+7-900-901-23-45", 0, 18 }, + { 10, "ST0123456", new DateOnly(1983, 12, 20), "Смирнова", "Татьяна", "Игоревна", "+7-900-012-34-56", 1, 11 } + }); + + // Seed Doctor-Specialization relationships + migrationBuilder.InsertData( + table: "DoctorSpecialization", + columns: new[] { "DoctorId", "SpecializationId" }, + values: new object[,] + { + { 1, 1 }, + { 1, 3 }, + { 2, 2 }, + { 3, 4 }, + { 3, 1 }, + { 4, 5 }, + { 5, 6 }, + { 5, 7 }, + { 6, 8 }, + { 7, 2 }, + { 7, 10 }, + { 8, 9 }, + { 9, 3 }, + { 9, 1 }, + { 10, 5 }, + { 10, 6 } + }); + + // Seed Patients (10 patients) + migrationBuilder.InsertData( + table: "Patients", + columns: new[] { "Id", "PassportNumber", "BirthDate", "LastName", "FirstName", "Patronymic", "PhoneNumber", "Gender", "Address", "BloodGroup", "RhesusFactor" }, + values: new object[,] + { + { 1, "KL6789012", new DateOnly(1970, 2, 14), "Смирнов", "Алексей", "Игоревич", "+7-901-111-11-11", 0, "г. Москва, ул. Ленина, д. 10, кв. 5", 0, 0 }, + { 2, "MN7890123", new DateOnly(1975, 6, 20), "Волкова", "Анна", "Дмитриевна", "+7-901-222-22-22", 1, "г. Москва, ул. Пушкина, д. 25, кв. 12", 1, 0 }, + { 3, "OP8901234", new DateOnly(1972, 9, 8), "Лебедев", "Сергей", "Викторович", "+7-901-333-33-33", 0, "г. Москва, пр. Мира, д. 50, кв. 8", 2, 1 }, + { 4, "QR9012345", new DateOnly(1988, 12, 3), "Новикова", "Ольга", "Андреевна", "+7-901-444-44-44", 1, "г. Москва, ул. Гагарина, д. 15, кв. 20", 3, 0 }, + { 5, "ST0123456", new DateOnly(1995, 4, 17), "Федоров", "Максим", "Сергеевич", "+7-901-555-55-55", 0, "г. Москва, ул. Чехова, д. 30, кв. 15", 0, 1 }, + { 6, "UV1234567", new DateOnly(1987, 7, 22), "Кузнецова", "Екатерина", "Александровна", "+7-901-666-66-66", 1, "г. Москва, ул. Тверская, д. 5, кв. 3", 1, 1 }, + { 7, "WX2345678", new DateOnly(1993, 11, 30), "Попов", "Андрей", "Николаевич", "+7-901-777-77-77", 0, "г. Москва, ул. Арбат, д. 20, кв. 7", 2, 0 }, + { 8, "YZ3456789", new DateOnly(1989, 3, 15), "Соколова", "Мария", "Владимировна", "+7-901-888-88-88", 1, "г. Москва, ул. Садовая, д. 12, кв. 9", 3, 1 }, + { 9, "AA4567890", new DateOnly(1991, 8, 5), "Михайлов", "Игорь", "Борисович", "+7-901-999-99-99", 0, "г. Москва, ул. Невский, д. 40, кв. 11", 0, 0 }, + { 10, "BB5678901", new DateOnly(1986, 1, 28), "Павлова", "Наталья", "Сергеевна", "+7-901-000-00-00", 1, "г. Москва, ул. Красная, д. 8, кв. 4", 1, 0 } + }); + + // Seed Appointments (15 appointments) + migrationBuilder.InsertData( + table: "Appointments", + columns: new[] { "Id", "PatientId", "PatientFullName", "DoctorId", "DoctorFullName", "DateTime", "RoomNumber", "IsReturnVisit" }, + values: new object[,] + { + { 1, 1, "Смирнов Алексей Игоревич", 1, "Иванов Иван Иванович", new DateTime(2025, 12, 10, 10, 0, 0, 0, DateTimeKind.Utc), 101, false }, + { 2, 2, "Волкова Анна Дмитриевна", 2, "Петрова Мария Сергеевна", new DateTime(2025, 12, 10, 11, 30, 0, 0, DateTimeKind.Utc), 205, false }, + { 3, 3, "Лебедев Сергей Викторович", 3, "Сидоров Александр Петрович", new DateTime(2025, 8, 21, 9, 0, 0, 0, DateTimeKind.Utc), 302, true }, + { 4, 4, "Новикова Ольга Андреевна", 4, "Козлова Елена Владимировна", new DateTime(2025, 12, 21, 14, 0, 0, 0, DateTimeKind.Utc), 401, false }, + { 5, 5, "Федоров Максим Сергеевич", 5, "Морозов Дмитрий Александрович", new DateTime(2025, 12, 22, 10, 30, 0, 0, DateTimeKind.Utc), 503, false }, + { 6, 6, "Кузнецова Екатерина Александровна", 6, "Волкова Анна Дмитриевна", new DateTime(2025, 12, 22, 15, 0, 0, 0, DateTimeKind.Utc), 201, false }, + { 7, 7, "Попов Андрей Николаевич", 7, "Лебедев Сергей Викторович", new DateTime(2025, 5, 13, 9, 30, 0, 0, DateTimeKind.Utc), 305, true }, + { 8, 8, "Соколова Мария Владимировна", 8, "Новикова Ольга Андреевна", new DateTime(2025, 6, 23, 11, 0, 0, 0, DateTimeKind.Utc), 402, false }, + { 9, 9, "Михайлов Игорь Борисович", 9, "Федоров Максим Сергеевич", new DateTime(2025, 12, 24, 10, 0, 0, 0, DateTimeKind.Utc), 104, false }, + { 10, 10, "Павлова Наталья Сергеевна", 10, "Смирнова Татьяна Игоревна", new DateTime(2025, 12, 24, 13, 30, 0, 0, DateTimeKind.Utc), 501, false }, + { 11, 1, "Смирнов Алексей Игоревич", 2, "Петрова Мария Сергеевна", new DateTime(2025, 3, 25, 10, 0, 0, 0, DateTimeKind.Utc), 101, true }, + { 12, 3, "Лебедев Сергей Викторович", 5, "Морозов Дмитрий Александрович", new DateTime(2025, 4, 25, 14, 0, 0, 0, DateTimeKind.Utc), 302, true }, + { 13, 5, "Федоров Максим Сергеевич", 5, "Морозов Дмитрий Александрович", new DateTime(2025, 11, 26, 9, 0, 0, 0, DateTimeKind.Utc), 503, true }, + { 14, 7, "Попов Андрей Николаевич", 7, "Лебедев Сергей Викторович", new DateTime(2025, 11, 26, 11, 30, 0, 0, DateTimeKind.Utc), 305, false }, + { 15, 9, "Михайлов Игорь Борисович", 9, "Федоров Максим Сергеевич", new DateTime(2025, 11, 27, 10, 30, 0, 0, DateTimeKind.Utc), 104, true } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Remove seed data in reverse order + migrationBuilder.DeleteData( + table: "Appointments", + keyColumn: "Id", + keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + + migrationBuilder.DeleteData( + table: "Patients", + keyColumn: "Id", + keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + + migrationBuilder.DeleteData( + table: "DoctorSpecialization", + keyColumns: new[] { "DoctorId", "SpecializationId" }, + keyValues: new object[,] + { + { 1, 1 }, + { 1, 3 }, + { 2, 2 }, + { 3, 4 }, + { 3, 1 }, + { 4, 5 }, + { 5, 6 }, + { 5, 7 }, + { 6, 8 }, + { 7, 2 }, + { 7, 10 }, + { 8, 9 }, + { 9, 3 }, + { 9, 1 }, + { 10, 5 }, + { 10, 6 } + }); + + migrationBuilder.DeleteData( + table: "Doctors", + keyColumn: "Id", + keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + + migrationBuilder.DeleteData( + table: "Specializations", + keyColumn: "Id", + keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } + } +} diff --git a/Clinic/Clinic.DataBase/Migrations/ClinicDbContextModelSnapshot.cs b/Clinic/Clinic.DataBase/Migrations/ClinicDbContextModelSnapshot.cs new file mode 100644 index 000000000..603e7633d --- /dev/null +++ b/Clinic/Clinic.DataBase/Migrations/ClinicDbContextModelSnapshot.cs @@ -0,0 +1,232 @@ +// +using System; +using Clinic.DataBase; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Clinic.DataBase.Migrations +{ + [DbContext(typeof(ClinicDbContext))] + partial class ClinicDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Clinic.Models.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DoctorFullName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("IsReturnVisit") + .HasColumnType("boolean"); + + b.Property("PatientFullName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("PatientId") + .HasColumnType("integer"); + + b.Property("RoomNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DoctorId"); + + b.HasIndex("PatientId"); + + b.ToTable("Appointments"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Doctor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("ExperienceYears") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.ToTable("Doctors"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("BloodGroup") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PassportNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Patronymic") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RhesusFactor") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Patients"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Specialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.ToTable("Specializations"); + }); + + modelBuilder.Entity("DoctorSpecialization", b => + { + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("SpecializationId") + .HasColumnType("integer"); + + b.HasKey("DoctorId", "SpecializationId"); + + b.HasIndex("SpecializationId"); + + b.ToTable("DoctorSpecialization"); + }); + + modelBuilder.Entity("Clinic.Models.Entities.Appointment", b => + { + b.HasOne("Clinic.Models.Entities.Doctor", null) + .WithMany() + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Clinic.Models.Entities.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("DoctorSpecialization", b => + { + b.HasOne("Clinic.Models.Entities.Doctor", null) + .WithMany() + .HasForeignKey("DoctorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Clinic.Models.Entities.Specialization", null) + .WithMany() + .HasForeignKey("SpecializationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Clinic/Clinic.InMemory/Clinic.InMemory.csproj b/Clinic/Clinic.InMemory/Clinic.InMemory.csproj new file mode 100644 index 000000000..8052819b6 --- /dev/null +++ b/Clinic/Clinic.InMemory/Clinic.InMemory.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/Clinic/Clinic.InMemory/InMemoryAppointmentRepository.cs b/Clinic/Clinic.InMemory/InMemoryAppointmentRepository.cs new file mode 100644 index 000000000..c914865ea --- /dev/null +++ b/Clinic/Clinic.InMemory/InMemoryAppointmentRepository.cs @@ -0,0 +1,81 @@ +using Clinic.Application.Ports; +using Clinic.Models.Entities; + +namespace Clinic.InMemory; +public sealed class InMemoryAppointmentRepository : IAppointmentRepository +{ + /// + /// In-memory storage for appointments. + /// + private readonly Dictionary _appointments = new(); + + /// + /// Retrieves an appointment by its ID. + /// + /// The ID of the appointment to retrieve. + /// The appointment with the specified ID, or null if not found. + public Appointment? GetAppointment(int id) => _appointments.GetValueOrDefault(id); + + /// + /// Retrieves all appointments from the in-memory storage. + /// + /// A read-only collection of all appointments. + public IReadOnlyCollection GetAllAppointments() => _appointments.Values; + + /// + /// Retrieves all appointments associated with a specific doctor. + /// + /// The ID of the doctor. + /// A read-only collection of appointments for the specified doctor. + public IReadOnlyCollection GetAppointmentsByDoctor(int doctorId) => + _appointments.Values.Where(a => a.DoctorId == doctorId).ToList(); + + /// + /// Retrieves all appointments associated with a specific patient. + /// + /// The ID of the patient. + /// A read-only collection of appointments for the specified patient. + public IReadOnlyCollection GetAppointmentsByPatient(int patientId) => + _appointments.Values.Where(a => a.PatientId == patientId).ToList(); + + /// + /// Adds a new appointment to the in-memory storage. + /// + /// The appointment to add. + /// True if the appointment was successfully added, false if it already exists. + public bool AddAppointment(Appointment appointment){ + if (_appointments.ContainsKey(appointment.Id)){ + return false; + } + + _appointments[appointment.Id] = appointment; + return true; + } + + /// + /// Updates an existing appointment in the in-memory storage. + /// + /// The appointment with updated information. + /// True if the appointment was successfully updated, false if it doesn't exist. + public bool UpdateAppointment(Appointment appointment){ + if (!_appointments.ContainsKey(appointment.Id)){ + return false; + } + + _appointments[appointment.Id] = appointment; + return true; + } + + /// + /// Removes an appointment from the in-memory storage. + /// + /// The ID of the appointment to remove. + /// True if the appointment was successfully removed, false if it doesn't exist. + public bool RemoveAppointment(int id) => _appointments.Remove(id); + + /// + /// Gets the count of appointments in the in-memory storage. + /// + /// The number of appointments. + public int AppointmentCount() => _appointments.Count(); +} diff --git a/Clinic/Clinic.InMemory/InMemoryDoctorRepository.cs b/Clinic/Clinic.InMemory/InMemoryDoctorRepository.cs new file mode 100644 index 000000000..5cbfbaf8a --- /dev/null +++ b/Clinic/Clinic.InMemory/InMemoryDoctorRepository.cs @@ -0,0 +1,63 @@ +using Clinic.Application.Ports; +using Clinic.Models.Entities; + +namespace Clinic.InMemory; +public sealed class InMemoryDoctorRepository : IDoctorRepository +{ + /// + /// In-memory storage for doctors. + /// + private readonly Dictionary _doctors = new(); + + /// + /// Retrieves a doctor by their ID. + /// + /// The ID of the doctor to retrieve. + /// The doctor with the specified ID, or null if not found. + public Doctor? GetDoctor(int id) => _doctors.GetValueOrDefault(id); + + /// + /// Retrieves all doctors from the in-memory storage. + /// + /// A read-only collection of all doctors. + public IReadOnlyCollection GetAllDoctors() => _doctors.Values; + + /// + /// Adds a new doctor to the in-memory storage. + /// + /// The doctor to add. + /// True if the doctor was successfully added, false if it already exists. + public bool AddDoctor(Doctor doctor){ + if (_doctors.ContainsKey(doctor.Id)){ + return false; + } + _doctors[doctor.Id] = doctor; + return true; + } + + /// + /// Updates an existing doctor in the in-memory storage. + /// + /// The doctor with updated information. + /// True if the doctor was successfully updated, false if it doesn't exist. + public bool UpdateDoctor(Doctor doctor){ + if (!_doctors.ContainsKey(doctor.Id)){ + return false; + } + _doctors[doctor.Id] = doctor; + return true; + } + + /// + /// Removes a doctor from the in-memory storage. + /// + /// The ID of the doctor to remove. + /// True if the doctor was successfully removed, false if it doesn't exist. + public bool RemoveDoctor(int id) => _doctors.Remove(id); + + /// + /// Gets the count of doctors in the in-memory storage. + /// + /// The number of doctors. + public int DoctorCount() => _doctors.Count(); +} diff --git a/Clinic/Clinic.InMemory/InMemoryPatientRepository.cs b/Clinic/Clinic.InMemory/InMemoryPatientRepository.cs new file mode 100644 index 000000000..74207d00c --- /dev/null +++ b/Clinic/Clinic.InMemory/InMemoryPatientRepository.cs @@ -0,0 +1,64 @@ +using Clinic.Application.Ports; +using Clinic.Models.Entities; + +namespace Clinic.InMemory; +public sealed class InMemoryPatientRepository : IPatientRepository +{ + /// + /// In-memory storage for patients. + /// + private readonly Dictionary _patients = new(); + + /// + /// Retrieves a patient by their ID. + /// + /// The ID of the patient to retrieve. + /// The patient with the specified ID, or null if not found. + public Patient? GetPatient(int id) => _patients.GetValueOrDefault(id); + + /// + /// Retrieves all patients from the in-memory storage. + /// + /// A read-only collection of all patients. + public IReadOnlyCollection GetAllPatients() => _patients.Values; + + /// + /// Adds a new patient to the in-memory storage. + /// + /// The patient to add. + /// True if the patient was successfully added, false if it already exists. + public bool AddPatient(Patient patient){ + if (_patients.ContainsKey(patient.Id)){ + return false; + } + + _patients[patient.Id] = patient; + return true; + } + + /// + /// Updates an existing patient in the in-memory storage. + /// + /// The patient with updated information. + /// True if the patient was successfully updated, false if it doesn't exist. + public bool UpdatePatient(Patient patient){ + if (!_patients.ContainsKey(patient.Id)){ + return false; + } + _patients[patient.Id] = patient; + return true; + } + + /// + /// Removes a patient from the in-memory storage. + /// + /// The ID of the patient to remove. + /// True if the patient was successfully removed, false if it doesn't exist. + public bool RemovePatient(int id) => _patients.Remove(id); + + /// + /// Gets the count of patients in the in-memory storage. + /// + /// The number of patients. + public int PatientCount() => _patients.Count; +} diff --git a/Clinic/Clinic.InMemory/InMemorySpecializationRepository.cs b/Clinic/Clinic.InMemory/InMemorySpecializationRepository.cs new file mode 100644 index 000000000..1e35f3fa0 --- /dev/null +++ b/Clinic/Clinic.InMemory/InMemorySpecializationRepository.cs @@ -0,0 +1,72 @@ +using Clinic.Application.Ports; +using Clinic.Models.Entities; + +namespace Clinic.InMemory; +public sealed class InMemorySpecializationRepository : ISpecializationRepository +{ + /// + /// In-memory storage for specializations. + /// + private readonly Dictionary _specializations = new(); + + /// + /// Retrieves all specializations from the in-memory storage. + /// + /// A read-only collection of all specializations. + public IReadOnlyCollection GetAllSpecializations() => _specializations.Values; + + /// + /// Adds a new specialization to the in-memory storage. + /// + /// The specialization to add. + /// True if the specialization was successfully added, false if it already exists. + public bool AddSpecialization(Specialization specialization) + { + if (_specializations.ContainsKey(specialization.Id)){ + return false; + } + _specializations[specialization.Id] = specialization; + return true; + } + + /// + /// Removes a specialization from the in-memory storage. + /// + /// The ID of the specialization to remove. + /// True if the specialization was successfully removed, false if it doesn't exist. + public bool RemoveSpecialization(int id) => _specializations.Remove(id); + + /// + /// Retrieves a specialization by its ID. + /// + /// The ID of the specialization to retrieve. + /// The specialization with the specified ID, or null if not found. + public Specialization? GetSpecialization(int id) + { + if (!_specializations.ContainsKey(id)) + { + return null; + } + return _specializations[id]; + } + + /// + /// Gets the count of specializations in the in-memory storage. + /// + /// The number of specializations. + public int SpecializationCount() => _specializations.Count; + + /// + /// Updates an existing specialization in the in-memory storage. + /// + /// The ID of the specialization to update. + /// The specialization with updated information. + /// The updated specialization, or null if it doesn't exist. + public Specialization? UpdateSpecialization(int id, Specialization specialization){ + if (!_specializations.ContainsKey(id)){ + return null; + } + _specializations[id].Name = specialization.Name; + return _specializations[id]; + } +} diff --git a/Clinic/Clinic.Models/Clinic.Models.csproj b/Clinic/Clinic.Models/Clinic.Models.csproj new file mode 100644 index 000000000..444d48ffe --- /dev/null +++ b/Clinic/Clinic.Models/Clinic.Models.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + \ No newline at end of file diff --git a/Clinic/Clinic.Models/Common/PersonInfo.cs b/Clinic/Clinic.Models/Common/PersonInfo.cs new file mode 100644 index 000000000..6731f8178 --- /dev/null +++ b/Clinic/Clinic.Models/Common/PersonInfo.cs @@ -0,0 +1,62 @@ +using Clinic.Models.Enums; + +namespace Clinic.Models.Common; + +/// +/// Represents personal information for a person. +/// +public class PersonInfo +{ + /// + /// Gets or sets the unique identifier. + /// + required public int Id { get; set; } + + /// + /// Gets or sets the passport number. + /// + required public string PassportNumber { get; set; } + + /// + /// Gets or sets the year of birth. + /// + required public DateOnly BirthDate { get; set; } + + /// + /// Gets or sets the last name. + /// + required public string LastName { get; set; } + + /// + /// Gets or sets the first name. + /// + required public string FirstName { get; set; } + + /// + /// Gets or sets the patronymic. + /// + public string? Patronymic { get; set; } + + /// + /// Gets or sets the patient's phone number. + /// + required public string PhoneNumber { get; set; } + + /// + /// Gets or sets the gender. + /// + required public Gender Gender { get; set; } + + /// + /// Gets the full name composed of last name, first name and optional patronymic. + /// + /// The full name as a single string. + public string GetFullName() + { + if (string.IsNullOrEmpty(Patronymic)) + { + return $"{LastName} {FirstName}"; + } + return $"{LastName} {FirstName} {Patronymic}"; + } +} \ No newline at end of file diff --git a/Clinic/Clinic.Models/Entities/Appointment.cs b/Clinic/Clinic.Models/Entities/Appointment.cs new file mode 100644 index 000000000..343fbb282 --- /dev/null +++ b/Clinic/Clinic.Models/Entities/Appointment.cs @@ -0,0 +1,47 @@ +namespace Clinic.Models.Entities; + +/// +/// Represents an appointment in the clinic, including patient, doctor, date/time, room, and visit type. +/// +public class Appointment +{ + /// + /// Gets or sets the unique identifier for the appointment. + /// + required public int Id { get; set; } + + /// + /// Gets or sets the patient identifier for the appointment. + /// + required public int PatientId { get; set; } + + /// + /// Gets or sets the full name of the patient at the time of appointment. + /// + required public string PatientFullName { get; set; } = null!; + + /// + /// Gets or sets the doctor identifier for the appointment. + /// + required public int DoctorId { get; set; } + + /// + /// Gets or sets the full name of the doctor at the time of appointment. + /// + required public string DoctorFullName { get; set; } = null!; + + /// + /// Gets or sets the date and time of the appointment. + /// + required public DateTime DateTime { get; set; } + + /// + /// Gets or sets the room number for the appointment. + /// + required public int RoomNumber { get; set; } + + /// + /// Gets or sets a value indicating whether this appointment is a return visit. + /// + required public bool IsReturnVisit { get; set; } +} diff --git a/Clinic/Clinic.Models/Entities/Doctor.cs b/Clinic/Clinic.Models/Entities/Doctor.cs new file mode 100644 index 000000000..ef5481a63 --- /dev/null +++ b/Clinic/Clinic.Models/Entities/Doctor.cs @@ -0,0 +1,19 @@ +using Clinic.Models.Common; + +namespace Clinic.Models.Entities; + +/// +/// Represents a doctor with personal and professional details. +/// +public class Doctor : PersonInfo +{ + /// + /// Gets or sets the list of medical specializations the doctor holds. + /// + required public List Specializations { get; set; } + + /// + /// Gets or sets the number of years of experience the doctor has. + /// + required public int ExperienceYears { get; set; } +} diff --git a/Clinic/Clinic.Models/Entities/Patient.cs b/Clinic/Clinic.Models/Entities/Patient.cs new file mode 100644 index 000000000..59cb254f0 --- /dev/null +++ b/Clinic/Clinic.Models/Entities/Patient.cs @@ -0,0 +1,25 @@ +using Clinic.Models.Common; +using Clinic.Models.Enums; + +namespace Clinic.Models.Entities; + +/// +/// Represents a patient entity with personal and medical information. +/// +public class Patient : PersonInfo +{ + /// + /// Gets or sets the patient's address. + /// + required public string Address { get; set; } + + /// + /// Gets or sets the patient's blood group. + /// + required public BloodGroup BloodGroup { get; set; } + + /// + /// Gets or sets the patient's rhesus factor. + /// + required public RhesusFactor RhesusFactor { get; set; } +} diff --git a/Clinic/Clinic.Models/Entities/Specialization.cs b/Clinic/Clinic.Models/Entities/Specialization.cs new file mode 100644 index 000000000..35483633a --- /dev/null +++ b/Clinic/Clinic.Models/Entities/Specialization.cs @@ -0,0 +1,19 @@ +using System.Dynamic; + +namespace Clinic.Models.Entities; + +/// +/// Represents a medical specialisation in the clinic. +/// +public class Specialization +{ + /// + /// Gets or sets the id specialization. + /// + required public int Id { get; set; } + + /// + /// Gets or sets the specialization name. + /// + required public string Name { get; set; } +} diff --git a/Clinic/Clinic.Models/Enums/BloodGroup.cs b/Clinic/Clinic.Models/Enums/BloodGroup.cs new file mode 100644 index 000000000..795931ac5 --- /dev/null +++ b/Clinic/Clinic.Models/Enums/BloodGroup.cs @@ -0,0 +1,12 @@ +namespace Clinic.Models.Enums; + +/// +/// Represents the blood group categories used in the clinic domain. +/// +public enum BloodGroup +{ + A, + B, + AB, + O +} diff --git a/Clinic/Clinic.Models/Enums/Gender.cs b/Clinic/Clinic.Models/Enums/Gender.cs new file mode 100644 index 000000000..60d17c06d --- /dev/null +++ b/Clinic/Clinic.Models/Enums/Gender.cs @@ -0,0 +1,10 @@ +namespace Clinic.Models.Enums; + +/// +/// Represents the gender of a person. +/// +public enum Gender +{ + Male, + Female +} diff --git a/Clinic/Clinic.Models/Enums/RhesusFactor.cs b/Clinic/Clinic.Models/Enums/RhesusFactor.cs new file mode 100644 index 000000000..ef6930906 --- /dev/null +++ b/Clinic/Clinic.Models/Enums/RhesusFactor.cs @@ -0,0 +1,10 @@ +namespace Clinic.Models.Enums; + +/// +/// Rhesus factor of the patient indicating presence of the RhD antigen. +/// +public enum RhesusFactor +{ + Positive, + Negative +} diff --git a/Clinic/Clinic.Tests/Clinic.Tests.csproj b/Clinic/Clinic.Tests/Clinic.Tests.csproj new file mode 100644 index 000000000..0ba0d0ca2 --- /dev/null +++ b/Clinic/Clinic.Tests/Clinic.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Clinic/Clinic.Tests/ClinicTests.cs b/Clinic/Clinic.Tests/ClinicTests.cs new file mode 100644 index 000000000..fcb2142df --- /dev/null +++ b/Clinic/Clinic.Tests/ClinicTests.cs @@ -0,0 +1,57 @@ +using Clinic.DataBase; +using Clinic.Application.Services; + + +namespace Clinic.Tests; +public abstract class ClinicTests(AnalyticsServices testServices) : IClassFixture +{ + [Fact] + public void GetDoctorsWithExperience_WhenExperienceAtLeast10Years_ReturnsExperiencedDoctorsOrderedByName() + { + var doctorsWithExperience10OrMore = new List {1, 2, 3, 4, 5, 6, 7, 9, 10}; + var result = testServices.GetDoctorsWithExperience10YearsOrMore().Select(d => d.Id); + Assert.Equal(doctorsWithExperience10OrMore, result); + } + + [Fact] + public void GetPatientsByDoctor_WhenDoctorIsSpecified_ReturnsPatientsOrderedByName() + { + var doctorId = 3; + var patientsByDoctor = new List {3}; + + var patients = testServices.GetPatientsByDoctorOrderedByFullName(doctorId); + Assert.NotNull(patients); + var result = patients!.Select(p => p.Id); + Assert.Equal(patientsByDoctor, result); + } + + [Fact] + public void CountAppointments_WhenRepeatVisitsInLastMonth_ReturnsCorrectCount() + { + var returnVisits = 2; + var result = testServices.GetReturnVisitsCountLastMonth(); + + Assert.Equal(returnVisits, result); + } + + [Fact] + public void GetPatients_WhenOver30WithMultipleDoctors_ReturnsPatientsOrderedByBirthDate() + { + var patientsOver30WithMultipleDoctors = new List {1, 3}; + var result = testServices.GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate().Select(p => p.Id); + + Assert.Equal(patientsOver30WithMultipleDoctors, result); + } + + [Fact] + public void GetAppointments_WhenInSpecificRoomCurrentMonth_ReturnsAppointmentsOrderedByDateTime() + { + var roomNumber = 101; + var appointmentsInRoomCurrentMonth = new List {1}; + + var result = testServices.GetAppointmentsInRoomForCurrentMonth(roomNumber).Select(a => a.Id); + + Assert.Equal(appointmentsInRoomCurrentMonth, result); + + } +} diff --git a/Clinic/Clinic.sln b/Clinic/Clinic.sln new file mode 100755 index 000000000..07efce8b8 --- /dev/null +++ b/Clinic/Clinic.sln @@ -0,0 +1,163 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.Models", "Clinic.Models\Clinic.Models.csproj", "{5351F009-4016-C1FF-B495-C469DDDFBE5E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.Api", "Clinic.Api\Clinic.Api.csproj", "{7433D860-E79F-44AA-BA33-9E0F5901578D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.DataBase", "Clinic.DataBase\Clinic.DataBase.csproj", "{9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.Tests", "Clinic.Tests\Clinic.Tests.csproj", "{D3E7E045-5C0D-4199-AE4A-7468F477DE74}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.AppHost", "Clinic.AppHost\Clinic.AppHost.csproj", "{7BD8C638-56B5-438F-BE87-2A628C5F7C8F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.Application", "Clinic.Application\Clinic.Application.csproj", "{43A97106-90F1-4FC0-B66E-1D6DA5950260}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.InMemory", "Clinic.InMemory\Clinic.InMemory.csproj", "{C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.Contracts", "Clinic.Contracts\Clinic.Contracts.csproj", "{A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.AppointmentGenerator", "Clinic.AppointmentGenerator\Clinic.AppointmentGenerator.csproj", "{8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clinic.Application.Services", "Clinic.Application.Services\Clinic.Application.Services.csproj", "{E53E900D-1022-490F-B559-E74774E27FE3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Debug|x64.ActiveCfg = Debug|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Debug|x64.Build.0 = Debug|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Debug|x86.ActiveCfg = Debug|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Debug|x86.Build.0 = Debug|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Release|Any CPU.Build.0 = Release|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Release|x64.ActiveCfg = Release|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Release|x64.Build.0 = Release|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Release|x86.ActiveCfg = Release|Any CPU + {5351F009-4016-C1FF-B495-C469DDDFBE5E}.Release|x86.Build.0 = Release|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Debug|x64.ActiveCfg = Debug|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Debug|x64.Build.0 = Debug|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Debug|x86.ActiveCfg = Debug|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Debug|x86.Build.0 = Debug|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Release|Any CPU.Build.0 = Release|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Release|x64.ActiveCfg = Release|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Release|x64.Build.0 = Release|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Release|x86.ActiveCfg = Release|Any CPU + {7433D860-E79F-44AA-BA33-9E0F5901578D}.Release|x86.Build.0 = Release|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Debug|x64.Build.0 = Debug|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Debug|x86.Build.0 = Debug|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Release|Any CPU.Build.0 = Release|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Release|x64.ActiveCfg = Release|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Release|x64.Build.0 = Release|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Release|x86.ActiveCfg = Release|Any CPU + {9A5F5BA1-0C3F-4B3F-9AE7-4FD8E3C5F2C4}.Release|x86.Build.0 = Release|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Debug|x64.Build.0 = Debug|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Debug|x86.Build.0 = Debug|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Release|Any CPU.Build.0 = Release|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Release|x64.ActiveCfg = Release|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Release|x64.Build.0 = Release|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Release|x86.ActiveCfg = Release|Any CPU + {D3E7E045-5C0D-4199-AE4A-7468F477DE74}.Release|x86.Build.0 = Release|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Debug|x64.Build.0 = Debug|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Debug|x86.Build.0 = Debug|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Release|Any CPU.Build.0 = Release|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Release|x64.ActiveCfg = Release|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Release|x64.Build.0 = Release|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Release|x86.ActiveCfg = Release|Any CPU + {7BD8C638-56B5-438F-BE87-2A628C5F7C8F}.Release|x86.Build.0 = Release|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Debug|x64.ActiveCfg = Debug|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Debug|x64.Build.0 = Debug|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Debug|x86.ActiveCfg = Debug|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Debug|x86.Build.0 = Debug|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Release|Any CPU.Build.0 = Release|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Release|x64.ActiveCfg = Release|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Release|x64.Build.0 = Release|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Release|x86.ActiveCfg = Release|Any CPU + {43A97106-90F1-4FC0-B66E-1D6DA5950260}.Release|x86.Build.0 = Release|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Debug|x64.ActiveCfg = Debug|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Debug|x64.Build.0 = Debug|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Debug|x86.ActiveCfg = Debug|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Debug|x86.Build.0 = Debug|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Release|Any CPU.Build.0 = Release|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Release|x64.ActiveCfg = Release|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Release|x64.Build.0 = Release|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Release|x86.ActiveCfg = Release|Any CPU + {C77BBDCF-DF57-404C-B821-9DA7E6C06AD7}.Release|x86.Build.0 = Release|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Debug|x64.ActiveCfg = Debug|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Debug|x64.Build.0 = Debug|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Debug|x86.ActiveCfg = Debug|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Debug|x86.Build.0 = Debug|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Release|Any CPU.Build.0 = Release|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Release|x64.ActiveCfg = Release|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Release|x64.Build.0 = Release|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Release|x86.ActiveCfg = Release|Any CPU + {A17E9A18-4BE4-4C25-88F8-3E0B6C264B55}.Release|x86.Build.0 = Release|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Debug|x64.Build.0 = Debug|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Debug|x86.Build.0 = Debug|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Release|Any CPU.Build.0 = Release|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Release|x64.ActiveCfg = Release|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Release|x64.Build.0 = Release|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Release|x86.ActiveCfg = Release|Any CPU + {8D7E3ED3-6C45-4A9F-8C09-A0F4D50F1B9F}.Release|x86.Build.0 = Release|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Debug|x64.ActiveCfg = Debug|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Debug|x64.Build.0 = Debug|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Debug|x86.ActiveCfg = Debug|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Debug|x86.Build.0 = Debug|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Release|Any CPU.Build.0 = Release|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Release|x64.ActiveCfg = Release|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Release|x64.Build.0 = Release|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Release|x86.ActiveCfg = Release|Any CPU + {E53E900D-1022-490F-B559-E74774E27FE3}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6E454636-A821-46B9-A0BD-05DA9F1F5DE1} + EndGlobalSection +EndGlobal