паралельный последовательный удалены

This commit is contained in:
gorbenko.pavel
2026-07-31 14:38:31 +05:00
parent d38962754d
commit de60fd0fdb
19 changed files with 48 additions and 115 deletions
@@ -12,7 +12,6 @@ public sealed record ResolvedRouteStepDto(
public sealed record ResolveTemplateRouteResultDto( public sealed record ResolveTemplateRouteResultDto(
int RouteId, int RouteId,
RouteType RouteType,
bool RequiresControl, bool RequiresControl,
IReadOnlyList<ResolvedRouteStepDto> Steps); IReadOnlyList<ResolvedRouteStepDto> Steps);
@@ -1,10 +1,7 @@
using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes; namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
public sealed record TemplateRouteDto( public sealed record TemplateRouteDto(
int Id, int Id,
RouteType RouteType,
int RequestTypeId, int RequestTypeId,
Guid? DepartmentId, Guid? DepartmentId,
Guid CompanyId, Guid CompanyId,
@@ -12,7 +9,6 @@ public sealed record TemplateRouteDto(
bool RequiresControl); bool RequiresControl);
public sealed record CreateTemplateRouteRequest( public sealed record CreateTemplateRouteRequest(
RouteType RouteType,
int RequestTypeId, int RequestTypeId,
Guid? DepartmentId, Guid? DepartmentId,
Guid CompanyId, Guid CompanyId,
@@ -20,14 +16,12 @@ public sealed record CreateTemplateRouteRequest(
bool RequiresControl); bool RequiresControl);
public sealed record UpdateTemplateRouteRequest( public sealed record UpdateTemplateRouteRequest(
RouteType RouteType,
Guid? DepartmentId, Guid? DepartmentId,
Guid CompanyId, Guid CompanyId,
bool ByProject, bool ByProject,
bool RequiresControl); bool RequiresControl);
public sealed record PatchTemplateRouteRequest( public sealed record PatchTemplateRouteRequest(
RouteType? RouteType,
Guid? DepartmentId, Guid? DepartmentId,
Guid? CompanyId, Guid? CompanyId,
bool? ByProject, bool? ByProject,
@@ -24,7 +24,7 @@ public sealed class TemplateRouteService(
var items = await query var items = await query
.Select(x => new TemplateRouteDto( .Select(x => new TemplateRouteDto(
x.Id, x.RouteType, x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject, x.RequiresControl)) x.Id, x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject, x.RequiresControl))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<TemplateRouteDto>>(items); return Result.Ok<IReadOnlyList<TemplateRouteDto>>(items);
@@ -52,7 +52,6 @@ public sealed class TemplateRouteService(
var entity = new TemplateRoute var entity = new TemplateRoute
{ {
RouteType = r.RouteType,
RequestTypeId = r.RequestTypeId, RequestTypeId = r.RequestTypeId,
DepartmentId = r.DepartmentId, DepartmentId = r.DepartmentId,
CompanyId = r.CompanyId, CompanyId = r.CompanyId,
@@ -76,7 +75,6 @@ public sealed class TemplateRouteService(
return Result.Fail<TemplateRouteDto>(new ConflictError( return Result.Fail<TemplateRouteDto>(new ConflictError(
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует.")); "Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
entity.RouteType = r.RouteType;
entity.DepartmentId = r.DepartmentId; entity.DepartmentId = r.DepartmentId;
entity.CompanyId = r.CompanyId; entity.CompanyId = r.CompanyId;
entity.ByProject = r.ByProject; entity.ByProject = r.ByProject;
@@ -101,7 +99,6 @@ public sealed class TemplateRouteService(
return Result.Fail<TemplateRouteDto>(new ConflictError( return Result.Fail<TemplateRouteDto>(new ConflictError(
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует.")); "Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
entity.RouteType = r.RouteType ?? entity.RouteType;
entity.DepartmentId = departmentId; entity.DepartmentId = departmentId;
entity.CompanyId = companyId; entity.CompanyId = companyId;
entity.ByProject = byProject; entity.ByProject = byProject;
@@ -179,7 +176,7 @@ public sealed class TemplateRouteService(
var collapsedSteps = CollapseByExecutorEmail(steps); var collapsedSteps = CollapseByExecutorEmail(steps);
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, collapsedSteps)); return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps));
} }
private Task<bool> HasDuplicateAsync( private Task<bool> HasDuplicateAsync(
@@ -260,5 +257,5 @@ public sealed class TemplateRouteService(
} }
private static TemplateRouteDto ToDto(TemplateRoute e) => new( private static TemplateRouteDto ToDto(TemplateRoute e) => new(
e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl); e.Id, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
} }
@@ -1,11 +1,8 @@
using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Domain.Entities; namespace Tnsu.ApprovalRoute.Domain.Entities;
public class TemplateRoute public class TemplateRoute
{ {
public int Id { get; set; } public int Id { get; set; }
public RouteType RouteType { get; set; } = RouteType.Sequential;
public int RequestTypeId { get; set; } public int RequestTypeId { get; set; }
public Guid? DepartmentId { get; set; } public Guid? DepartmentId { get; set; }
@@ -1,8 +0,0 @@
namespace Tnsu.ApprovalRoute.Domain.Enums;
public enum RouteType
{
None,
Parallel,
Sequential
}
@@ -24,7 +24,6 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
modelBuilder.Entity<TemplateRoute>(e => modelBuilder.Entity<TemplateRoute>(e =>
{ {
e.HasKey(x => x.Id); e.HasKey(x => x.Id);
e.Property(x => x.RouteType).HasConversion<string>().HasMaxLength(32).IsRequired();
e.HasOne(x => x.RequestType).WithMany(x => x.Routes).HasForeignKey(x => x.RequestTypeId); e.HasOne(x => x.RequestType).WithMany(x => x.Routes).HasForeignKey(x => x.RequestTypeId);
e.HasIndex(x => new { x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject }).IsUnique(); e.HasIndex(x => new { x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject }).IsUnique();
}); });
@@ -12,7 +12,7 @@ using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
{ {
[DbContext(typeof(ApprovalRouteDbContext))] [DbContext(typeof(ApprovalRouteDbContext))]
[Migration("20260731073536_AddRequestTypeListNameSectionAndStepConditionFields")] [Migration("20260731093159_AddRequestTypeListNameSectionAndStepConditionFields")]
partial class AddRequestTypeListNameSectionAndStepConditionFields partial class AddRequestTypeListNameSectionAndStepConditionFields
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -97,12 +97,6 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("requires_control"); .HasColumnName("requires_control");
b.Property<string>("RouteType")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("route_type");
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_template_routes"); .HasName("pk_template_routes");
@@ -10,6 +10,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropColumn(
name: "route_type",
table: "template_routes");
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "field_name", name: "field_name",
table: "template_route_steps", table: "template_route_steps",
@@ -80,6 +84,14 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "site_url", name: "site_url",
table: "request_types"); table: "request_types");
migrationBuilder.AddColumn<string>(
name: "route_type",
table: "template_routes",
type: "character varying(32)",
maxLength: 32,
nullable: false,
defaultValue: "");
} }
} }
} }
@@ -94,12 +94,6 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("requires_control"); .HasColumnName("requires_control");
b.Property<string>("RouteType")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("route_type");
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_template_routes"); .HasName("pk_template_routes");
@@ -48,7 +48,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Action // Action
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -65,7 +65,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
{ {
// Action // Action
var result = await _service.CreateAsync( var result = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, 999999, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(999999, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -77,7 +77,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
{ {
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Business trip"); var requestTypeId = await CreateRequestTypeAsync("Business trip");
var request = new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false); var request = new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false);
var first = await _service.CreateAsync(request, CancellationToken.None); var first = await _service.CreateAsync(request, CancellationToken.None);
first.IsSuccess.ShouldBeTrue(); first.IsSuccess.ShouldBeTrue();
@@ -94,18 +94,17 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Sick leave"); var requestTypeId = await CreateRequestTypeAsync("Sick leave");
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
// Action // Action
var updated = await _service.UpdateAsync( var updated = await _service.UpdateAsync(
created.Value.Id, created.Value.Id,
new UpdateTemplateRouteRequest(RouteType.Parallel, DepartmentId, CompanyId, ByProject: false, RequiresControl: true), new UpdateTemplateRouteRequest(DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
updated.IsSuccess.ShouldBeTrue(); updated.IsSuccess.ShouldBeTrue();
updated.Value.RouteType.ShouldBe(RouteType.Parallel);
updated.Value.RequiresControl.ShouldBeTrue(); updated.Value.RequiresControl.ShouldBeTrue();
} }
@@ -115,18 +114,17 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Patch route request"); var requestTypeId = await CreateRequestTypeAsync("Patch route request");
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
// Action // Action
var patched = await _service.PatchAsync( var patched = await _service.PatchAsync(
created.Value.Id, created.Value.Id,
new PatchTemplateRouteRequest(RouteType.Parallel, DepartmentId: null, CompanyId: null, ByProject: null, RequiresControl: true), new PatchTemplateRouteRequest(DepartmentId: null, CompanyId: null, ByProject: null, RequiresControl: true),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
patched.IsSuccess.ShouldBeTrue(); patched.IsSuccess.ShouldBeTrue();
patched.Value.RouteType.ShouldBe(RouteType.Parallel);
patched.Value.RequiresControl.ShouldBeTrue(); patched.Value.RequiresControl.ShouldBeTrue();
patched.Value.CompanyId.ShouldBe(CompanyId); patched.Value.CompanyId.ShouldBe(CompanyId);
patched.Value.DepartmentId.ShouldBe(DepartmentId); patched.Value.DepartmentId.ShouldBe(DepartmentId);
@@ -138,7 +136,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Remote work"); var requestTypeId = await CreateRequestTypeAsync("Remote work");
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
// Action // Action
@@ -157,7 +155,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Exact match request"); var requestTypeId = await CreateRequestTypeAsync("Exact match request");
var route = await _service.CreateAsync( var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
CancellationToken.None); CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
@@ -198,7 +196,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var requestTypeId = (await requestTypeService.CreateAsync( var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates", null, null, "General"), CancellationToken.None)).Value.Id; new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync( var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
await stepService.CreateAsync( await stepService.CreateAsync(
@@ -234,7 +232,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Project request"); var requestTypeId = await CreateRequestTypeAsync("Project request");
var route = await _service.CreateAsync( var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
@@ -274,7 +272,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var requestTypeId = (await requestTypeService.CreateAsync( var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("Position request", "/position-request", null, null, "General"), CancellationToken.None)).Value.Id; new CreateRequestTypeRequest("Position request", "/position-request", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync( var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
await stepService.CreateAsync( await stepService.CreateAsync(
route.Value.Id, route.Value.Id,
@@ -313,7 +311,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var requestTypeId = (await requestTypeService.CreateAsync( var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("Chief request", "/chief-request", null, null, "General"), CancellationToken.None)).Value.Id; new CreateRequestTypeRequest("Chief request", "/chief-request", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync( var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
await stepService.CreateAsync( await stepService.CreateAsync(
route.Value.Id, route.Value.Id,
@@ -348,7 +346,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var requestTypeId = (await requestTypeService.CreateAsync( var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request", null, null, "General"), CancellationToken.None)).Value.Id; new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync( var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
await stepService.CreateAsync( await stepService.CreateAsync(
route.Value.Id, route.Value.Id,
@@ -373,7 +371,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Chief not found request"); var requestTypeId = await CreateRequestTypeAsync("Chief not found request");
var route = await _service.CreateAsync( var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
@@ -401,7 +399,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Position not found request"); var requestTypeId = await CreateRequestTypeAsync("Position not found request");
var route = await _service.CreateAsync( var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
@@ -429,7 +427,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Project role not found request"); var requestTypeId = await CreateRequestTypeAsync("Project role not found request");
var route = await _service.CreateAsync( var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
@@ -457,7 +455,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
// Arrange // Arrange
var requestTypeId = await CreateRequestTypeAsync("Company-wide request"); var requestTypeId = await CreateRequestTypeAsync("Company-wide request");
var route = await _service.CreateAsync( var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId: null, CompanyId, ByProject: false, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, DepartmentId: null, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
// Action // Action
@@ -31,7 +31,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, siteUrl, "General"), CancellationToken.None)).Value.Id; new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, siteUrl, "General"), CancellationToken.None)).Value.Id;
var route = await _routeService.CreateAsync( var route = await _routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, null, CompanyId, byProject, RequiresControl: false), new CreateTemplateRouteRequest(requestTypeId, null, CompanyId, byProject, RequiresControl: false),
CancellationToken.None); CancellationToken.None);
return route.Value.Id; return route.Value.Id;
@@ -3,7 +3,7 @@ import styles from './ResolveScreen.module.scss';
import { ApprovalRouteApi } from '../../services/api'; import { ApprovalRouteApi } from '../../services/api';
import { PortalDictionaryClient } from '../../services/portalDictionary'; import { PortalDictionaryClient } from '../../services/portalDictionary';
import { Project, RouteStepStatus } from '../../models'; import { Project, RouteStepStatus } from '../../models';
import { routeStepStatusLabels, routeTypeLabels, texts } from '../../texts'; import { routeStepStatusLabels, texts } from '../../texts';
import { useRequestTypes } from '../../hooks/useRequestTypes'; import { useRequestTypes } from '../../hooks/useRequestTypes';
import { useResolveRoute } from '../../hooks/useResolveRoute'; import { useResolveRoute } from '../../hooks/useResolveRoute';
import { usePortalEmployees } from '../../hooks/usePortalEmployees'; import { usePortalEmployees } from '../../hooks/usePortalEmployees';
@@ -137,7 +137,7 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
{result && ( {result && (
<> <>
<Alert kind="ok"> <Alert kind="ok">
{texts.resolve.summary(result.routeId, routeTypeLabels[result.routeType], result.requiresControl)} {texts.resolve.summary(result.routeId, result.requiresControl)}
</Alert> </Alert>
{unresolvedCount > 0 && <Alert kind="warn">{texts.resolve.unresolved(unresolvedCount)}</Alert>} {unresolvedCount > 0 && <Alert kind="warn">{texts.resolve.unresolved(unresolvedCount)}</Alert>}
<DataTable <DataTable
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Company, Department, RequestType, RouteType, TemplateRoute } from '../../models'; import { Company, Department, RequestType, TemplateRoute } from '../../models';
import { routeTypeLabels, texts } from '../../texts'; import { texts } from '../../texts';
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit'; import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
import { ApprovalRouteApi } from '../../services/api'; import { ApprovalRouteApi } from '../../services/api';
import { Checkbox, Field, FormGrid, FormModal, SearchableSelect, Select } from '../ui'; import { Checkbox, Field, FormGrid, FormModal, SearchableSelect, Select } from '../ui';
@@ -21,22 +21,16 @@ export interface IRouteFormModalProps {
interface IFormState { interface IFormState {
requestTypeId: string; requestTypeId: string;
routeType: RouteType;
companyId: string; companyId: string;
departmentId: string; departmentId: string;
byProject: boolean; byProject: boolean;
requiresControl: boolean; requiresControl: boolean;
} }
const ROUTE_TYPE_OPTIONS = Object.values(RouteType)
.filter(v => v !== RouteType.None)
.map(v => ({ value: v, label: routeTypeLabels[v] }));
function toFormState(editing: TemplateRoute | undefined, initialRequestTypeId: string): IFormState { function toFormState(editing: TemplateRoute | undefined, initialRequestTypeId: string): IFormState {
if (!editing) { if (!editing) {
return { return {
requestTypeId: initialRequestTypeId, requestTypeId: initialRequestTypeId,
routeType: RouteType.Sequential,
companyId: '', companyId: '',
departmentId: '', departmentId: '',
byProject: false, byProject: false,
@@ -45,7 +39,6 @@ function toFormState(editing: TemplateRoute | undefined, initialRequestTypeId: s
} }
return { return {
requestTypeId: String(editing.requestTypeId), requestTypeId: String(editing.requestTypeId),
routeType: editing.routeType,
companyId: editing.companyId, companyId: editing.companyId,
departmentId: editing.departmentId ?? '', departmentId: editing.departmentId ?? '',
byProject: editing.byProject, byProject: editing.byProject,
@@ -88,14 +81,12 @@ export const RouteFormModal: React.FC<IRouteFormModalProps> = ({
const departmentId = form.departmentId.trim() ? form.departmentId.trim() : null; const departmentId = form.departmentId.trim() ? form.departmentId.trim() : null;
const request = editing const request = editing
? api.templateRoutes.update(editing.id, { ? api.templateRoutes.update(editing.id, {
routeType: form.routeType,
companyId: form.companyId.trim(), companyId: form.companyId.trim(),
departmentId, departmentId,
byProject: form.byProject, byProject: form.byProject,
requiresControl: form.requiresControl requiresControl: form.requiresControl
}) })
: api.templateRoutes.create({ : api.templateRoutes.create({
routeType: form.routeType,
requestTypeId: Number(form.requestTypeId), requestTypeId: Number(form.requestTypeId),
companyId: form.companyId.trim(), companyId: form.companyId.trim(),
departmentId, departmentId,
@@ -129,13 +120,6 @@ export const RouteFormModal: React.FC<IRouteFormModalProps> = ({
onChange={e => setForm({ ...form, requestTypeId: e.target.value })} onChange={e => setForm({ ...form, requestTypeId: e.target.value })}
/> />
</Field> </Field>
<Field label={texts.routes.routeTypeLabel} required full>
<Select
options={ROUTE_TYPE_OPTIONS}
value={form.routeType}
onChange={e => setForm({ ...form, routeType: e.target.value as RouteType })}
/>
</Field>
<Field label={texts.routes.companyLabel} required full hint={texts.routes.companyHint}> <Field label={texts.routes.companyLabel} required full hint={texts.routes.companyHint}>
<SearchableSelect <SearchableSelect
options={companies.map(c => ({ value: c.id, label: c.name }))} options={companies.map(c => ({ value: c.id, label: c.name }))}
@@ -4,7 +4,7 @@ import { ApprovalRouteApi } from '../../services/api';
import { PortalDictionaryClient } from '../../services/portalDictionary'; import { PortalDictionaryClient } from '../../services/portalDictionary';
import { SharePointListsClient } from '../../services/sharePointLists'; import { SharePointListsClient } from '../../services/sharePointLists';
import { RequestType, RouteStepStatus, TemplateRoute, TemplateRouteStep } from '../../models'; import { RequestType, RouteStepStatus, TemplateRoute, TemplateRouteStep } from '../../models';
import { routeStepStatusLabels, routeTypeLabels, texts } from '../../texts'; import { routeStepStatusLabels, texts } from '../../texts';
import { describeStepCondition, describeStepExecutor } from '../../utils/stepExecutor'; import { describeStepCondition, describeStepExecutor } from '../../utils/stepExecutor';
import { useRouteSteps } from '../../hooks/useRouteSteps'; import { useRouteSteps } from '../../hooks/useRouteSteps';
import { useDeleteTarget } from '../../hooks/useDeleteTarget'; import { useDeleteTarget } from '../../hooks/useDeleteTarget';
@@ -76,7 +76,7 @@ export const RouteStepsScreen: React.FC<IRouteStepsScreenProps> = ({ api, route,
/> />
<PageHeader <PageHeader
icon="flag-check" icon="flag-check"
subtitle={texts.routeSteps.subtitle(requestTypeName, routeTypeLabels[route.routeType])} subtitle={texts.routeSteps.subtitle(requestTypeName)}
title={texts.routeSteps.title(route.id)} title={texts.routeSteps.title(route.id)}
actions={ actions={
<> <>
@@ -3,8 +3,8 @@ import styles from './RoutesScreen.module.scss';
import { RouteFormModal } from './RouteFormModal'; import { RouteFormModal } from './RouteFormModal';
import { ApprovalRouteApi } from '../../services/api'; import { ApprovalRouteApi } from '../../services/api';
import { PortalDictionaryClient } from '../../services/portalDictionary'; import { PortalDictionaryClient } from '../../services/portalDictionary';
import { RouteType, TemplateRoute } from '../../models'; import { TemplateRoute } from '../../models';
import { routeTypeLabels, texts } from '../../texts'; import { texts } from '../../texts';
import { shortGuid } from '../../utils/guid'; import { shortGuid } from '../../utils/guid';
import { useRequestTypes } from '../../hooks/useRequestTypes'; import { useRequestTypes } from '../../hooks/useRequestTypes';
import { useTemplateRoutes } from '../../hooks/useTemplateRoutes'; import { useTemplateRoutes } from '../../hooks/useTemplateRoutes';
@@ -92,11 +92,6 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, on
header: texts.routes.columnRequestType, header: texts.routes.columnRequestType,
render: r => <PrimaryCell>{requestTypeName(r.requestTypeId)}</PrimaryCell> render: r => <PrimaryCell>{requestTypeName(r.requestTypeId)}</PrimaryCell>
}, },
{
key: 'routeType',
header: texts.routes.columnRouteType,
render: r => <Badge variant={r.routeType === RouteType.Parallel ? 'blue' : 'gold'}>{routeTypeLabels[r.routeType]}</Badge>
},
{ key: 'company', header: texts.routes.columnCompany, render: r => companyName(r.companyId) }, { key: 'company', header: texts.routes.columnCompany, render: r => companyName(r.companyId) },
{ key: 'department', header: texts.routes.columnDepartment, render: r => (r.departmentId ? departmentName(r.departmentId) : texts.common.empty) }, { key: 'department', header: texts.routes.columnDepartment, render: r => (r.departmentId ? departmentName(r.departmentId) : texts.common.empty) },
{ {
@@ -2,12 +2,6 @@
// Значения должны 1:1 соответствовать C#-стороне: они ходят по сети как строки. // Значения должны 1:1 соответствовать C#-стороне: они ходят по сети как строки.
// Человекочитаемые подписи к этим значениям живут не здесь, а в texts.ts. // Человекочитаемые подписи к этим значениям живут не здесь, а в texts.ts.
export enum RouteType {
None = 'None',
Parallel = 'Parallel',
Sequential = 'Sequential'
}
export enum RouteStepStatus { export enum RouteStepStatus {
None = 'None', None = 'None',
Approval = 'Approval', Approval = 'Approval',
@@ -1,7 +1,7 @@
// DTO эндпоинта api/template-routes/resolve // DTO эндпоинта api/template-routes/resolve
// (Application/TemplateRoutes/ResolveTemplateRouteDto.cs). // (Application/TemplateRoutes/ResolveTemplateRouteDto.cs).
import { RouteStepStatus, RouteType } from './enums'; import { RouteStepStatus } from './enums';
export interface ResolveTemplateRouteQuery { export interface ResolveTemplateRouteQuery {
requestTypeId: number; requestTypeId: number;
@@ -22,7 +22,6 @@ export interface ResolvedRouteStep {
export interface ResolveTemplateRouteResult { export interface ResolveTemplateRouteResult {
routeId: number; routeId: number;
routeType: RouteType;
requiresControl: boolean; requiresControl: boolean;
steps: ResolvedRouteStep[]; steps: ResolvedRouteStep[];
} }
@@ -1,10 +1,7 @@
// DTO эндпоинта api/template-routes (Application/TemplateRoutes/*.cs). // DTO эндпоинта api/template-routes (Application/TemplateRoutes/*.cs).
import { RouteType } from './enums';
export interface TemplateRoute { export interface TemplateRoute {
id: number; id: number;
routeType: RouteType;
requestTypeId: number; requestTypeId: number;
departmentId: string | null; departmentId: string | null;
companyId: string; companyId: string;
@@ -13,7 +10,6 @@ export interface TemplateRoute {
} }
export interface CreateTemplateRouteInput { export interface CreateTemplateRouteInput {
routeType: RouteType;
requestTypeId: number; requestTypeId: number;
departmentId: string | null; departmentId: string | null;
companyId: string; companyId: string;
@@ -23,7 +19,6 @@ export interface CreateTemplateRouteInput {
// Тип заявки у существующего маршрута не меняется — его нет в update-контракте. // Тип заявки у существующего маршрута не меняется — его нет в update-контракте.
export interface UpdateTemplateRouteInput { export interface UpdateTemplateRouteInput {
routeType: RouteType;
departmentId: string | null; departmentId: string | null;
companyId: string; companyId: string;
byProject: boolean; byProject: boolean;
@@ -31,7 +26,6 @@ export interface UpdateTemplateRouteInput {
} }
export interface PatchTemplateRouteInput { export interface PatchTemplateRouteInput {
routeType?: RouteType;
departmentId?: string | null; departmentId?: string | null;
companyId?: string; companyId?: string;
byProject?: boolean; byProject?: boolean;
+4 -13
View File
@@ -4,19 +4,13 @@
// //
// Строки с подстановкой оформлены функциями, чтобы шаблон и данные не разъезжались. // Строки с подстановкой оформлены функциями, чтобы шаблон и данные не разъезжались.
import { ComparisonOperator, HeadType, ProjectRole, RouteStepStatus, RouteType } from './models'; import { ComparisonOperator, HeadType, ProjectRole, RouteStepStatus } from './models';
import { pluralRu } from './utils/plural'; import { pluralRu } from './utils/plural';
// ---- Подписи enum'ов ---- // ---- Подписи enum'ов ----
// Record<enum, string> гарантирует, что при добавлении значения в enum // Record<enum, string> гарантирует, что при добавлении значения в enum
// компилятор потребует и подпись к нему. // компилятор потребует и подпись к нему.
export const routeTypeLabels: Record<RouteType, string> = {
[RouteType.None]: 'Не задан',
[RouteType.Parallel]: 'Параллельный',
[RouteType.Sequential]: 'Последовательный'
};
export const routeStepStatusLabels: Record<RouteStepStatus, string> = { export const routeStepStatusLabels: Record<RouteStepStatus, string> = {
[RouteStepStatus.None]: 'Не задан', [RouteStepStatus.None]: 'Не задан',
[RouteStepStatus.Approval]: 'Согласование', [RouteStepStatus.Approval]: 'Согласование',
@@ -149,7 +143,6 @@ export const texts = {
dictionaryFailed: (message: string): string => `Справочник компаний и отделов недоступен: ${message}`, dictionaryFailed: (message: string): string => `Справочник компаний и отделов недоступен: ${message}`,
emptyList: 'Маршрутов пока нет.', emptyList: 'Маршрутов пока нет.',
columnRequestType: 'Тип заявки', columnRequestType: 'Тип заявки',
columnRouteType: 'Тип маршрута',
columnCompany: 'Компания', columnCompany: 'Компания',
columnDepartment: 'Отдел', columnDepartment: 'Отдел',
columnByProject: 'По проекту', columnByProject: 'По проекту',
@@ -160,7 +153,6 @@ export const texts = {
editTitle: (routeId: number): string => `Маршрут #${routeId}`, editTitle: (routeId: number): string => `Маршрут #${routeId}`,
requestTypeLabel: 'Тип заявки', requestTypeLabel: 'Тип заявки',
requestTypePlaceholder: 'Выберите тип заявки', requestTypePlaceholder: 'Выберите тип заявки',
routeTypeLabel: 'Тип маршрута',
companyLabel: 'Компания', companyLabel: 'Компания',
companyHint: 'Справочник 1С (api.tnsu.kz).', companyHint: 'Справочник 1С (api.tnsu.kz).',
companyPlaceholder: 'Выберите компанию', companyPlaceholder: 'Выберите компанию',
@@ -185,8 +177,7 @@ export const texts = {
routeSteps: { routeSteps: {
breadcrumb: (routeId: number): string => `Шаги маршрута #${routeId}`, breadcrumb: (routeId: number): string => `Шаги маршрута #${routeId}`,
title: (routeId: number): string => `Маршрут #${routeId}`, title: (routeId: number): string => `Маршрут #${routeId}`,
subtitle: (requestTypeName: string, routeTypeLabel: string): string => subtitle: (requestTypeName: string): string => requestTypeName,
`${requestTypeName} · ${routeTypeLabel}`,
back: 'К маршрутам', back: 'К маршрутам',
add: 'Добавить шаг', add: 'Добавить шаг',
emptyList: 'Шагов пока нет — добавьте первый.', emptyList: 'Шагов пока нет — добавьте первый.',
@@ -260,8 +251,8 @@ export const texts = {
run: 'Проверить', run: 'Проверить',
running: 'Проверка...', running: 'Проверка...',
validationRequestType: 'Выберите тип заявки.', validationRequestType: 'Выберите тип заявки.',
summary: (routeId: number, routeTypeLabel: string, requiresControl: boolean): string => summary: (routeId: number, requiresControl: boolean): string =>
`Маршрут #${routeId} · ${routeTypeLabel}${requiresControl ? ' · требуется контроль исполнения' : ''}`, `Маршрут #${routeId}${requiresControl ? ' · требуется контроль исполнения' : ''}`,
unresolved: (count: number): string => unresolved: (count: number): string =>
`Исполнитель не определён: ${count} ${pluralRu(count, ['шаг', 'шага', 'шагов'])} — причина указана в строке.`, `Исполнитель не определён: ${count} ${pluralRu(count, ['шаг', 'шага', 'шагов'])} — причина указана в строке.`,
positionMissing: 'Должность не указана в карточке сотрудника.', positionMissing: 'Должность не указана в карточке сотрудника.',