feat: delete actions for warehouse/reservations/coupons/customers + Koja listing toggle
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m10s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 52s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 55s
CI/CD / Deploy · all services (push) Successful in 3m29s

Delete (every manageable entity that only had "add" now has delete):
- Ingredients (warehouse): new DELETE /inventory/ingredients/{id} (soft-delete via
  the global DeletedAt filter — no FK trouble with recipes/movements) + NoOp stub +
  trash button in the materials cards.
- Reservations: new DELETE /reservations/{id} (soft-delete) + per-card delete button.
- Coupons & Customers: backend DELETE already existed; wired delete buttons in the UI.
- Shared ConfirmDialog component used by all delete flows (RTL-aware).
- Audit result: tables/branches/taxes/kitchen-stations/expenses/menu/terminals already
  had delete; HR has no "add" so no delete needed; shifts intentionally excluded
  (financial open/close records, not add-style entities).

Koja visibility:
- New Cafe.ShowOnKoja flag, default TRUE (DB default true so existing cafés stay
  listed). Discover query now filters IsVerified && !Deleted && ShowOnKoja.
- public-profile GET/PUT expose showOnKoja; dashboard public-profile panel has an
  on-by-default toggle that persists immediately. Platform IsVerified gate unchanged.
- EF migration AddCafeShowOnKoja (defaultValue: true).

Also: added the missing errors.generic i18n key (fa/en/ar) so useApiError's fallback
resolves instead of rendering the literal "errors.generic". 81 API tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 16:14:40 +03:30
parent 60e2ac1355
commit 15def7ff1c
22 changed files with 3765 additions and 133 deletions
@@ -9,6 +9,7 @@ import {
updateCafePublicProfile,
uploadGalleryPhoto,
type CafeProfileEdit,
type UpdateCafeProfilePayload,
} from "@/lib/api/cafe-public-profile";
import type { WorkingHours } from "@/lib/api/public-discover";
import { resolveMediaUrl } from "@/lib/api/client";
@@ -42,6 +43,7 @@ export function CafePublicProfilePanel({ cafeId }: Props) {
const [instagram, setInstagram] = useState<string>("");
const [website, setWebsite] = useState<string>("");
const [hours, setHours] = useState<WorkingHours>(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<UpdateCafeProfilePayload>) =>
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" && (
<Card className="rounded-xl border border-border/80">
<CardContent className="space-y-4 p-4">
<label className="flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-[#0F6E56]/25 bg-[#E1F5EE]/40 px-3 py-2.5">
<span className="min-w-0">
<span className="block text-sm font-medium">{t("showOnKoja")}</span>
<span className="block text-xs text-muted-foreground">{t("showOnKojaHint")}</span>
</span>
<input
type="checkbox"
checked={showOnKoja}
onChange={(e) => {
const v = e.target.checked;
setShowOnKoja(v);
// Persist immediately (pass the new value to avoid stale state).
saveMutation.mutate({ showOnKoja: v });
}}
className="h-5 w-5 shrink-0 cursor-pointer accent-[#0F6E56]"
/>
</label>
<div className="space-y-1">
<Label>{t("description")}</Label>
<textarea
@@ -167,7 +189,7 @@ export function CafePublicProfilePanel({ cafeId }: Props) {
className="w-full resize-none rounded-lg border border-border/80 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#0F6E56]"
/>
</div>
<SaveButton saving={saveMutation.isPending} saved={saved} onSave={() => saveMutation.mutate()} t={t} />
<SaveButton saving={saveMutation.isPending} saved={saved} onSave={() => saveMutation.mutate(undefined)} t={t} />
</CardContent>
</Card>
)}
@@ -276,7 +298,7 @@ export function CafePublicProfilePanel({ cafeId }: Props) {
);
})}
</div>
<SaveButton saving={saveMutation.isPending} saved={saved} onSave={() => saveMutation.mutate()} t={t} />
<SaveButton saving={saveMutation.isPending} saved={saved} onSave={() => saveMutation.mutate(undefined)} t={t} />
</CardContent>
</Card>
)}
@@ -307,7 +329,7 @@ export function CafePublicProfilePanel({ cafeId }: Props) {
dir="ltr"
/>
</div>
<SaveButton saving={saveMutation.isPending} saved={saved} onSave={() => saveMutation.mutate()} t={t} />
<SaveButton saving={saveMutation.isPending} saved={saved} onSave={() => saveMutation.mutate(undefined)} t={t} />
</CardContent>
</Card>
)}