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
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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user