131ecdbbe6
Complete merchant dashboard upgrade:
Next.js 16 compatibility:
- Fix params/searchParams typed as Promise<{}> throughout App Router
- Replace middleware.ts with proxy.ts (Next.js 16 convention)
- Remove unused @ts-expect-error directives caught by stricter TS
- Cast dynamic next-intl t() keys to fix TranslateArgs type errors
Offline POS:
- IndexedDB queue (meezi_pos_offline) for orders created while offline
- Zustand sync store tracking queueCount, isSyncing, isOnline
- useOfflineSync hook: auto-syncs on reconnect/visibility-change
- SyncStatusIndicator chip in topbar (amber=offline, blue=syncing)
- submitOrderToApi falls back to local order on network failure
- Local orders skip payment flow; sync on reconnect
PWA (installable):
- @ducanh2912/next-pwa with Workbox runtime caching rules
- Web App Manifest (manifest.ts) — RTL/Farsi, theme #0F6E56
- PWA icons: 192px, 512px, maskable 512px
- next.config.ts replaces next.config.mjs
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
572 lines
23 KiB
TypeScript
572 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useLocale, useTranslations } from "next-intl";
|
|
import { useIsRtl } from "@/lib/use-is-rtl";
|
|
import { Box, Pencil, Video } from "lucide-react";
|
|
import { Menu3dUpload } from "@/components/media/menu-3d-upload";
|
|
import { MenuAi3dGenerate } from "@/components/media/menu-ai-3d-generate";
|
|
import { CategoryVisual } from "@/components/menu/category-visual";
|
|
import { CategoryMediaFields } from "@/components/menu/category-media-fields";
|
|
import type { CategoryIconSelection } from "@/components/menu/category-preset-picker";
|
|
import { DEFAULT_CATEGORY_ICON_STYLE } from "@/lib/category-icon-presets";
|
|
import { apiGet, apiPatch, apiPost } from "@/lib/api/client";
|
|
import { useAuthStore } from "@/lib/stores/auth.store";
|
|
import { formatCurrency, formatNumber } from "@/lib/format";
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
import { MediaPairUpload } from "@/components/media/media-pair-upload";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { LabeledField } from "@/components/ui/labeled-field";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { cn } from "@/lib/utils";
|
|
import { MenuItemLabels } from "@/components/menu/menu-item-labels";
|
|
import { MenuItemMedia } from "@/components/menu/menu-item-media";
|
|
import { buildCategoryNameMap, inferMenuItemKind } from "@/lib/menu-item-image";
|
|
|
|
interface MenuCategory {
|
|
id: string;
|
|
name: string;
|
|
nameEn?: string;
|
|
nameAr?: string;
|
|
sortOrder: number;
|
|
discountPercent: number;
|
|
icon?: string;
|
|
iconPresetId?: string;
|
|
iconStyle?: string;
|
|
imageUrl?: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
interface MenuItem {
|
|
id: string;
|
|
categoryId: string;
|
|
name: string;
|
|
nameEn?: string;
|
|
nameAr?: string;
|
|
price: number;
|
|
discountPercent: number;
|
|
imageUrl?: string;
|
|
videoUrl?: string;
|
|
model3dUrl?: string;
|
|
isAvailable: boolean;
|
|
}
|
|
|
|
function discountedPrice(price: number, percent: number) {
|
|
if (percent <= 0) return price;
|
|
return Math.round(price * (1 - percent / 100));
|
|
}
|
|
|
|
function mediaField(url: string) {
|
|
return url.trim() === "" ? "" : url;
|
|
}
|
|
|
|
export function MenuAdminScreen() {
|
|
const t = useTranslations("menuAdmin");
|
|
const tCommon = useTranslations("common");
|
|
const isRtl = useIsRtl();
|
|
const locale = useLocale();
|
|
const numberLocale = locale === "en" ? "en-US" : "fa-IR";
|
|
const cafeId = useAuthStore((s) => s.user?.cafeId);
|
|
const queryClient = useQueryClient();
|
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
const [editingCategoryId, setEditingCategoryId] = useState<string | null>(null);
|
|
|
|
const [catName, setCatName] = useState("");
|
|
const [catIcon, setCatIcon] = useState("");
|
|
const [catIconPreset, setCatIconPreset] = useState<CategoryIconSelection>({
|
|
iconPresetId: null,
|
|
iconStyle: DEFAULT_CATEGORY_ICON_STYLE,
|
|
});
|
|
const [catImageUrl, setCatImageUrl] = useState("");
|
|
const [editCatName, setEditCatName] = useState("");
|
|
const [editCatIcon, setEditCatIcon] = useState("");
|
|
const [editCatIconPreset, setEditCatIconPreset] = useState<CategoryIconSelection>({
|
|
iconPresetId: null,
|
|
iconStyle: DEFAULT_CATEGORY_ICON_STYLE,
|
|
});
|
|
const [editCatImageUrl, setEditCatImageUrl] = useState("");
|
|
const [itemName, setItemName] = useState("");
|
|
const [itemNameEn, setItemNameEn] = useState("");
|
|
const [itemPrice, setItemPrice] = useState("");
|
|
const [itemDiscount, setItemDiscount] = useState("0");
|
|
const [itemCategoryId, setItemCategoryId] = useState("");
|
|
const [itemImageUrl, setItemImageUrl] = useState("");
|
|
const [itemVideoUrl, setItemVideoUrl] = useState("");
|
|
const [itemModel3dUrl, setItemModel3dUrl] = useState("");
|
|
|
|
const [editName, setEditName] = useState("");
|
|
const [editNameEn, setEditNameEn] = useState("");
|
|
const [editPrice, setEditPrice] = useState("");
|
|
const [editDiscount, setEditDiscount] = useState("0");
|
|
const [editImageUrl, setEditImageUrl] = useState("");
|
|
const [editVideoUrl, setEditVideoUrl] = useState("");
|
|
const [editModel3dUrl, setEditModel3dUrl] = useState("");
|
|
|
|
const { data: categories = [] } = useQuery({
|
|
queryKey: ["menu-categories", cafeId],
|
|
queryFn: () => apiGet<MenuCategory[]>(`/api/cafes/${cafeId}/menu/categories`),
|
|
enabled: !!cafeId,
|
|
});
|
|
|
|
const { data: items = [], isLoading } = useQuery({
|
|
queryKey: ["menu-items-all", cafeId],
|
|
queryFn: () => apiGet<MenuItem[]>(`/api/cafes/${cafeId}/menu/items`),
|
|
enabled: !!cafeId,
|
|
});
|
|
|
|
const categoryNameById = useMemo(
|
|
() => buildCategoryNameMap(categories),
|
|
[categories]
|
|
);
|
|
|
|
const invalidateMenu = () => {
|
|
queryClient.invalidateQueries({ queryKey: ["menu-items-all", cafeId] });
|
|
queryClient.invalidateQueries({ queryKey: ["menu-categories", cafeId] });
|
|
};
|
|
|
|
const addCategory = useMutation({
|
|
mutationFn: () =>
|
|
apiPost(`/api/cafes/${cafeId}/menu/categories`, {
|
|
name: catName,
|
|
sortOrder: categories.length + 1,
|
|
discountPercent: 0,
|
|
icon: catIcon.trim() || null,
|
|
iconPresetId: catIconPreset.iconPresetId,
|
|
iconStyle: catIconPreset.iconPresetId ? catIconPreset.iconStyle : null,
|
|
imageUrl: catImageUrl.trim() || null,
|
|
}),
|
|
onSuccess: () => {
|
|
setCatName("");
|
|
setCatIcon("");
|
|
setCatIconPreset({ iconPresetId: null, iconStyle: DEFAULT_CATEGORY_ICON_STYLE });
|
|
setCatImageUrl("");
|
|
invalidateMenu();
|
|
},
|
|
});
|
|
|
|
const updateCategory = useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiPatch(`/api/cafes/${cafeId}/menu/categories/${id}`, {
|
|
name: editCatName,
|
|
icon: mediaField(editCatIcon),
|
|
iconPresetId: editCatIconPreset.iconPresetId ?? "",
|
|
iconStyle: editCatIconPreset.iconPresetId ? editCatIconPreset.iconStyle : "",
|
|
imageUrl: mediaField(editCatImageUrl),
|
|
}),
|
|
onSuccess: () => {
|
|
setEditingCategoryId(null);
|
|
invalidateMenu();
|
|
},
|
|
});
|
|
|
|
const addItem = useMutation({
|
|
mutationFn: () =>
|
|
apiPost(`/api/cafes/${cafeId}/menu/items`, {
|
|
categoryId: itemCategoryId,
|
|
name: itemName,
|
|
nameEn: itemNameEn.trim(),
|
|
price: parseFloat(itemPrice),
|
|
discountPercent: parseFloat(itemDiscount) || 0,
|
|
imageUrl: itemImageUrl || null,
|
|
videoUrl: itemVideoUrl || null,
|
|
model3dUrl: itemModel3dUrl || null,
|
|
}),
|
|
onSuccess: () => {
|
|
setItemName("");
|
|
setItemNameEn("");
|
|
setItemPrice("");
|
|
setItemDiscount("0");
|
|
setItemImageUrl("");
|
|
setItemVideoUrl("");
|
|
setItemModel3dUrl("");
|
|
invalidateMenu();
|
|
},
|
|
});
|
|
|
|
const updateItem = useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiPatch(`/api/cafes/${cafeId}/menu/items/${id}`, {
|
|
name: editName,
|
|
nameEn: editNameEn.trim(),
|
|
price: parseFloat(editPrice),
|
|
discountPercent: parseFloat(editDiscount) || 0,
|
|
imageUrl: mediaField(editImageUrl),
|
|
videoUrl: mediaField(editVideoUrl),
|
|
model3dUrl: mediaField(editModel3dUrl),
|
|
}),
|
|
onSuccess: () => {
|
|
setEditingId(null);
|
|
invalidateMenu();
|
|
},
|
|
});
|
|
|
|
const toggleItem = useMutation({
|
|
mutationFn: ({ id, isAvailable }: { id: string; isAvailable: boolean }) =>
|
|
apiPatch(`/api/cafes/${cafeId}/menu/items/${id}/availability`, { isAvailable }),
|
|
onSuccess: invalidateMenu,
|
|
});
|
|
|
|
const startCategoryEdit = (cat: MenuCategory) => {
|
|
setEditingCategoryId(cat.id);
|
|
setEditCatName(cat.name);
|
|
setEditCatIcon(cat.icon ?? "");
|
|
setEditCatIconPreset({
|
|
iconPresetId: cat.iconPresetId ?? null,
|
|
iconStyle: (cat.iconStyle as CategoryIconSelection["iconStyle"]) ?? DEFAULT_CATEGORY_ICON_STYLE,
|
|
});
|
|
setEditCatImageUrl(cat.imageUrl ?? "");
|
|
};
|
|
|
|
const startEdit = (item: MenuItem) => {
|
|
setEditingId(item.id);
|
|
setEditName(item.name);
|
|
setEditNameEn(item.nameEn ?? "");
|
|
setEditPrice(String(item.price));
|
|
setEditDiscount(String(item.discountPercent));
|
|
setEditImageUrl(item.imageUrl ?? "");
|
|
setEditVideoUrl(item.videoUrl ?? "");
|
|
setEditModel3dUrl(item.model3dUrl ?? "");
|
|
};
|
|
|
|
if (!cafeId) return null;
|
|
|
|
return (
|
|
<div className="space-y-6" dir={isRtl ? "rtl" : "ltr"}>
|
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
|
<Card className="rounded-xl border border-border/80 bg-card shadow-sm">
|
|
<CardHeader className="pb-2">
|
|
<p className="text-[11px] font-medium uppercase tracking-[0.06em] text-muted-foreground">
|
|
{t("categories")}
|
|
</p>
|
|
<CardTitle className="text-base">{t("addCategory")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex flex-wrap items-end gap-2">
|
|
<LabeledField label={t("name")} htmlFor="cat-name" className="min-w-[12rem] flex-1">
|
|
<Input id="cat-name" value={catName} onChange={(e) => setCatName(e.target.value)} />
|
|
</LabeledField>
|
|
<Button
|
|
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
|
|
disabled={!catName.trim()}
|
|
onClick={() => addCategory.mutate()}
|
|
>
|
|
{t("addCategory")}
|
|
</Button>
|
|
</div>
|
|
<CategoryMediaFields
|
|
cafeId={cafeId}
|
|
icon={catIcon}
|
|
iconPresetId={catIconPreset.iconPresetId}
|
|
iconStyle={catIconPreset.iconStyle}
|
|
imageUrl={catImageUrl}
|
|
onIconChange={setCatIcon}
|
|
onPresetChange={setCatIconPreset}
|
|
onImageChange={(url) => setCatImageUrl(url ?? "")}
|
|
/>
|
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
{categories.map((c) => {
|
|
const isEditingCat = editingCategoryId === c.id;
|
|
return (
|
|
<div
|
|
key={c.id}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-lg border border-border/80 bg-card px-3 py-2 transition-colors hover:border-[#0F6E56]/40",
|
|
isEditingCat && "ring-1 ring-[#0F6E56]/30"
|
|
)}
|
|
>
|
|
<CategoryVisual
|
|
icon={c.icon}
|
|
iconPresetId={c.iconPresetId}
|
|
iconStyle={c.iconStyle}
|
|
imageUrl={c.imageUrl}
|
|
size="sm"
|
|
/>
|
|
{isEditingCat ? (
|
|
<div className="min-w-0 flex-1 space-y-2">
|
|
<Input value={editCatName} onChange={(e) => setEditCatName(e.target.value)} />
|
|
<CategoryMediaFields
|
|
cafeId={cafeId}
|
|
icon={editCatIcon}
|
|
iconPresetId={editCatIconPreset.iconPresetId}
|
|
iconStyle={editCatIconPreset.iconStyle}
|
|
imageUrl={editCatImageUrl}
|
|
onIconChange={setEditCatIcon}
|
|
onPresetChange={setEditCatIconPreset}
|
|
onImageChange={(url) => setEditCatImageUrl(url ?? "")}
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
|
|
disabled={!editCatName.trim()}
|
|
onClick={() => updateCategory.mutate(c.id)}
|
|
>
|
|
{tCommon("save")}
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={() => setEditingCategoryId(null)}>
|
|
{tCommon("cancel")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<span className="min-w-0 flex-1 truncate text-sm font-medium">{c.name}</span>
|
|
<Button size="sm" variant="ghost" onClick={() => startCategoryEdit(c)}>
|
|
<Pencil className="h-3 w-3" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<section>
|
|
<p className="mb-3 text-[11px] font-medium uppercase tracking-[0.06em] text-muted-foreground">
|
|
{t("items")}
|
|
</p>
|
|
|
|
<Card className="mb-4 rounded-xl border border-border/80 bg-card shadow-sm">
|
|
<CardContent className="space-y-3 pt-6">
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
|
<LabeledField label={t("category")} htmlFor="item-category">
|
|
<select
|
|
id="item-category"
|
|
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm"
|
|
value={itemCategoryId}
|
|
onChange={(e) => setItemCategoryId(e.target.value)}
|
|
>
|
|
<option value="">—</option>
|
|
{categories.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</LabeledField>
|
|
<LabeledField label={t("name")} htmlFor="item-name">
|
|
<Input id="item-name" value={itemName} onChange={(e) => setItemName(e.target.value)} />
|
|
</LabeledField>
|
|
<LabeledField label={t("nameEn")} htmlFor="item-name-en">
|
|
<Input
|
|
id="item-name-en"
|
|
value={itemNameEn}
|
|
onChange={(e) => setItemNameEn(e.target.value)}
|
|
dir="ltr"
|
|
className="text-start"
|
|
/>
|
|
</LabeledField>
|
|
<LabeledField label={t("price")} htmlFor="item-price">
|
|
<Input
|
|
id="item-price"
|
|
value={itemPrice}
|
|
onChange={(e) => setItemPrice(e.target.value)}
|
|
inputMode="numeric"
|
|
dir="ltr"
|
|
className="text-end"
|
|
/>
|
|
</LabeledField>
|
|
<LabeledField label={t("discountPercent")} htmlFor="item-discount">
|
|
<Input
|
|
id="item-discount"
|
|
value={itemDiscount}
|
|
onChange={(e) => setItemDiscount(e.target.value)}
|
|
inputMode="numeric"
|
|
dir="ltr"
|
|
className="text-end"
|
|
/>
|
|
</LabeledField>
|
|
<Button
|
|
className="bg-[#0F6E56] hover:bg-[#0c5a46] self-end"
|
|
disabled={!itemName.trim() || !itemNameEn.trim() || !itemCategoryId || !itemPrice}
|
|
onClick={() => addItem.mutate()}
|
|
>
|
|
{t("addItem")}
|
|
</Button>
|
|
</div>
|
|
<LabeledField label={t("media")}>
|
|
<MediaPairUpload
|
|
cafeId={cafeId}
|
|
kind="menu"
|
|
imageUrl={itemImageUrl}
|
|
videoUrl={itemVideoUrl}
|
|
onImageChange={(url) => setItemImageUrl(url ?? "")}
|
|
onVideoChange={(url) => setItemVideoUrl(url ?? "")}
|
|
/>
|
|
<Menu3dUpload
|
|
cafeId={cafeId}
|
|
model3dUrl={itemModel3dUrl || null}
|
|
onChange={(url) => setItemModel3dUrl(url ?? "")}
|
|
/>
|
|
</LabeledField>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{isLoading ? (
|
|
<p className="text-sm text-muted-foreground">{tCommon("loading")}</p>
|
|
) : items.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{t("empty")}</p>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{items.map((item) => {
|
|
const kind = inferMenuItemKind(
|
|
item.categoryId,
|
|
categoryNameById.get(item.categoryId)
|
|
);
|
|
const hasDiscount = item.discountPercent > 0;
|
|
const salePrice = discountedPrice(item.price, item.discountPercent);
|
|
const isEditing = editingId === item.id;
|
|
|
|
return (
|
|
<Card
|
|
key={item.id}
|
|
className={cn(
|
|
"relative overflow-hidden rounded-xl border border-border/80 bg-card shadow-sm transition-colors hover:border-[#0F6E56]/40",
|
|
!item.isAvailable && "opacity-60"
|
|
)}
|
|
>
|
|
{hasDiscount ? (
|
|
<span
|
|
className={cn(
|
|
"absolute top-2 z-10 rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-[#BA7517]",
|
|
isRtl ? "start-2" : "end-2"
|
|
)}
|
|
>
|
|
{formatNumber(item.discountPercent)}٪ {t("discountBadge")}
|
|
</span>
|
|
) : null}
|
|
<div className="relative aspect-[4/3] overflow-hidden bg-muted/50">
|
|
<MenuItemMedia
|
|
imageUrl={item.imageUrl}
|
|
kind={kind}
|
|
size="md"
|
|
className="absolute inset-0"
|
|
/>
|
|
{item.videoUrl ? (
|
|
<span className="absolute bottom-2 start-2 flex items-center gap-1 rounded-md bg-black/60 px-2 py-0.5 text-[10px] text-white">
|
|
<Video className="h-3 w-3" />
|
|
Video
|
|
</span>
|
|
) : null}
|
|
{item.model3dUrl ? (
|
|
<span className="absolute bottom-2 end-2 flex items-center gap-1 rounded-md bg-[#0F6E56]/90 px-2 py-0.5 text-[10px] text-white">
|
|
<Box className="h-3 w-3" />
|
|
3D
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<CardContent className="space-y-2 p-4">
|
|
{isEditing ? (
|
|
<div className="space-y-2">
|
|
<LabeledField label={t("name")} htmlFor={`edit-name-${item.id}`}>
|
|
<Input
|
|
id={`edit-name-${item.id}`}
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
/>
|
|
</LabeledField>
|
|
<LabeledField label={t("nameEn")} htmlFor={`edit-name-en-${item.id}`}>
|
|
<Input
|
|
id={`edit-name-en-${item.id}`}
|
|
value={editNameEn}
|
|
onChange={(e) => setEditNameEn(e.target.value)}
|
|
dir="ltr"
|
|
className="text-start"
|
|
/>
|
|
</LabeledField>
|
|
<LabeledField label={t("price")} htmlFor={`edit-price-${item.id}`}>
|
|
<Input
|
|
id={`edit-price-${item.id}`}
|
|
value={editPrice}
|
|
onChange={(e) => setEditPrice(e.target.value)}
|
|
dir="ltr"
|
|
className="text-end"
|
|
/>
|
|
</LabeledField>
|
|
<LabeledField label={t("discountPercent")} htmlFor={`edit-discount-${item.id}`}>
|
|
<Input
|
|
id={`edit-discount-${item.id}`}
|
|
value={editDiscount}
|
|
onChange={(e) => setEditDiscount(e.target.value)}
|
|
dir="ltr"
|
|
className="text-end"
|
|
/>
|
|
</LabeledField>
|
|
<MediaPairUpload
|
|
cafeId={cafeId}
|
|
kind="menu"
|
|
imageUrl={editImageUrl}
|
|
videoUrl={editVideoUrl}
|
|
onImageChange={(url) => setEditImageUrl(url ?? "")}
|
|
onVideoChange={(url) => setEditVideoUrl(url ?? "")}
|
|
/>
|
|
<Menu3dUpload
|
|
cafeId={cafeId}
|
|
model3dUrl={editModel3dUrl || null}
|
|
onChange={(url) => setEditModel3dUrl(url ?? "")}
|
|
/>
|
|
<MenuAi3dGenerate
|
|
cafeId={cafeId}
|
|
itemId={item.id}
|
|
imageUrl={editImageUrl || item.imageUrl}
|
|
onGenerated={(url) => setEditModel3dUrl(url)}
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" onClick={() => updateItem.mutate(item.id)}>
|
|
{tCommon("save")}
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={() => setEditingId(null)}>
|
|
{tCommon("cancel")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<MenuItemLabels item={item} primaryClassName="text-sm" />
|
|
<div className="flex items-baseline gap-2">
|
|
{hasDiscount ? (
|
|
<>
|
|
<span className="text-xs text-muted-foreground line-through">
|
|
{formatCurrency(item.price)}
|
|
</span>
|
|
<span className="text-sm font-medium text-[#0F6E56]">
|
|
{formatCurrency(salePrice)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-sm font-medium text-[#0F6E56]">
|
|
{formatCurrency(item.price)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2 pt-1">
|
|
<Button size="sm" variant="outline" onClick={() => startEdit(item)}>
|
|
<Pencil className="me-1 h-3 w-3" />
|
|
{t("editItem")}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => toggleItem.mutate({ id: item.id, isAvailable: !item.isAvailable })}
|
|
>
|
|
{item.isAvailable ? t("available") : t("unavailable")}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|