From 024a455ab3e35b5c870096d4d3fed13240d10d08 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 1 Jun 2026 18:23:31 +0330 Subject: [PATCH] fix: menu item/category create, demo banner reach, token refresh, blog publish Dashboard & API bug fixes for owner-reported breakage: - MenuController validators (PosValidators): NameEn was required but the dashboard sends null when blank, so every manual menu-item create failed and category create failed 100% (the form never sends nameEn). Now optional. - DemoDataBanner: only showed when a cafe was exactly empty, so showcase-seeded cafes (2-3 cats / 3-5 items) could never trigger the one-click seed. Widened gate to sparse menus (<5 cats && <10 items) and added a clear "nothing to add" message when already populated. - client.ts: added one-time JWT refresh-and-retry on 401 (shared in-flight promise) before bouncing to /login. Expired access tokens silently broke ticket list, add-table, and other reads. - Surface API errors as toasts on menu + table mutations (were swallowed silently, so failures looked like "nothing happens"). - Admin blog editor: saving an edit dropped IsPublished (defaulted false, silently unpublishing the post on every save); now persisted with a toggle. Also hoisted the inner Field component to module scope - it was remounting every input on each keystroke and dropping focus. - Admin integrations: replaced raw radio gateway selector with a styled RadioDot matching the iOS toggles. Co-Authored-By: Claude Opus 4.8 --- src/Meezi.API/Validators/PosValidators.cs | 4 +- web/admin/messages/ar.json | 1 + web/admin/messages/en.json | 1 + web/admin/messages/fa.json | 1 + .../src/components/admin/admin-screens.tsx | 35 ++++- .../admin/admin-website-screens.tsx | 132 ++++++++++++------ .../src/components/demo/demo-data-banner.tsx | 20 ++- .../src/components/menu/menu-admin-screen.tsx | 15 +- .../src/components/tables/tables-screen.tsx | 9 +- web/dashboard/src/lib/api/client.ts | 59 +++++++- 10 files changed, 217 insertions(+), 60 deletions(-) diff --git a/src/Meezi.API/Validators/PosValidators.cs b/src/Meezi.API/Validators/PosValidators.cs index 04948be..6f33853 100644 --- a/src/Meezi.API/Validators/PosValidators.cs +++ b/src/Meezi.API/Validators/PosValidators.cs @@ -12,7 +12,7 @@ public class CreateMenuCategoryRequestValidator : AbstractValidator x.Name).NotEmpty().MaximumLength(200); - RuleFor(x => x.NameEn).NotEmpty().MaximumLength(200); + RuleFor(x => x.NameEn).MaximumLength(200).When(x => !string.IsNullOrWhiteSpace(x.NameEn)); RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0); RuleFor(x => x.DiscountPercent).InclusiveBetween(0, 100); RuleFor(x => x.Icon).MaximumLength(32).When(x => x.Icon is not null); @@ -39,7 +39,7 @@ public class CreateMenuItemRequestValidator : AbstractValidator x.CategoryId).NotEmpty(); RuleFor(x => x.Name).NotEmpty().MaximumLength(200); - RuleFor(x => x.NameEn).NotEmpty().MaximumLength(200); + RuleFor(x => x.NameEn).MaximumLength(200).When(x => !string.IsNullOrWhiteSpace(x.NameEn)); RuleFor(x => x.Price).GreaterThanOrEqualTo(0); RuleFor(x => x.DiscountPercent).InclusiveBetween(0, 100); } diff --git a/web/admin/messages/ar.json b/web/admin/messages/ar.json index 802e109..039313f 100644 --- a/web/admin/messages/ar.json +++ b/web/admin/messages/ar.json @@ -1057,6 +1057,7 @@ "fieldCategoryEn": "الفئة بالإنجليزية", "fieldContentFa": "المحتوى بالفارسية (Markdown)", "fieldContentEn": "المحتوى بالإنجليزية (Markdown)", + "fieldPublished": "منشور", "commentsTitle": "إدارة التعليقات", "noComments": "لا توجد تعليقات", "approved": "موافق عليه", diff --git a/web/admin/messages/en.json b/web/admin/messages/en.json index eea4a46..dfe90ec 100644 --- a/web/admin/messages/en.json +++ b/web/admin/messages/en.json @@ -1058,6 +1058,7 @@ "fieldCategoryEn": "Category (English)", "fieldContentFa": "Content (Persian, Markdown)", "fieldContentEn": "Content (English, Markdown)", + "fieldPublished": "Published", "commentsTitle": "Comment management", "noComments": "No comments found", "approved": "Approved", diff --git a/web/admin/messages/fa.json b/web/admin/messages/fa.json index 0d3b472..fa598cd 100644 --- a/web/admin/messages/fa.json +++ b/web/admin/messages/fa.json @@ -1058,6 +1058,7 @@ "fieldCategoryEn": "دسته‌بندی انگلیسی", "fieldContentFa": "محتوا فارسی (Markdown)", "fieldContentEn": "محتوا انگلیسی (Markdown)", + "fieldPublished": "وضعیت انتشار", "commentsTitle": "مدیریت نظرات", "noComments": "نظری یافت نشد", "approved": "تأیید شده", diff --git a/web/admin/src/components/admin/admin-screens.tsx b/web/admin/src/components/admin/admin-screens.tsx index c25c2ad..61c41a2 100644 --- a/web/admin/src/components/admin/admin-screens.tsx +++ b/web/admin/src/components/admin/admin-screens.tsx @@ -62,6 +62,33 @@ function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: ( ); } +// Styled single-select indicator (replaces raw ). +function RadioDot({ + selected, + onSelect, + disabled, +}: { + selected: boolean; + onSelect: () => void; + disabled?: boolean; +}) { + return ( + + ); +} + export function AdminDashboardScreen() { const t = useTranslations("admin.dashboard"); const { data } = useQuery({ @@ -632,11 +659,9 @@ export function AdminIntegrationsScreen() {
- setActiveGateway(g.id)} + setActiveGateway(g.id)} /> {g.displayNameFa} {activeGateway === g.id ? ( diff --git a/web/admin/src/components/admin/admin-website-screens.tsx b/web/admin/src/components/admin/admin-website-screens.tsx index e022df0..a9101e8 100644 --- a/web/admin/src/components/admin/admin-website-screens.tsx +++ b/web/admin/src/components/admin/admin-website-screens.tsx @@ -16,6 +16,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { notify } from "@/lib/notify"; +import { cn } from "@/lib/utils"; import { Link } from "@/i18n/routing"; import { CheckCircle2, @@ -149,6 +150,74 @@ export function AdminBlogListScreen() { // ── Blog Post Editor ───────────────────────────────────────────────────────── +// iOS-style toggle (mirrors the one in admin-screens.tsx). +function BlogToggle({ + checked, + onChange, +}: { + checked: boolean; + onChange: (v: boolean) => void; +}) { + return ( + + ); +} + +// Module-level so it keeps a stable component identity across renders. +// (Previously defined inside the editor, which remounted every input on each +// keystroke and dropped focus after a single character.) +function BlogField({ + label, + value, + onChange, + multiline, + dir, +}: { + label: string; + value: string; + onChange: (v: string) => void; + multiline?: boolean; + dir: "rtl" | "ltr"; +}) { + return ( +
+ + {multiline ? ( +