Изображения в услугах, поиске, возможность открыть карточку услуги
This commit is contained in:
@@ -43,7 +43,7 @@ public static class SearchEndpoints
|
||||
}
|
||||
|
||||
var nearbyGeoRaw = await nearbyGeoQuery
|
||||
.Select(x => new { x.Id, x.State, Latitude = x.Location.Coordinate.Y, Longitude = x.Location.Coordinate.X })
|
||||
.Select(x => new { x.Id, x.State, Latitude = x.Location.Coordinate.Y, Longitude = x.Location.Coordinate.X, x.LastUpdatedAt })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var nearbyGeo = nearbyGeoRaw.Select(x => new
|
||||
@@ -52,6 +52,7 @@ public static class SearchEndpoints
|
||||
x.State,
|
||||
x.Latitude,
|
||||
x.Longitude,
|
||||
x.LastUpdatedAt,
|
||||
DistanceMeters = CalculateDistanceInMeters(lat, lon, x.Latitude, x.Longitude)
|
||||
}).ToList();
|
||||
|
||||
@@ -60,7 +61,7 @@ public static class SearchEndpoints
|
||||
|
||||
// 2. Ищем совпадения по услугам (CatalogDbContext)
|
||||
var offersQuery = catalogDb.Offers
|
||||
.Where(o => nearbyIds.Contains(o.PerformerId) && o.IsActive);
|
||||
.Where(o => nearbyIds.Contains(o.PerformerId) && o.IsActive && !o.IsDeleted);
|
||||
|
||||
var qLower = q?.ToLower();
|
||||
if (!string.IsNullOrWhiteSpace(qLower))
|
||||
@@ -72,10 +73,10 @@ public static class SearchEndpoints
|
||||
}
|
||||
|
||||
var matchedOffersRaw = await offersQuery
|
||||
.Select(o => new { o.Id, o.PerformerId, o.Title, Description = o.Description ?? "", o.Price.Amount, o.Price.Currency })
|
||||
.Select(o => new { o.Id, o.PerformerId, o.Title, Description = o.Description ?? "", o.Price.Amount, o.Price.Currency, o.Images })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var matchedOffers = matchedOffersRaw.Select(o => new { o.Id, o.PerformerId, o.Title, o.Description, PriceAmount = o.Amount, PriceCurrency = "₽" }).ToList();
|
||||
var matchedOffers = matchedOffersRaw.Select(o => new { o.Id, o.PerformerId, o.Title, o.Description, PriceAmount = o.Amount, PriceCurrency = "₽", ImageUrl = o.Images.FirstOrDefault() }).ToList();
|
||||
|
||||
var performersWithOffers = matchedOffers.Select(o => o.PerformerId).Distinct().ToList();
|
||||
|
||||
@@ -119,6 +120,7 @@ public static class SearchEndpoints
|
||||
|
||||
string finalStatus = CalculateSmartStatus(
|
||||
g.State.ToString(), // Передаем реальный статус из Geo
|
||||
g.LastUpdatedAt, // Передаем время последнего обновления
|
||||
p.WorkSchedule?.IsAlwaysReady ?? false,
|
||||
p.WorkSchedule?.WorkingDays,
|
||||
currentDay,
|
||||
@@ -164,10 +166,15 @@ public static class SearchEndpoints
|
||||
.WithTags("Search");
|
||||
}
|
||||
|
||||
private static string CalculateSmartStatus(string liveState, bool isAlwaysReady, string? workingDaysJson, string currentDay, TimeSpan currentTime)
|
||||
private static string CalculateSmartStatus(string liveState, DateTime lastUpdatedAt, bool isAlwaysReady, string? workingDaysJson, string currentDay, TimeSpan currentTime)
|
||||
{
|
||||
if (liveState != "Available") return "Офлайн";
|
||||
if (isAlwaysReady) return "Готов к заказу";
|
||||
|
||||
// Если пользователь вручную обновил статус 'Available' сегодня, он остается онлайн до конца дня,
|
||||
// игнорируя расписание на сегодня. В следующий день снова начнет работать расписание.
|
||||
if (lastUpdatedAt.Date == DateTime.UtcNow.Date) return "Готов к заказу";
|
||||
|
||||
if (string.IsNullOrEmpty(workingDaysJson)) return "Готов к заказу"; // Если расписания нет - считаем готовым
|
||||
|
||||
try
|
||||
|
||||
@@ -36,13 +36,16 @@ public record CreateOfferCommand : IRequest<Result<Guid>>
|
||||
/// </summary>
|
||||
public JsonDocument? Attributes { get; init; }
|
||||
|
||||
public CreateOfferCommand(Guid categoryId, string title, string description, Price price, JsonDocument? attributes)
|
||||
public List<string>? Images { get; init; }
|
||||
|
||||
public CreateOfferCommand(Guid categoryId, string title, string description, Price price, JsonDocument? attributes, List<string>? images)
|
||||
{
|
||||
CategoryId = categoryId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
Images = images;
|
||||
}
|
||||
|
||||
public CreateOfferCommand() { }
|
||||
@@ -70,7 +73,8 @@ public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Res
|
||||
request.Title,
|
||||
request.Description,
|
||||
request.Price,
|
||||
request.Attributes
|
||||
request.Attributes,
|
||||
request.Images
|
||||
);
|
||||
|
||||
await _repository.AddAsync(offer, cancellationToken);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
|
||||
public record DeleteOfferCommand(Guid OfferId) : IRequest<bool>;
|
||||
|
||||
public class DeleteOfferCommandHandler : IRequestHandler<DeleteOfferCommand, bool>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public DeleteOfferCommandHandler(IOfferRepository repository, ICurrentUserService currentUser)
|
||||
{
|
||||
_repository = repository;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(DeleteOfferCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var offer = await _repository.GetByIdAsync(request.OfferId, cancellationToken);
|
||||
if (offer == null) throw new Exception("Offer not found");
|
||||
|
||||
if (offer.PerformerId != _currentUser.UserId)
|
||||
throw new UnauthorizedAccessException("Not your offer");
|
||||
|
||||
offer.Delete();
|
||||
await _repository.UpdateAsync(offer, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
|
||||
public record ToggleOfferStatusCommand(Guid OfferId) : IRequest<bool>;
|
||||
|
||||
public class ToggleOfferStatusCommandHandler : IRequestHandler<ToggleOfferStatusCommand, bool>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public ToggleOfferStatusCommandHandler(IOfferRepository repository, ICurrentUserService currentUser)
|
||||
{
|
||||
_repository = repository;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(ToggleOfferStatusCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var offer = await _repository.GetByIdAsync(request.OfferId, cancellationToken);
|
||||
if (offer == null) throw new Exception("Offer not found");
|
||||
|
||||
if (offer.PerformerId != _currentUser.UserId)
|
||||
throw new UnauthorizedAccessException("Not your offer");
|
||||
|
||||
offer.ToggleActive();
|
||||
await _repository.UpdateAsync(offer, cancellationToken);
|
||||
return offer.IsActive;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||
using Nashel.Modules.Catalog.Domain.Aggregates;
|
||||
using Nashel.Modules.Catalog.Domain.Repositories;
|
||||
using Nashel.Modules.Catalog.Domain.ValueObjects;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
|
||||
public record UpdateOfferCommand(
|
||||
Guid OfferId,
|
||||
string Title,
|
||||
string Description,
|
||||
decimal PriceAmount,
|
||||
int PriceType, // 0-Fixed, 1-Hourly, 2-Negotiable
|
||||
Dictionary<string, string>? Attributes,
|
||||
List<string>? Images) : IRequest<bool>;
|
||||
|
||||
public class UpdateOfferCommandHandler : IRequestHandler<UpdateOfferCommand, bool>
|
||||
{
|
||||
private readonly IOfferRepository _repository;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public UpdateOfferCommandHandler(IOfferRepository repository, ICurrentUserService currentUser)
|
||||
{
|
||||
_repository = repository;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(UpdateOfferCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var offer = await _repository.GetByIdAsync(request.OfferId, cancellationToken);
|
||||
if (offer == null) throw new Exception("Offer not found");
|
||||
|
||||
if (offer.PerformerId != _currentUser.UserId)
|
||||
throw new UnauthorizedAccessException("Not your offer");
|
||||
|
||||
var price = new Price(request.PriceAmount, (Nashel.Modules.Catalog.Domain.Enums.OfferType)request.PriceType);
|
||||
var jsonAttrs = request.Attributes != null && request.Attributes.Count > 0
|
||||
? JsonDocument.Parse(JsonSerializer.Serialize(request.Attributes))
|
||||
: null;
|
||||
|
||||
offer.Update(request.Title, request.Description, price, jsonAttrs, request.Images);
|
||||
|
||||
await _repository.UpdateAsync(offer, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using MediatR;
|
||||
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||
|
||||
namespace Nashel.Modules.Catalog.Application.Commands;
|
||||
|
||||
public record UploadOfferImageCommand(byte[] Content, string FileName) : IRequest<string>;
|
||||
|
||||
public class UploadOfferImageCommandHandler : IRequestHandler<UploadOfferImageCommand, string>
|
||||
{
|
||||
private const long MaxFileSize = 5 * 1024 * 1024; // 5MB
|
||||
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".webp" };
|
||||
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public UploadOfferImageCommandHandler(ICurrentUserService currentUserService)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
public Task<string> Handle(UploadOfferImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.UserId;
|
||||
if (userId == null) throw new UnauthorizedAccessException();
|
||||
|
||||
if (request.Content.Length > MaxFileSize)
|
||||
throw new InvalidOperationException("Размер файла не должен превышать 5 МБ");
|
||||
|
||||
var extension = Path.GetExtension(request.FileName).ToLowerInvariant();
|
||||
if (Array.IndexOf(AllowedExtensions, extension) == -1)
|
||||
throw new InvalidOperationException("Допустимые форматы: JPG, PNG, WEBP");
|
||||
|
||||
var base64String = Convert.ToBase64String(request.Content);
|
||||
|
||||
var mimeType = extension switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".webp" => "image/webp",
|
||||
_ => "image/jpeg"
|
||||
};
|
||||
|
||||
return Task.FromResult($"data:{mimeType};base64,{base64String}");
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,12 @@ public record OfferDto
|
||||
/// </summary>
|
||||
public bool IsActive { get; init; }
|
||||
|
||||
public OfferDto(Guid id, Guid performerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes, bool isActive)
|
||||
/// <summary>
|
||||
/// Изображения услуги.
|
||||
/// </summary>
|
||||
public List<string> Images { get; init; } = new();
|
||||
|
||||
public OfferDto(Guid id, Guid performerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes, bool isActive, List<string>? images = null)
|
||||
{
|
||||
Id = id;
|
||||
PerformerId = performerId;
|
||||
@@ -100,6 +105,10 @@ public record OfferDto
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
IsActive = isActive;
|
||||
if (images != null)
|
||||
{
|
||||
Images.AddRange(images);
|
||||
}
|
||||
}
|
||||
|
||||
public OfferDto() { }
|
||||
|
||||
@@ -37,7 +37,8 @@ public class GetMyOffersQueryHandler : IRequestHandler<GetMyOffersQuery, Result<
|
||||
offer.Description ?? string.Empty,
|
||||
offer.Price,
|
||||
offer.Attributes,
|
||||
offer.IsActive
|
||||
offer.IsActive,
|
||||
offer.Images
|
||||
)).ToList();
|
||||
|
||||
return Result<List<OfferDto>>.Success(list);
|
||||
|
||||
@@ -32,7 +32,8 @@ public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, Resul
|
||||
offer.Description ?? string.Empty,
|
||||
offer.Price,
|
||||
offer.Attributes,
|
||||
offer.IsActive
|
||||
offer.IsActive,
|
||||
offer.Images
|
||||
);
|
||||
|
||||
return Result<OfferDto>.Success(dto);
|
||||
|
||||
@@ -48,18 +48,28 @@ public class Offer
|
||||
/// </summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Удалено ли объявление (Soft Delete).
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Дата создания.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Коллекция изображений (Base64 URL)
|
||||
/// </summary>
|
||||
public List<string> Images { get; private set; } = new();
|
||||
|
||||
// Конструктор по умолчанию для EF Core
|
||||
private Offer() { }
|
||||
|
||||
/// <summary>
|
||||
/// Создает новый оффер.
|
||||
/// </summary>
|
||||
public Offer(Guid performerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes = null)
|
||||
public Offer(Guid performerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes = null, List<string>? images = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
PerformerId = performerId;
|
||||
@@ -69,6 +79,36 @@ public class Offer
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
IsActive = true;
|
||||
IsDeleted = false;
|
||||
CreatedAt = DateTimeOffset.UtcNow;
|
||||
if (images != null)
|
||||
{
|
||||
Images.AddRange(images);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(string title, string description, Price price, JsonDocument? attributes, List<string>? images)
|
||||
{
|
||||
Title = title;
|
||||
Description = description;
|
||||
Price = price;
|
||||
Attributes = attributes;
|
||||
|
||||
Images.Clear();
|
||||
if (images != null)
|
||||
{
|
||||
Images.AddRange(images);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleActive()
|
||||
{
|
||||
IsActive = !IsActive;
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
IsDeleted = true;
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ public interface IOfferRepository
|
||||
Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<List<Offer>> GetByPerformerIdAsync(Guid performerId, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Offer offer, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Offer offer, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Nashel.Modules.Catalog.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(CatalogDbContext))]
|
||||
[Migration("20260306185654_AddOfferImagesAndSoftDelete")]
|
||||
partial class AddOfferImagesAndSoftDelete
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("AttributeSchema")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Categories", "catalog");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<JsonDocument>("Attributes")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Images")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("PerformerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Attributes");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Attributes"), "gin");
|
||||
|
||||
b.ToTable("Offers", "catalog");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
|
||||
{
|
||||
b.HasOne("Nashel.Modules.Catalog.Domain.Aggregates.Category", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
|
||||
{
|
||||
b.OwnsOne("Nashel.Modules.Catalog.Domain.ValueObjects.Price", "Price", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("OfferId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric")
|
||||
.HasColumnName("PriceAmount");
|
||||
|
||||
b1.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("character varying(3)")
|
||||
.HasColumnName("PriceCurrency");
|
||||
|
||||
b1.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("PriceType");
|
||||
|
||||
b1.HasKey("OfferId");
|
||||
|
||||
b1.ToTable("Offers", "catalog");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("OfferId");
|
||||
});
|
||||
|
||||
b.Navigation("Price")
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOfferImagesAndSoftDelete : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<List<string>>(
|
||||
name: "Images",
|
||||
schema: "catalog",
|
||||
table: "Offers",
|
||||
type: "text[]",
|
||||
nullable: false,
|
||||
defaultValue: new string[0]);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
schema: "catalog",
|
||||
table: "Offers",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Images",
|
||||
schema: "catalog",
|
||||
table: "Offers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
schema: "catalog",
|
||||
table: "Offers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -72,9 +73,16 @@ namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Images")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("PerformerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
|
||||
@@ -32,4 +32,10 @@ public class OfferRepository : IOfferRepository
|
||||
await _context.Offers.AddAsync(offer, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Offer offer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Offers.Update(offer);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,5 +62,49 @@ public static class CatalogEndpoints
|
||||
})
|
||||
.WithName("GetOfferById")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Получить детали услуги по ID", Description = "Возвращает полную информацию об услуге." });
|
||||
|
||||
protectedOffersGroup.MapPut("/offers/{id:guid}", async (Guid id, [FromBody] UpdateOfferPayload payload, ISender sender) =>
|
||||
{
|
||||
var command = new UpdateOfferCommand(id, payload.Title, payload.Description, payload.Price.Amount, payload.Price.Type, payload.Attributes, payload.Images);
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithName("UpdateOffer")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Обновить услугу" });
|
||||
|
||||
protectedOffersGroup.MapPatch("/offers/{id:guid}/toggle", async (Guid id, ISender sender) =>
|
||||
{
|
||||
var isActive = await sender.Send(new ToggleOfferStatusCommand(id));
|
||||
return Results.Ok(new { IsActive = isActive });
|
||||
})
|
||||
.WithName("ToggleOfferStatus")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Приостановить/активировать услугу" });
|
||||
|
||||
protectedOffersGroup.MapDelete("/offers/{id:guid}", async (Guid id, ISender sender) =>
|
||||
{
|
||||
await sender.Send(new DeleteOfferCommand(id));
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithName("DeleteOffer")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Удалить услугу" });
|
||||
|
||||
protectedOffersGroup.MapPost("/offers/image", async (IFormFile file, ISender sender) =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms);
|
||||
var url = await sender.Send(new UploadOfferImageCommand(ms.ToArray(), file.FileName));
|
||||
return Results.Ok(new { url });
|
||||
})
|
||||
.WithName("UploadOfferImage")
|
||||
.WithOpenApi(operation => new(operation) { Summary = "Загрузить изображение для услуги" })
|
||||
.DisableAntiforgery();
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateOfferPayload(
|
||||
string Title,
|
||||
string Description,
|
||||
PricePayload Price,
|
||||
Dictionary<string, string>? Attributes,
|
||||
List<string>? Images);
|
||||
public record PricePayload(decimal Amount, int Type);
|
||||
|
||||
Reference in New Issue
Block a user