Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/dotnet_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: .NET Tests

on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Restore dependencies
run: dotnet restore Polyclinic/Polyclinic.sln

- name: Build
run: dotnet build Polyclinic/Polyclinic.sln --no-restore --configuration Release

- name: Test
run: dotnet test Polyclinic/Polyclinic.sln --no-build --configuration Release --verbosity normal
117 changes: 117 additions & 0 deletions Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Microsoft.AspNetCore.Mvc;
using Polyclinic.Application.Contracts;
using Polyclinic.Application.Contracts.Analytics;
using Polyclinic.Application.Contracts.Appointments;
using Polyclinic.Application.Contracts.Doctors;
using Polyclinic.Application.Contracts.Patients;

namespace Polyclinic.Api.Host.Controllers;

/// <summary>
/// Контроллер для получения аналитических отчетов и выборок
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase
{
/// <summary>
/// Получить врачей со стажем работы более указанного (по умолчанию 10 лет)
/// </summary>
/// <param name="minExperience">Минимальный стаж в годах</param>
[HttpGet("doctors/experienced")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IList<DoctorDto>>> GetExperiencedDoctors([FromQuery] int minExperience = 10)
{
try
{
var result = await analyticsService.GetDoctorsWithExperienceAsync(minExperience);
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Получить список пациентов, записанных к конкретному врачу, отсортированный по ФИО
/// </summary>
/// <param name="doctorId">Идентификатор врача</param>
[HttpGet("doctors/{doctorId}/patients")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IList<PatientDto>>> GetPatientsByDoctor(int doctorId)
{
try
{
var result = await analyticsService.GetPatientsByDoctorAsync(doctorId);
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Получить статистику повторных приемов за указанный месяц
/// </summary>
/// <param name="date">Дата для определения месяца и года</param>
[HttpGet("appointments/stats/monthly")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<MonthlyAppointmentStatsDto>> GetMonthlyStats([FromQuery] DateTime date)
{
try
{
var result = await analyticsService.GetMonthlyRepeatStatsAsync(date);
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Получить пациентов старше указанного возраста (по умолчанию 30), посетивших более одного врача
/// </summary>
/// <param name="minAge">Минимальный возраст</param>
[HttpGet("patients/active-visitors")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IList<PatientDto>>> GetPatientsWithMultipleDoctors([FromQuery] int minAge = 30)
{
try
{
var result = await analyticsService.GetPatientsWithMultipleDoctorsAsync(minAge);
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Получить список приемов в конкретном кабинете за месяц
/// </summary>
/// <param name="roomNumber">Номер кабинета</param>
/// <param name="date">Дата для определения месяца выборки</param>
[HttpGet("rooms/{roomNumber}/appointments")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IList<AppointmentDto>>> GetAppointmentsByRoom(string roomNumber, [FromQuery] DateTime date)
{
try
{
var result = await analyticsService.GetAppointmentsByRoomAsync(roomNumber, date);
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Polyclinic.Application.Contracts;
using Polyclinic.Application.Contracts.Appointments;

namespace Polyclinic.Api.Host.Controllers;

/// <summary>
/// Контроллер для управления записями на прием
/// </summary>
public class AppointmentsController(
IApplicationService<AppointmentDto, AppointmentCreateUpdateDto, int> service)
: CrudControllerBase<AppointmentDto, AppointmentCreateUpdateDto, int>(service);
137 changes: 137 additions & 0 deletions Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Microsoft.AspNetCore.Mvc;
using Polyclinic.Application.Contracts;

namespace Polyclinic.Api.Host.Controllers;

/// <summary>
/// Базовый абстрактный контроллер, реализующий стандартные CRUD-операции
/// </summary>
/// <typeparam name="TDto">Тип DTO для чтения</typeparam>
/// <typeparam name="TCreateUpdateDto">Тип DTO для создания и обновления</typeparam>
/// <typeparam name="TKey">Тип идентификатора сущности</typeparam>
[ApiController]
[Route("api/[controller]")]
public abstract class CrudControllerBase<TDto, TCreateUpdateDto, TKey>(
IApplicationService<TDto, TCreateUpdateDto, TKey> service)
: ControllerBase
where TDto : class
where TCreateUpdateDto : class
where TKey : struct
{
/// <summary>
/// Получает список всех сущностей
/// </summary>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual async Task<ActionResult<IList<TDto>>> GetAll()
{
try
{
var result = await service.GetAll();
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Получает сущность по идентификатору
/// </summary>
/// <param name="id">Идентификатор сущности</param>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual async Task<ActionResult<TDto>> Get(TKey id)
{
try
{
var result = await service.Get(id);
if (result == null) return NotFound();

return Ok(result);
}
catch (KeyNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Создает новую сущность
/// </summary>
/// <param name="dto">DTO с данными для создания</param>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual async Task<ActionResult<TDto>> Create([FromBody] TCreateUpdateDto dto)
{
try
{
var result = await service.Create(dto);
return CreatedAtAction(nameof(this.Create), result);
}
catch (KeyNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Обновляет существующую сущность
/// </summary>
/// <param name="id">Идентификатор обновляемой сущности</param>
/// <param name="dto">DTO с новыми данными</param>
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual async Task<ActionResult<TDto>> Update(TKey id, [FromBody] TCreateUpdateDto dto)
{
try
{
var result = await service.Update(dto, id);
return Ok(result);
}
catch (KeyNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}

/// <summary>
/// Удаляет сущность по идентификатору
/// </summary>
/// <param name="id">Идентификатор удаляемой сущности</param>
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public virtual async Task<ActionResult> Delete(TKey id)
{
try
{
await service.Delete(id);
return NoContent();
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}
}
11 changes: 11 additions & 0 deletions Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Polyclinic.Application.Contracts;
using Polyclinic.Application.Contracts.Doctors;

namespace Polyclinic.Api.Host.Controllers;

/// <summary>
/// Контроллер для управления данными врачей
/// </summary>
public class DoctorController(
IApplicationService<DoctorDto, DoctorCreateUpdateDto, int> service)
: CrudControllerBase<DoctorDto, DoctorCreateUpdateDto, int>(service);
11 changes: 11 additions & 0 deletions Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Polyclinic.Application.Contracts;
using Polyclinic.Application.Contracts.Patients;

namespace Polyclinic.Api.Host.Controllers;

/// <summary>
/// Контроллер для управления данными пациентов
/// </summary>
public class PatientsController(
IApplicationService<PatientDto, PatientCreateUpdateDto, int> service)
: CrudControllerBase<PatientDto, PatientCreateUpdateDto, int>(service);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Polyclinic.Application.Contracts;
using Polyclinic.Application.Contracts.Specializations;

namespace Polyclinic.Api.Host.Controllers;

/// <summary>
/// Контроллер для управления справочником специализаций
/// </summary>
public class SpecializationController(
IApplicationService<SpecializationDto, SpecializationCreateUpdateDto, int> service)
: CrudControllerBase<SpecializationDto, SpecializationCreateUpdateDto, int>(service);
25 changes: 25 additions & 0 deletions Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="13.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Polyclinic.Application\Polyclinic.Application.csproj" />
<ProjectReference Include="..\Polyclinic.Infrastructure.EfCore\Polyclinic.Infrastructure.EfCore.csproj" />
<ProjectReference Include="..\Polyclinic.ServiceDefaults\Polyclinic.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
Loading