feat: demo data seeder — one-click setup for new cafés
CI/CD / CI · API (dotnet build + test) (push) Successful in 47s
CI/CD / CI · Admin API (dotnet build) (push) Has been cancelled
CI/CD / CI · Dashboard (tsc) (push) Has been cancelled
CI/CD / CI · Admin Web (tsc) (push) Has been cancelled
CI/CD / CI · Website (tsc) (push) Has been cancelled
CI/CD / CI · Koja (tsc) (push) Has been cancelled
CI/CD / Deploy · all services (push) Has been cancelled

Adds POST /api/cafes/{cafeId}/demo/seed (owner-only) that seeds:
- 9% default VAT tax
- 7 menu categories + 59+ items via DemoMenuSeeder
- 15 inventory ingredients (coffee shop staples)
- 10 tables across 3 floors on the first active branch

Frontend DemoDataBanner appears on menu, tables, and inventory
pages when the café is completely empty, so owners can populate
demo data in one click instead of entering everything manually.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-01 00:27:34 +03:30
parent ae5896d440
commit 260429afba
7 changed files with 342 additions and 5 deletions
@@ -0,0 +1,95 @@
"use client";
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Sparkles, Loader2 } from "lucide-react";
import { apiPost } from "@/lib/api/client";
import { useAuthStore } from "@/lib/stores/auth.store";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface DemoSeedResult {
categoriesAdded: number;
itemsAdded: number;
tablesAdded: number;
ingredientsAdded: number;
taxCreated: boolean;
}
interface Props {
/** Which queries to invalidate after seeding. */
invalidateKeys: string[][];
className?: string;
}
export function DemoDataBanner({ invalidateKeys, className }: Props) {
const cafeId = useAuthStore((s) => s.user?.cafeId);
const role = useAuthStore((s) => s.user?.role);
const qc = useQueryClient();
const [done, setDone] = useState(false);
const [summary, setSummary] = useState<DemoSeedResult | null>(null);
const seed = useMutation({
mutationFn: () =>
apiPost<DemoSeedResult>(`/api/cafes/${cafeId}/demo/seed`, {}),
onSuccess: (result) => {
setSummary(result);
setDone(true);
for (const key of invalidateKeys) {
qc.invalidateQueries({ queryKey: key });
}
},
});
if (!cafeId || (role !== "Owner" && role !== "Manager")) return null;
if (done && summary) {
return (
<div
className={cn(
"flex items-center gap-3 rounded-xl border border-[#0F6E56]/30 bg-[#E1F5EE] px-4 py-3 text-sm text-[#0F6E56]",
className
)}
>
<Sparkles className="size-4 shrink-0" />
<span>
دادههای نمونه اضافه شد {summary.categoriesAdded} دسته،{" "}
{summary.itemsAdded} آیتم، {summary.tablesAdded} میز،{" "}
{summary.ingredientsAdded} ماده اولیه
{summary.taxCreated ? "، مالیات ۹٪" : ""}.
</span>
</div>
);
}
return (
<div
className={cn(
"flex flex-col gap-3 rounded-xl border border-dashed border-[#0F6E56]/40 bg-[#E1F5EE]/40 px-5 py-4 sm:flex-row sm:items-center sm:justify-between",
className
)}
>
<div className="flex items-center gap-3">
<Sparkles className="size-5 shrink-0 text-[#0F6E56]" />
<div>
<p className="text-sm font-semibold text-[#0F6E56]">شروع سریع با دادههای نمونه</p>
<p className="text-xs text-muted-foreground mt-0.5">
۷ دسته، ۵۹+ آیتم منو، ۱۰ میز، ۱۵ ماده اولیه و مالیات ۹٪ بهصورت خودکار اضافه میشود.
</p>
</div>
</div>
<Button
size="sm"
className="shrink-0 bg-[#0F6E56] hover:bg-[#0c5a46]"
disabled={seed.isPending}
onClick={() => seed.mutate()}
>
{seed.isPending ? (
<Loader2 className="me-1.5 size-4 animate-spin" />
) : (
<Sparkles className="me-1.5 size-4" />
)}
افزودن دادههای نمونه
</Button>
</div>
);
}
@@ -5,6 +5,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslations, useLocale } from "next-intl";
import { Link } from "@/i18n/routing";
import { Pencil } from "lucide-react";
import { DemoDataBanner } from "@/components/demo/demo-data-banner";
import { apiGet, apiPatch, apiPost, apiPut } from "@/lib/api/client";
import { InventoryUnitField } from "@/components/inventory/inventory-unit-field";
import { useAuthStore } from "@/lib/stores/auth.store";
@@ -366,7 +367,17 @@ export function InventoryScreen() {
{isLoading ? (
<p className="text-sm text-muted-foreground">{tCommon("loading")}</p>
) : ingredients.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("empty")}</p>
<div className="space-y-3">
<DemoDataBanner
invalidateKeys={[
["inventory", cafeId!],
["menu-categories", cafeId!],
["menu-items-all", cafeId!],
["tables-board", cafeId!],
]}
/>
<p className="text-sm text-muted-foreground">{t("empty")}</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{ingredients.map((ing) => {
@@ -5,6 +5,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useLocale, useTranslations } from "next-intl";
import { useIsRtl } from "@/lib/use-is-rtl";
import { Box, Pencil, Plus, Search, Video, X } from "lucide-react";
import { DemoDataBanner } from "@/components/demo/demo-data-banner";
import { Menu3dUpload } from "@/components/media/menu-3d-upload";
import { MenuAi3dGenerate } from "@/components/media/menu-ai-3d-generate";
import { CategoryVisual } from "@/components/menu/category-visual";
@@ -449,7 +450,19 @@ export function MenuAdminScreen() {
)
) : (
/* ── Catalog tab ─────────────────────────────────────────────────── */
<div className="flex min-h-0 gap-4">
<div className="flex min-h-0 flex-col gap-4">
{categories.length === 0 && items.length === 0 && (
<DemoDataBanner
invalidateKeys={[
["menu-categories", cafeId],
["menu-items-all", cafeId],
["menu-items", cafeId],
["tables-board", cafeId],
["inventory", cafeId],
]}
/>
)}
<div className="flex min-h-0 gap-4">
{/* ── Category Sidebar (desktop) ─────────────────────────────── */}
<aside className="hidden w-52 shrink-0 lg:block">
@@ -752,6 +765,7 @@ export function MenuAdminScreen() {
</div>
)}
</div>
</div>
</div>
)}
@@ -5,6 +5,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import * as signalR from "@microsoft/signalr";
import { Plus, QrCode, Pencil, Video, Trash2, Copy, ExternalLink } from "lucide-react";
import { DemoDataBanner } from "@/components/demo/demo-data-banner";
import { notify } from "@/lib/notify";
import { MediaPairUpload } from "@/components/media/media-pair-upload";
import { PageHeader } from "@/components/layout/page-header";
@@ -346,9 +347,19 @@ export function TablesScreen() {
{isLoading ? (
<p className="text-sm text-muted-foreground">{tCommon("loading")}</p>
) : tables.length === 0 ? (
<p className="text-sm text-muted-foreground">
{branchId ? t("emptyBranch") : t("empty")}
</p>
<div className="space-y-3">
<DemoDataBanner
invalidateKeys={[
["tables-board", cafeId],
["menu-categories", cafeId],
["menu-items-all", cafeId],
["inventory", cafeId],
]}
/>
<p className="text-sm text-muted-foreground">
{branchId ? t("emptyBranch") : t("empty")}
</p>
</div>
) : (
<>
{actionMessage ? (