diff --git a/src/Meezi.API/Controllers/CafePublicProfileController.cs b/src/Meezi.API/Controllers/CafePublicProfileController.cs index baba028..7b34e40 100644 --- a/src/Meezi.API/Controllers/CafePublicProfileController.cs +++ b/src/Meezi.API/Controllers/CafePublicProfileController.cs @@ -57,7 +57,8 @@ public class CafePublicProfileController : CafeApiControllerBase gallery, cafe.InstagramHandle, cafe.WebsiteUrl, - ToHoursDto(hours)))); + ToHoursDto(hours), + cafe.ShowOnKoja))); } // ── PUT (description / social / hours) ─────────────────────────────────── @@ -91,6 +92,10 @@ public class CafePublicProfileController : CafeApiControllerBase if (request.WorkingHours is not null) cafe.WorkingHoursJson = JsonSerializer.Serialize(ToHoursSchedule(request.WorkingHours), _jsonOpts); + // Koja (public discovery) listing preference + if (request.ShowOnKoja.HasValue) + cafe.ShowOnKoja = request.ShowOnKoja.Value; + await _db.SaveChangesAsync(ct); var gallery = Deserialize>(cafe.GalleryJson) ?? []; @@ -101,7 +106,8 @@ public class CafePublicProfileController : CafeApiControllerBase gallery, cafe.InstagramHandle, cafe.WebsiteUrl, - ToHoursDto(hours)))); + ToHoursDto(hours), + cafe.ShowOnKoja))); } // ── POST gallery/upload ─────────────────────────────────────────────────── @@ -207,13 +213,15 @@ public record UpdateCafePublicProfileRequest( string? Description, string? InstagramHandle, string? WebsiteUrl, - WorkingHoursPublicDto? WorkingHours); + WorkingHoursPublicDto? WorkingHours, + bool? ShowOnKoja = null); public record CafeProfileEditDto( string? Description, IReadOnlyList GalleryUrls, string? InstagramHandle, string? WebsiteUrl, - WorkingHoursPublicDto? WorkingHours); + WorkingHoursPublicDto? WorkingHours, + bool ShowOnKoja); public record GalleryDto(IReadOnlyList GalleryUrls); diff --git a/src/Meezi.API/Controllers/InventoryController.cs b/src/Meezi.API/Controllers/InventoryController.cs index 69dc5b4..4187de8 100644 --- a/src/Meezi.API/Controllers/InventoryController.cs +++ b/src/Meezi.API/Controllers/InventoryController.cs @@ -61,6 +61,19 @@ public class InventoryController : CafeApiControllerBase return Ok(new ApiResponse(true, updated)); } + [HttpDelete("ingredients/{ingredientId}")] + public async Task Delete( + string cafeId, + string ingredientId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var deleted = await _inventory.DeleteAsync(cafeId, ingredientId, ct); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id = ingredientId })); + } + [HttpPost("ingredients/{ingredientId}/adjust")] public async Task Adjust( string cafeId, diff --git a/src/Meezi.API/Controllers/ReservationsController.cs b/src/Meezi.API/Controllers/ReservationsController.cs index a13b58d..8b33fa3 100644 --- a/src/Meezi.API/Controllers/ReservationsController.cs +++ b/src/Meezi.API/Controllers/ReservationsController.cs @@ -66,6 +66,19 @@ public class ReservationsController : CafeApiControllerBase if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); } + + [HttpDelete("{id}")] + public async Task Delete( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var deleted = await _reservations.DeleteAsync(cafeId, id, ct); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } } public record UpdateReservationStatusRequest(ReservationStatus Status); diff --git a/src/Meezi.API/Services/InventoryService.cs b/src/Meezi.API/Services/InventoryService.cs index 1c2f684..b7a93bb 100644 --- a/src/Meezi.API/Services/InventoryService.cs +++ b/src/Meezi.API/Services/InventoryService.cs @@ -89,6 +89,7 @@ public interface IInventoryService Task> LowStockAsync(string cafeId, CancellationToken ct = default); Task CreateAsync(string cafeId, CreateIngredientRequest request, CancellationToken ct = default); Task UpdateAsync(string cafeId, string ingredientId, UpdateIngredientRequest request, CancellationToken ct = default); + Task DeleteAsync(string cafeId, string ingredientId, CancellationToken ct = default); Task AdjustAsync( string cafeId, string ingredientId, @@ -205,6 +206,18 @@ public class InventoryService : IInventoryService return ToDto(entity); } + public async Task DeleteAsync(string cafeId, string ingredientId, CancellationToken ct = default) + { + var entity = await _db.Ingredients.FirstOrDefaultAsync(i => i.Id == ingredientId && i.CafeId == cafeId, ct); + if (entity is null) return false; + + // Soft delete: Ingredient has a global DeletedAt query filter, so it (and its + // recipe lines / stock movements) drop out of every query without FK trouble. + entity.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return true; + } + public async Task AdjustAsync( string cafeId, string ingredientId, diff --git a/src/Meezi.API/Services/ReservationService.cs b/src/Meezi.API/Services/ReservationService.cs index b013acc..1f2f74e 100644 --- a/src/Meezi.API/Services/ReservationService.cs +++ b/src/Meezi.API/Services/ReservationService.cs @@ -24,6 +24,11 @@ public interface IReservationService string reservationId, ReservationStatus status, CancellationToken cancellationToken = default); + + Task DeleteAsync( + string cafeId, + string reservationId, + CancellationToken cancellationToken = default); } public class ReservationService : IReservationService @@ -118,6 +123,25 @@ public class ReservationService : IReservationService return Map(entity); } + public async Task DeleteAsync( + string cafeId, + string reservationId, + CancellationToken cancellationToken = default) + { + var entity = await _db.TableReservations + .FirstOrDefaultAsync(r => r.Id == reservationId && r.CafeId == cafeId, cancellationToken); + if (entity is null) return false; + + // Soft delete: TableReservation has a global DeletedAt query filter. + entity.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + + if (!string.IsNullOrEmpty(entity.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + return true; + } + internal static ReservationDto Map(TableReservation r) => new( r.Id, r.CafeId, diff --git a/src/Meezi.API/Services/ReviewService.cs b/src/Meezi.API/Services/ReviewService.cs index 4a78c2b..bb54b0b 100644 --- a/src/Meezi.API/Services/ReviewService.cs +++ b/src/Meezi.API/Services/ReviewService.cs @@ -62,7 +62,7 @@ public class ReviewService : IReviewService DiscoverFilterParams filters, CancellationToken cancellationToken = default) { - var query = _db.Cafes.Where(c => c.IsVerified && c.DeletedAt == null); + var query = _db.Cafes.Where(c => c.IsVerified && c.DeletedAt == null && c.ShowOnKoja); if (!string.IsNullOrWhiteSpace(filters.City)) query = query.Where(c => c.City != null && c.City.Contains(filters.City)); diff --git a/src/Meezi.Core/Entities/Cafe.cs b/src/Meezi.Core/Entities/Cafe.cs index d578eae..4795cf5 100644 --- a/src/Meezi.Core/Entities/Cafe.cs +++ b/src/Meezi.Core/Entities/Cafe.cs @@ -17,6 +17,9 @@ public class Cafe : BaseEntity public PlanTier PlanTier { get; set; } = PlanTier.Free; public DateTime? PlanExpiresAt { get; set; } public bool IsVerified { get; set; } + /// Owner preference: list this café on Koja (public discovery). Defaults true so a + /// verified café is discoverable out of the box; the owner can opt out from settings. + public bool ShowOnKoja { get; set; } = true; /// When true, merchant API access is blocked until reactivated by platform admin. public bool IsSuspended { get; set; } public string? SnappfoodVendorId { get; set; } diff --git a/src/Meezi.Infrastructure/Data/AppDbContext.cs b/src/Meezi.Infrastructure/Data/AppDbContext.cs index 72584cb..0f2b7cd 100644 --- a/src/Meezi.Infrastructure/Data/AppDbContext.cs +++ b/src/Meezi.Infrastructure/Data/AppDbContext.cs @@ -111,6 +111,9 @@ public class AppDbContext : DbContext e.Property(x => x.DiscoverProfileJson).HasMaxLength(8000); e.Property(x => x.DiscoverBadgesJson).HasMaxLength(2000); e.Property(x => x.DefaultTaxRate).HasPrecision(5, 2); + // Default true at the DB level so existing cafés stay listed on Koja after + // the column is added (EF doesn't read the C# initializer for the SQL default). + e.Property(x => x.ShowOnKoja).HasDefaultValue(true); e.HasQueryFilter(x => x.DeletedAt == null); }); diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260602121746_AddCafeShowOnKoja.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260602121746_AddCafeShowOnKoja.Designer.cs new file mode 100644 index 0000000..a85e141 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260602121746_AddCafeShowOnKoja.Designer.cs @@ -0,0 +1,3310 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260602121746_AddCafeShowOnKoja")] + partial class AddCafeShowOnKoja + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.AuditLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("ActorId") + .HasColumnType("text"); + + b.Property("ActorName") + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ActorRole") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DetailsJson") + .HasColumnType("text"); + + b.Property("EntityId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntityType") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "BranchId"); + + b.HasIndex("CafeId", "Category"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("ScheduledPermanentDeleteAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverBadgesJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("GalleryJson") + .HasColumnType("text"); + + b.Property("InstagramHandle") + .HasColumnType("text"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowOnKoja") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("WebsiteUrl") + .HasColumnType("text"); + + b.Property("WorkingHoursJson") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewId"); + + b.ToTable("CafeReviewPhotos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.ConsumerAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("ConsumerAccounts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DemoRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AdminNotes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("BranchCount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("BusinessName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("website"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("DemoRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeBranchRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId"); + + b.HasIndex("EmployeeId", "BranchId") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("EmployeeBranchRoles"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PrinterPort") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "SortOrder"); + + b.ToTable("KitchenStations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenStationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("KitchenStationId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CancelReason") + .HasColumnType("text"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByEmployeeId") + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PushDevice", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConsumerAccountId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("City"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("PushDevices"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Author") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CategoryFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentFa") + .IsRequired() + .HasColumnType("text"); + + b.Property("CoverImage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExcerptEn") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ExcerptFa") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TagsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasDefaultValue("[]"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TitleFa") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("ViewCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("IsPublished", "PublishedAt"); + + b.ToTable("WebsiteBlogPosts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("PostSlug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("PostSlug", "IsApproved", "CreatedAt"); + + b.ToTable("WebsiteComments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.AuditLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.HasOne("Meezi.Core.Entities.CafeReview", "Review") + .WithMany("Photos") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeBranchRole", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("StaffRoles") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("BranchRoles") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.KitchenStation", "KitchenStation") + .WithMany("Categories") + .HasForeignKey("KitchenStationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("KitchenStation"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.HasOne("Meezi.Core.Entities.WebsiteBlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("PostSlug") + .HasPrincipalKey("Slug") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("StaffRoles"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("BranchRoles"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Navigation("Comments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260602121746_AddCafeShowOnKoja.cs b/src/Meezi.Infrastructure/Data/Migrations/20260602121746_AddCafeShowOnKoja.cs new file mode 100644 index 0000000..daddf9c --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260602121746_AddCafeShowOnKoja.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddCafeShowOnKoja : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShowOnKoja", + table: "Cafes", + type: "boolean", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShowOnKoja", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs index def1271..5649ed9 100644 --- a/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs @@ -360,6 +360,11 @@ namespace Meezi.Infrastructure.Data.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ShowOnKoja") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + b.Property("Slug") .IsRequired() .HasMaxLength(100) diff --git a/tests/Meezi.API.Tests/NoOpInventoryService.cs b/tests/Meezi.API.Tests/NoOpInventoryService.cs index 3794e5a..2ad036e 100644 --- a/tests/Meezi.API.Tests/NoOpInventoryService.cs +++ b/tests/Meezi.API.Tests/NoOpInventoryService.cs @@ -16,6 +16,9 @@ internal sealed class NoOpInventoryService : IInventoryService public Task UpdateAsync(string cafeId, string ingredientId, UpdateIngredientRequest request, CancellationToken ct = default) => Task.FromResult(null); + public Task DeleteAsync(string cafeId, string ingredientId, CancellationToken ct = default) => + Task.FromResult(false); + public Task AdjustAsync(string cafeId, string ingredientId, AdjustStockRequest request, string? userId, CancellationToken ct = default) => Task.FromResult(null); diff --git a/web/dashboard/messages/ar.json b/web/dashboard/messages/ar.json index 9ef52de..807f7a2 100644 --- a/web/dashboard/messages/ar.json +++ b/web/dashboard/messages/ar.json @@ -21,31 +21,11 @@ "errorGeneric": "حدث خطأ. حاول مرة أخرى." }, "errors": { - "generic": "حدث خطأ. حاول مرة أخرى.", - "REQUEST_FAILED": "فشل الطلب. حاول مرة أخرى.", - "VALIDATION_ERROR": "البيانات المدخلة غير صالحة.", - "FORBIDDEN": "ليس لديك إذن للقيام بذلك.", - "OWNER_REQUIRED": "يمكن لمالك المقهى فقط القيام بذلك.", - "MANAGER_REQUIRED": "يتطلب هذا الإجراء صلاحية المدير.", - "PLAN_LIMIT_REACHED": "لقد بلغت حد باقتك. قم بالترقية للمتابعة.", - "PLAN_FEATURE_DISABLED": "هذه الميزة غير متاحة في باقتك الحالية.", - "NOT_FOUND": "غير موجود.", - "ORDER_NOT_FOUND": "الطلب غير موجود.", - "ITEM_NOT_FOUND": "العنصر غير موجود.", - "ITEM_ALREADY_VOIDED": "تم إلغاء هذا العنصر بالفعل.", - "ORDER_ALREADY_CLOSED": "هذا الطلب مغلق بالفعل.", - "TABLE_OCCUPIED": "هذه الطاولة مشغولة حاليًا.", - "TABLE_CLEANING": "هذه الطاولة قيد التنظيف.", - "TABLE_NOT_FOUND": "الطاولة غير موجودة.", - "TABLE_HAS_OPEN_ORDER": "هذه الطاولة لديها طلب مفتوح ولا يمكن حذفها.", - "TABLE_SECTION_HAS_TABLES": "يحتوي هذا القسم على طاولات ولا يمكن حذفه.", - "BRANCH_NOT_FOUND": "الفرع غير موجود.", - "SECTION_NOT_FOUND": "القسم غير موجود.", - "RATE_LIMITED": "طلبات كثيرة جدًا. يرجى الانتظار قليلاً.", - "SMS_FAILED": "تعذّر إرسال الرسالة القصيرة. حاول مرة أخرى.", - "INVALID_OTP": "رمز التحقق غير صالح أو منتهي الصلاحية.", - "TICKET_CLOSED": "هذه التذكرة مغلقة ولا يمكنها استقبال الرسائل.", - "ALREADY_REGISTERED": "يوجد حساب بالفعل لهذا الرقم. يرجى تسجيل الدخول." + "planLimit": "وصلت إلى حد الخطة", + "notFound": "غير موجود", + "unauthorized": "غير مصرح", + "network": "خطأ في الاتصال", + "generic": "حدث خطأ. حاول مرة أخرى." }, "brand": { "name": "ميزي" @@ -400,7 +380,10 @@ "duplicatePhone": "رقم الجوال مسجل مسبقاً.", "generic": "تعذر الحفظ. حاول مرة أخرى." } - } + }, + "deleted": "تم حذف العميل", + "deleteConfirmTitle": "حذف العميل", + "deleteConfirmDesc": "هل أنت متأكد من حذف «{name}»؟" }, "coupons": { "title": "القسائم", @@ -416,7 +399,10 @@ "FixedAmount": "مبلغ ثابت", "FreeItem": "عنصر مجاني" }, - "noCoupons": "لا توجد قسائم" + "noCoupons": "لا توجد قسائم", + "deleted": "تم حذف القسيمة", + "deleteConfirmTitle": "حذف القسيمة", + "deleteConfirmDesc": "هل أنت متأكد من حذف القسيمة «{code}»؟" }, "hr": { "title": "الموارد البشرية", @@ -863,7 +849,10 @@ "purchasesThisMonth": "مشتريات المواد هذا الشهر", "purchaseCount": "{count} عملية شراء", "viewInExpenses": "عرض في المصروفات", - "selectBranchForPurchases": "اختر الفرع من الشريط العلوي لتسجيل مشتريات المستودع." + "selectBranchForPurchases": "اختر الفرع من الشريط العلوي لتسجيل مشتريات المستودع.", + "deleted": "تم حذف المادة", + "deleteConfirmTitle": "حذف المادة", + "deleteConfirmDesc": "هل أنت متأكد من حذف «{name}»؟ لا يمكن التراجع." }, "qr": { "brand": "ميزي", @@ -978,7 +967,10 @@ "Cancelled": "ملغى", "Seated": "جالس", "Completed": "مكتمل" - } + }, + "deleted": "تم حذف الحجز", + "deleteConfirmTitle": "حذف الحجز", + "deleteConfirmDesc": "هل أنت متأكد من حذف حجز «{name}»؟" }, "branchesPage": { "title": "الفروع", @@ -1394,12 +1386,6 @@ } } }, - "errors": { - "planLimit": "وصلت إلى حد الخطة", - "notFound": "غير موجود", - "unauthorized": "غير مصرح", - "network": "خطأ في الاتصال" - }, "discoverPublic": { "brand": "ميزي", "title": "اكتشاف المقاهي", @@ -1546,5 +1532,9 @@ "mid": "میانه", "premium": "پریمیوم" } + }, + "cafePublicProfile": { + "showOnKoja": "العرض على كوجا", + "showOnKojaHint": "إدراج مقهاك في دليل كوجا العام (koja.meezi.ir). مفعّل افتراضيًا." } } diff --git a/web/dashboard/messages/en.json b/web/dashboard/messages/en.json index 6109a57..b890d18 100644 --- a/web/dashboard/messages/en.json +++ b/web/dashboard/messages/en.json @@ -21,31 +21,11 @@ "errorGeneric": "Something went wrong. Please try again." }, "errors": { - "generic": "Something went wrong. Please try again.", - "REQUEST_FAILED": "Request failed. Please try again.", - "VALIDATION_ERROR": "The information entered is invalid.", - "FORBIDDEN": "You don't have permission to do this.", - "OWNER_REQUIRED": "Only the café owner can do this.", - "MANAGER_REQUIRED": "This action requires manager access.", - "PLAN_LIMIT_REACHED": "You've reached your plan limit. Upgrade to continue.", - "PLAN_FEATURE_DISABLED": "This feature isn't available on your current plan.", - "NOT_FOUND": "Not found.", - "ORDER_NOT_FOUND": "Order not found.", - "ITEM_NOT_FOUND": "Item not found.", - "ITEM_ALREADY_VOIDED": "This item is already voided.", - "ORDER_ALREADY_CLOSED": "This order is already closed.", - "TABLE_OCCUPIED": "This table is currently occupied.", - "TABLE_CLEANING": "This table is being cleaned.", - "TABLE_NOT_FOUND": "Table not found.", - "TABLE_HAS_OPEN_ORDER": "This table has an open order and can't be removed.", - "TABLE_SECTION_HAS_TABLES": "This section has tables and can't be removed.", - "BRANCH_NOT_FOUND": "Branch not found.", - "SECTION_NOT_FOUND": "Section not found.", - "RATE_LIMITED": "Too many requests. Please wait a moment.", - "SMS_FAILED": "Could not send the SMS. Please try again.", - "INVALID_OTP": "Invalid or expired verification code.", - "TICKET_CLOSED": "This ticket is closed and can't receive messages.", - "ALREADY_REGISTERED": "An account already exists for this number. Please sign in." + "planLimit": "Plan limit reached. Please upgrade.", + "notFound": "Not found", + "unauthorized": "Unauthorized", + "network": "Network error", + "generic": "Something went wrong. Please try again." }, "brand": { "name": "Meezi" @@ -419,7 +399,10 @@ "duplicatePhone": "This phone number is already registered.", "generic": "Could not save. Please try again." } - } + }, + "deleted": "Customer deleted", + "deleteConfirmTitle": "Delete customer", + "deleteConfirmDesc": "Delete “{name}”?" }, "coupons": { "title": "Coupons", @@ -435,7 +418,10 @@ "FixedAmount": "Fixed amount", "FreeItem": "Free item" }, - "noCoupons": "No coupons yet" + "noCoupons": "No coupons yet", + "deleted": "Coupon deleted", + "deleteConfirmTitle": "Delete coupon", + "deleteConfirmDesc": "Delete coupon “{code}”?" }, "hr": { "title": "Human resources", @@ -932,7 +918,10 @@ "purchasesThisMonth": "Material purchases this month", "purchaseCount": "{count} purchases", "viewInExpenses": "View in expenses", - "selectBranchForPurchases": "Select a branch in the top bar to record warehouse purchases." + "selectBranchForPurchases": "Select a branch in the top bar to record warehouse purchases.", + "deleted": "Material deleted", + "deleteConfirmTitle": "Delete material", + "deleteConfirmDesc": "Delete “{name}”? This can’t be undone." }, "qr": { "brand": "Meezi", @@ -1048,7 +1037,10 @@ "Cancelled": "Cancelled", "Seated": "Seated", "Completed": "Completed" - } + }, + "deleted": "Reservation deleted", + "deleteConfirmTitle": "Delete reservation", + "deleteConfirmDesc": "Delete the reservation for “{name}”?" }, "branchesPage": { "title": "Branches", @@ -1476,12 +1468,6 @@ } } }, - "errors": { - "planLimit": "Plan limit reached. Please upgrade.", - "notFound": "Not found", - "unauthorized": "Unauthorized", - "network": "Network error" - }, "discoverPublic": { "brand": "Meezi", "title": "Discover cafés", @@ -1586,7 +1572,9 @@ "save": "Save", "saved": "Saved", "saveFailed": "Save failed", - "loading": "Loading…" + "loading": "Loading…", + "showOnKoja": "Show on Koja", + "showOnKojaHint": "List your café in the public Koja directory (koja.meezi.ir). On by default." }, "discoverProfile": { "sections": { diff --git a/web/dashboard/messages/fa.json b/web/dashboard/messages/fa.json index c443344..73b72ea 100644 --- a/web/dashboard/messages/fa.json +++ b/web/dashboard/messages/fa.json @@ -21,31 +21,11 @@ "errorGeneric": "خطایی رخ داد. دوباره تلاش کنید." }, "errors": { - "generic": "خطایی رخ داد. دوباره تلاش کنید.", - "REQUEST_FAILED": "درخواست ناموفق بود. دوباره تلاش کنید.", - "VALIDATION_ERROR": "اطلاعات واردشده نامعتبر است.", - "FORBIDDEN": "شما اجازه این کار را ندارید.", - "OWNER_REQUIRED": "فقط مالک کافه می‌تواند این کار را انجام دهد.", - "MANAGER_REQUIRED": "این عملیات نیاز به دسترسی مدیر دارد.", - "PLAN_LIMIT_REACHED": "محدودیت پلن شما پر شده است. برای ادامه پلن را ارتقا دهید.", - "PLAN_FEATURE_DISABLED": "این قابلیت در پلن فعلی شما فعال نیست.", - "NOT_FOUND": "مورد موردنظر یافت نشد.", - "ORDER_NOT_FOUND": "سفارش یافت نشد.", - "ITEM_NOT_FOUND": "آیتم یافت نشد.", - "ITEM_ALREADY_VOIDED": "این آیتم قبلاً ابطال شده است.", - "ORDER_ALREADY_CLOSED": "این سفارش بسته شده است.", - "TABLE_OCCUPIED": "این میز هم‌اکنون مشغول است.", - "TABLE_CLEANING": "این میز در حال نظافت است.", - "TABLE_NOT_FOUND": "میز یافت نشد.", - "TABLE_HAS_OPEN_ORDER": "این میز سفارش باز دارد و قابل حذف نیست.", - "TABLE_SECTION_HAS_TABLES": "این بخش دارای میز است و قابل حذف نیست.", - "BRANCH_NOT_FOUND": "شعبه یافت نشد.", - "SECTION_NOT_FOUND": "بخش یافت نشد.", - "RATE_LIMITED": "تعداد درخواست بیش از حد مجاز است. کمی صبر کنید.", - "SMS_FAILED": "ارسال پیامک ناموفق بود. دوباره تلاش کنید.", - "INVALID_OTP": "کد تأیید نامعتبر یا منقضی شده است.", - "TICKET_CLOSED": "این تیکت بسته شده و امکان ارسال پیام ندارد.", - "ALREADY_REGISTERED": "برای این شماره قبلاً حساب ساخته شده است. وارد شوید." + "planLimit": "به سقف پلن رسیده‌اید. برای ادامه ارتقا دهید", + "notFound": "یافت نشد", + "unauthorized": "دسترسی ندارید", + "network": "خطای ارتباط با سرور", + "generic": "خطایی رخ داد. دوباره تلاش کنید." }, "brand": { "name": "میزی" @@ -419,7 +399,10 @@ "duplicatePhone": "این شماره موبایل قبلاً ثبت شده است.", "generic": "ذخیره انجام نشد. دوباره تلاش کنید." } - } + }, + "deleted": "مشتری حذف شد", + "deleteConfirmTitle": "حذف مشتری", + "deleteConfirmDesc": "آیا از حذف «{name}» مطمئن هستید؟" }, "coupons": { "title": "کوپن‌ها", @@ -435,7 +418,10 @@ "FixedAmount": "مبلغ ثابت", "FreeItem": "آیتم رایگان" }, - "noCoupons": "کوپنی ثبت نشده" + "noCoupons": "کوپنی ثبت نشده", + "deleted": "کوپن حذف شد", + "deleteConfirmTitle": "حذف کوپن", + "deleteConfirmDesc": "آیا از حذف کوپن «{code}» مطمئن هستید؟" }, "hr": { "title": "منابع انسانی", @@ -932,7 +918,10 @@ "purchasesThisMonth": "خرید مواد این ماه", "purchaseCount": "{count} خرید", "viewInExpenses": "مشاهده در هزینه‌ها", - "selectBranchForPurchases": "برای ثبت خرید انبار، ابتدا شعبه را از نوار بالا انتخاب کنید." + "selectBranchForPurchases": "برای ثبت خرید انبار، ابتدا شعبه را از نوار بالا انتخاب کنید.", + "deleted": "ماده حذف شد", + "deleteConfirmTitle": "حذف ماده", + "deleteConfirmDesc": "آیا از حذف «{name}» مطمئن هستید؟ این عمل قابل بازگشت نیست." }, "qr": { "brand": "میزی", @@ -1049,7 +1038,10 @@ "Cancelled": "لغو شده", "Seated": "نشسته", "Completed": "انجام شده" - } + }, + "deleted": "رزرو حذف شد", + "deleteConfirmTitle": "حذف رزرو", + "deleteConfirmDesc": "آیا از حذف رزرو «{name}» مطمئن هستید؟" }, "branchesPage": { "title": "شعب", @@ -1477,12 +1469,6 @@ } } }, - "errors": { - "planLimit": "به سقف پلن رسیده‌اید. برای ادامه ارتقا دهید", - "notFound": "یافت نشد", - "unauthorized": "دسترسی ندارید", - "network": "خطای ارتباط با سرور" - }, "discoverPublic": { "brand": "میزی", "title": "کافه‌یاب", @@ -1587,7 +1573,9 @@ "save": "ذخیره", "saved": "ذخیره شد", "saveFailed": "ذخیره ناموفق بود", - "loading": "در حال بارگذاری…" + "loading": "در حال بارگذاری…", + "showOnKoja": "نمایش در کوجا", + "showOnKojaHint": "کافه شما در فهرست عمومی کوجا (koja.meezi.ir) نمایش داده شود. پیش‌فرض روشن است." }, "discoverProfile": { "sections": { diff --git a/web/dashboard/src/components/coupons/coupons-screen.tsx b/web/dashboard/src/components/coupons/coupons-screen.tsx index b30dccb..34584d8 100644 --- a/web/dashboard/src/components/coupons/coupons-screen.tsx +++ b/web/dashboard/src/components/coupons/coupons-screen.tsx @@ -3,24 +3,29 @@ import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; -import { Plus } from "lucide-react"; -import { apiGet, apiPost } from "@/lib/api/client"; +import { Plus, Trash2 } from "lucide-react"; +import { apiDelete, apiGet, apiPost } from "@/lib/api/client"; import type { Coupon, CouponType } from "@/lib/api/types"; import { useAuthStore } from "@/lib/stores/auth.store"; import { formatNumber } from "@/lib/format"; +import { notify } from "@/lib/notify"; +import { useApiError } from "@/lib/use-api-error"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { LabeledField } from "@/components/ui/labeled-field"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; export function CouponsScreen() { const t = useTranslations("coupons"); const tCommon = useTranslations("common"); + const apiError = useApiError(); const cafeId = useAuthStore((s) => s.user?.cafeId); const queryClient = useQueryClient(); const [showForm, setShowForm] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); const [code, setCode] = useState(""); const [type, setType] = useState("Percentage"); const [value, setValue] = useState("10"); @@ -47,6 +52,16 @@ export function CouponsScreen() { }, }); + const deleteCoupon = useMutation({ + mutationFn: (id: string) => apiDelete(`/api/cafes/${cafeId}/coupons/${id}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["coupons", cafeId] }); + setDeleteTarget(null); + notify.success(t("deleted")); + }, + onError: (err) => notify.error(apiError(err)), + }); + if (!cafeId) return null; return ( @@ -132,11 +147,34 @@ export function CouponsScreen() { {t("usage")}: {formatNumber(c.usedCount)} {c.usageLimit ? ` / ${formatNumber(c.usageLimit)}` : ""}

