fix: sidebar accordion + koja slug + support ticket LINQ crash
CI/CD / CI · API (dotnet build + test) (push) Successful in 5m50s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m3s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Has been cancelled

Sidebar:
- All groups start collapsed on first load (v4 storage key resets old state)
- Opening one group closes all others (accordion)
- Navigating to a section opens only that section's group

Koja slug:
- SlugHelper: Persian->Latin transliteration, slug validation
- Registration accepts optional custom slug; auto-derives from cafe name
- Slug can be updated from dashboard Settings -> Profile
- Settings PATCH validates uniqueness (SLUG_TAKEN) and format (INVALID_SLUG)
- koja.meezi.ir/{slug} now redirects to /fa/cafe/{slug} (short URL support)

Bug fix:
- SupportTicketService: cafeId/status filters applied before Select() projection
  to fix EF "could not be translated" crash on the support tickets page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-31 22:28:25 +03:30
parent 38e3f6a5a2
commit cd1af30bbc
17 changed files with 401 additions and 58 deletions
@@ -50,8 +50,10 @@ public class SupportTicketService : ISupportTicketService
string cafeId,
CancellationToken cancellationToken = default)
{
return await QueryTickets()
.Where(t => t.CafeId == cafeId)
// NOTE: The Where MUST be applied on the EF entity set BEFORE the Select projection.
// Applying Where() after Select() onto a DTO record causes an EF translation error
// because EF can't translate "new SupportTicketDto(...).CafeId == x".
return await QueryTickets(cafeId)
.OrderByDescending(t => t.UpdatedAt)
.ToListAsync(cancellationToken);
}
@@ -119,11 +121,10 @@ public class SupportTicketService : ISupportTicketService
SupportTicketStatus? status,
CancellationToken cancellationToken = default)
{
var q = QueryTickets();
if (status.HasValue)
q = q.Where(t => t.Status == status.Value);
return await q.OrderByDescending(t => t.UpdatedAt).ToListAsync(cancellationToken);
// status filter is applied on the entity before projection — safe for EF translation.
return await QueryTickets(cafeId: null, status: status)
.OrderByDescending(t => t.UpdatedAt)
.ToListAsync(cancellationToken);
}
public async Task<SupportTicketDetailDto?> GetAdminAsync(
@@ -185,22 +186,36 @@ public class SupportTicketService : ISupportTicketService
return await GetAdminAsync(ticketId, cancellationToken);
}
private IQueryable<SupportTicketDto> QueryTickets() =>
_db.SupportTickets
.AsNoTracking()
.Select(t => new SupportTicketDto(
t.Id,
t.CafeId,
t.Cafe != null ? t.Cafe.Name : "",
t.Subject,
t.Status,
t.Priority,
t.CreatedByEmployeeId,
t.CreatedByEmployee != null ? t.CreatedByEmployee.Name : null,
t.AssignedAdminId,
t.CreatedAt,
t.UpdatedAt,
t.Messages.Count));
/// <summary>
/// Builds an EF-translatable query for support ticket list rows.
/// Filters are applied on the entity BEFORE the Select projection to avoid EF translation errors.
/// </summary>
private IQueryable<SupportTicketDto> QueryTickets(
string? cafeId = null,
SupportTicketStatus? status = null)
{
var q = _db.SupportTickets.AsNoTracking().AsQueryable();
// Apply entity-level filters BEFORE Select so EF can translate them.
if (cafeId is not null)
q = q.Where(t => t.CafeId == cafeId);
if (status.HasValue)
q = q.Where(t => t.Status == status.Value);
return q.Select(t => new SupportTicketDto(
t.Id,
t.CafeId,
t.Cafe != null ? t.Cafe.Name : "",
t.Subject,
t.Status,
t.Priority,
t.CreatedByEmployeeId,
t.CreatedByEmployee != null ? t.CreatedByEmployee.Name : null,
t.AssignedAdminId,
t.CreatedAt,
t.UpdatedAt,
t.Messages.Count));
}
private static SupportTicketDto MapTicket(SupportTicket t) =>
new(