+
+ +
))} )} + + { + if (!o) setDeleteTarget(null); + }} + title={t("deleteConfirmTitle")} + description={deleteTarget ? t("deleteConfirmDesc", { code: deleteTarget.code }) : undefined} + busy={deleteCoupon.isPending} + onConfirm={() => deleteTarget && deleteCoupon.mutate(deleteTarget.id)} + /> ); } diff --git a/web/dashboard/src/components/crm/crm-screen.tsx b/web/dashboard/src/components/crm/crm-screen.tsx index 86cbe0e..7b50e87 100644 --- a/web/dashboard/src/components/crm/crm-screen.tsx +++ b/web/dashboard/src/components/crm/crm-screen.tsx @@ -1,23 +1,27 @@ "use client"; import { useState } from "react"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; -import { Plus, Pencil, Search } from "lucide-react"; -import { apiGet } from "@/lib/api/client"; +import { Plus, Pencil, Search, Trash2 } from "lucide-react"; +import { apiDelete, apiGet } from "@/lib/api/client"; import type { Customer } from "@/lib/api/types"; import { useAuthStore } from "@/lib/stores/auth.store"; import { formatNumber } from "@/lib/format"; +import { notify } from "@/lib/notify"; +import { useApiError } from "@/lib/use-api-error"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { LabeledField } from "@/components/ui/labeled-field"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { CustomerWizard, type CustomerWizardMode } from "@/components/crm/customer-wizard"; export function CrmScreen() { const t = useTranslations("crm"); const tCommon = useTranslations("common"); + const apiError = useApiError(); const cafeId = useAuthStore((s) => s.user?.cafeId); const queryClient = useQueryClient(); @@ -26,6 +30,7 @@ export function CrmScreen() { const [wizardOpen, setWizardOpen] = useState(false); const [wizardMode, setWizardMode] = useState("create"); const [editingCustomer, setEditingCustomer] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); const { data: customers = [], isLoading } = useQuery({ queryKey: ["customers", cafeId, debouncedSearch], @@ -46,6 +51,16 @@ export function CrmScreen() { queryClient.invalidateQueries({ queryKey: ["customers", cafeId] }); }; + const deleteCustomer = useMutation({ + mutationFn: (id: string) => apiDelete(`/api/cafes/${cafeId}/customers/${id}`), + onSuccess: () => { + refreshCustomers(); + setDeleteTarget(null); + notify.success(t("deleted")); + }, + onError: (err) => notify.error(apiError(err)), + }); + if (!cafeId) return null; return ( @@ -104,21 +119,43 @@ export function CrmScreen() { {t("loyaltyPoints")}: {formatNumber(c.loyaltyPoints)}

- +
+ + +
))} )} + { + if (!o) setDeleteTarget(null); + }} + title={t("deleteConfirmTitle")} + description={deleteTarget ? t("deleteConfirmDesc", { name: deleteTarget.name }) : undefined} + busy={deleteCustomer.isPending} + onConfirm={() => deleteTarget && deleteCustomer.mutate(deleteTarget.id)} + /> + (""); const [website, setWebsite] = useState(""); const [hours, setHours] = useState(emptyHours()); + const [showOnKoja, setShowOnKoja] = useState(true); const [initialized, setInitialized] = useState(false); // Populate local state once we get server data @@ -50,17 +52,20 @@ export function CafePublicProfilePanel({ cafeId }: Props) { setInstagram(profile.instagramHandle ?? ""); setWebsite(profile.websiteUrl ?? ""); setHours(profile.workingHours ?? emptyHours()); + setShowOnKoja(profile.showOnKoja ?? true); setInitialized(true); } // ── Save info/social/hours ──────────────────────────────────────────────── const saveMutation = useMutation({ - mutationFn: () => + mutationFn: (override?: Partial) => updateCafePublicProfile(cafeId, { description, instagramHandle: instagram || null, websiteUrl: website || null, workingHours: hours, + showOnKoja, + ...override, }), onSuccess: (data) => { qc.setQueryData(["cafe-public-profile", cafeId], data); @@ -157,6 +162,23 @@ export function CafePublicProfilePanel({ cafeId }: Props) { {tab === "info" && ( +