Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
954
oudelaa_dashboard/app/(dashboard)/marketplace/page.tsx
Normal file
954
oudelaa_dashboard/app/(dashboard)/marketplace/page.tsx
Normal file
@@ -0,0 +1,954 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { NoPermissionState } from "@/components/auth/no-permission-state";
|
||||
import { useSuperAdminSession } from "@/components/auth/session-context";
|
||||
import { PageHeader } from "@/components/dashboard/page-header";
|
||||
import { PaginationControls } from "@/components/dashboard/pagination-controls";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import { getItems, getPagination } from "@/lib/api/core";
|
||||
import { listPlatformAdmins } from "@/lib/api/admin-users";
|
||||
import {
|
||||
createMarketplaceInstrumentForSuperAdmin,
|
||||
createMarketplaceListingForSuperAdmin,
|
||||
createMarketplaceRepairShopForSuperAdmin,
|
||||
deleteModerationListing,
|
||||
deleteModerationRepairShop,
|
||||
getMarketplaceHome,
|
||||
listModerationListings,
|
||||
listModerationRepairShops,
|
||||
updateMarketplaceShopProfileForSuperAdmin,
|
||||
updateModerationListingStatus,
|
||||
updateModerationRepairShopStatus,
|
||||
} from "@/lib/api/marketplace";
|
||||
import { formatCurrency } from "@/lib/format";
|
||||
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
|
||||
import type {
|
||||
ApiUser,
|
||||
MarketplaceHomeResponse,
|
||||
MarketplaceListing,
|
||||
MarketplaceRepairShop,
|
||||
MarketplaceRepairShopResponse,
|
||||
MarketplaceResponse,
|
||||
UsersResponse,
|
||||
} from "@/types/api";
|
||||
|
||||
type CreateMode = "listing" | "instrument" | "repair_shop" | "shop_profile";
|
||||
|
||||
type ListingFormState = {
|
||||
title: string;
|
||||
description: string;
|
||||
price: string;
|
||||
currency: string;
|
||||
quantity: string;
|
||||
listingCategory: string;
|
||||
condition: string;
|
||||
instrumentType: string;
|
||||
isActive: boolean;
|
||||
imageFiles: File[];
|
||||
};
|
||||
|
||||
type RepairShopFormState = {
|
||||
name: string;
|
||||
description: string;
|
||||
services: string;
|
||||
phone: string;
|
||||
whatsapp: string;
|
||||
location: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
isActive: boolean;
|
||||
imageFiles: File[];
|
||||
};
|
||||
|
||||
type ShopProfileFormState = {
|
||||
shopName: string;
|
||||
shopDescription: string;
|
||||
shopLocation: string;
|
||||
shopLatitude: string;
|
||||
shopLongitude: string;
|
||||
shopImageFiles: File[];
|
||||
};
|
||||
|
||||
const defaultListingForm: ListingFormState = {
|
||||
title: "",
|
||||
description: "",
|
||||
price: "",
|
||||
currency: "SAR",
|
||||
quantity: "1",
|
||||
listingCategory: "other",
|
||||
condition: "used",
|
||||
instrumentType: "",
|
||||
isActive: true,
|
||||
imageFiles: [],
|
||||
};
|
||||
|
||||
const defaultRepairShopForm: RepairShopFormState = {
|
||||
name: "",
|
||||
description: "",
|
||||
services: "",
|
||||
phone: "",
|
||||
whatsapp: "",
|
||||
location: "",
|
||||
latitude: "",
|
||||
longitude: "",
|
||||
isActive: true,
|
||||
imageFiles: [],
|
||||
};
|
||||
|
||||
const defaultShopProfileForm: ShopProfileFormState = {
|
||||
shopName: "",
|
||||
shopDescription: "",
|
||||
shopLocation: "",
|
||||
shopLatitude: "",
|
||||
shopLongitude: "",
|
||||
shopImageFiles: [],
|
||||
};
|
||||
|
||||
function toNumber(value: string) {
|
||||
return Number(value.trim());
|
||||
}
|
||||
|
||||
function toOptionalNumber(value: string) {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? Number(trimmed) : undefined;
|
||||
}
|
||||
|
||||
function parseCommaSeparated(value: string) {
|
||||
return value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getAdminLabel(admin: ApiUser) {
|
||||
return admin.shopName || admin.stageName || admin.name || admin.username || admin.email;
|
||||
}
|
||||
|
||||
function promptReason(actionLabel: string) {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const result = window.prompt(`Optional reason for ${actionLabel}`, "");
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
export default function MarketplacePage() {
|
||||
const { permissions } = useSuperAdminSession();
|
||||
const [query, setQuery] = useState("");
|
||||
const [listingCategory, setListingCategory] = useState("all");
|
||||
const [activeFilter, setActiveFilter] = useState("all");
|
||||
const [listingsPage, setListingsPage] = useState(1);
|
||||
const [shopsPage, setShopsPage] = useState(1);
|
||||
const [home, setHome] = useState<MarketplaceHomeResponse | null>(null);
|
||||
const [listingsResponse, setListingsResponse] = useState<MarketplaceResponse | null>(null);
|
||||
const [shopsResponse, setShopsResponse] = useState<MarketplaceRepairShopResponse | null>(null);
|
||||
const [adminsResponse, setAdminsResponse] = useState<UsersResponse | null>(null);
|
||||
const [ownerAdminId, setOwnerAdminId] = useState("");
|
||||
const [createMode, setCreateMode] = useState<CreateMode>("listing");
|
||||
const [listingForm, setListingForm] = useState<ListingFormState>(defaultListingForm);
|
||||
const [repairShopForm, setRepairShopForm] = useState<RepairShopFormState>(defaultRepairShopForm);
|
||||
const [shopProfileForm, setShopProfileForm] = useState<ShopProfileFormState>(defaultShopProfileForm);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const filtersRef = useRef({ query, listingCategory, activeFilter });
|
||||
|
||||
const canManageMarketplace = hasPermission(
|
||||
permissions,
|
||||
SUPERADMIN_PERMISSIONS.MARKETPLACE_MANAGE,
|
||||
);
|
||||
const canReadUsers = hasPermission(permissions, SUPERADMIN_PERMISSIONS.USERS_READ);
|
||||
|
||||
filtersRef.current = { query, listingCategory, activeFilter };
|
||||
|
||||
const loadMarketplace = useCallback(async () => {
|
||||
if (!canManageMarketplace) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const {
|
||||
query: currentQuery,
|
||||
listingCategory: currentListingCategory,
|
||||
activeFilter: currentActiveFilter,
|
||||
} = filtersRef.current;
|
||||
const isActive =
|
||||
currentActiveFilter === "all" ? undefined : currentActiveFilter === "active" ? true : false;
|
||||
|
||||
const [homeResponse, listings, shops] = await Promise.all([
|
||||
getMarketplaceHome(),
|
||||
listModerationListings({
|
||||
page: listingsPage,
|
||||
limit: 10,
|
||||
q: currentQuery || undefined,
|
||||
listingCategory: currentListingCategory === "all" ? undefined : currentListingCategory,
|
||||
isActive,
|
||||
sortBy: "createdAt",
|
||||
sortOrder: "desc",
|
||||
}),
|
||||
listModerationRepairShops({
|
||||
page: shopsPage,
|
||||
limit: 10,
|
||||
q: currentQuery || undefined,
|
||||
isActive,
|
||||
sortBy: "createdAt",
|
||||
sortOrder: "desc",
|
||||
}),
|
||||
]);
|
||||
setHome(homeResponse);
|
||||
setListingsResponse(listings);
|
||||
setShopsResponse(shops);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "تعذر تحميل بيانات الماركت",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canManageMarketplace, listingsPage, shopsPage, toast]);
|
||||
|
||||
const loadAdmins = useCallback(async () => {
|
||||
if (!canReadUsers) {
|
||||
setAdminsResponse({ items: [], data: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await listPlatformAdmins(1, 100);
|
||||
setAdminsResponse(response);
|
||||
const admins = getItems(response) as ApiUser[];
|
||||
setOwnerAdminId((current) => current || admins[0]?._id || "");
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "تعذر تحميل قائمة الأدمنز",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
}
|
||||
}, [canReadUsers, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadMarketplace();
|
||||
}, [loadMarketplace]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadAdmins();
|
||||
}, [loadAdmins]);
|
||||
|
||||
const admins = getItems(adminsResponse) as ApiUser[];
|
||||
const listings = getItems(listingsResponse) as MarketplaceListing[];
|
||||
const shops = getItems(shopsResponse) as MarketplaceRepairShop[];
|
||||
|
||||
const applyFilters = () => {
|
||||
const shouldReloadDirectly = listingsPage === 1 && shopsPage === 1;
|
||||
setListingsPage(1);
|
||||
setShopsPage(1);
|
||||
if (shouldReloadDirectly) {
|
||||
void loadMarketplace();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!canReadUsers) {
|
||||
toast({
|
||||
title: "Missing permission",
|
||||
description: "Choosing the owner admin requires users.read.",
|
||||
variant: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ownerAdminId) {
|
||||
toast({
|
||||
title: "اختر الأدمن المالك أولًا",
|
||||
description: "السوبر أدمن ينشئ العنصر على حساب أدمن محدد.",
|
||||
variant: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (createMode === "shop_profile") {
|
||||
await updateMarketplaceShopProfileForSuperAdmin(ownerAdminId, {
|
||||
shopName: shopProfileForm.shopName,
|
||||
shopDescription: shopProfileForm.shopDescription,
|
||||
shopLocation: shopProfileForm.shopLocation,
|
||||
shopLatitude: toOptionalNumber(shopProfileForm.shopLatitude),
|
||||
shopLongitude: toOptionalNumber(shopProfileForm.shopLongitude),
|
||||
shopImageFiles: shopProfileForm.shopImageFiles,
|
||||
});
|
||||
setShopProfileForm(defaultShopProfileForm);
|
||||
toast({ title: "تم تحديث ملف المتجر", description: "أصبح الأدمن جاهزًا لامتلاك عناصر ماركت." });
|
||||
} else if (createMode === "repair_shop") {
|
||||
await createMarketplaceRepairShopForSuperAdmin(ownerAdminId, {
|
||||
name: repairShopForm.name,
|
||||
description: repairShopForm.description,
|
||||
services: parseCommaSeparated(repairShopForm.services),
|
||||
phone: repairShopForm.phone,
|
||||
whatsapp: repairShopForm.whatsapp,
|
||||
location: repairShopForm.location,
|
||||
latitude: toOptionalNumber(repairShopForm.latitude),
|
||||
longitude: toOptionalNumber(repairShopForm.longitude),
|
||||
isActive: repairShopForm.isActive,
|
||||
imageFiles: repairShopForm.imageFiles,
|
||||
});
|
||||
setRepairShopForm(defaultRepairShopForm);
|
||||
toast({ title: "تم إنشاء متجر الصيانة", description: "أضيف المتجر بنجاح تحت الأدمن المحدد." });
|
||||
} else {
|
||||
const payload = {
|
||||
title: listingForm.title,
|
||||
description: listingForm.description,
|
||||
price: toNumber(listingForm.price),
|
||||
currency: listingForm.currency,
|
||||
quantity: toNumber(listingForm.quantity),
|
||||
condition: listingForm.condition,
|
||||
instrumentType: listingForm.instrumentType,
|
||||
isActive: listingForm.isActive,
|
||||
imageFiles: listingForm.imageFiles,
|
||||
...(createMode === "instrument"
|
||||
? { listingCategory: "musical_instrument" }
|
||||
: { listingCategory: listingForm.listingCategory }),
|
||||
};
|
||||
|
||||
if (createMode === "instrument") {
|
||||
await createMarketplaceInstrumentForSuperAdmin(ownerAdminId, payload);
|
||||
toast({ title: "تم إنشاء الآلة الموسيقية", description: "أضيفت الآلة تحت الأدمن المحدد." });
|
||||
} else {
|
||||
await createMarketplaceListingForSuperAdmin(ownerAdminId, payload);
|
||||
toast({ title: "تم إنشاء العنصر العام", description: "أضيف العنصر إلى الماركت بنجاح." });
|
||||
}
|
||||
|
||||
setListingForm({
|
||||
...defaultListingForm,
|
||||
listingCategory: createMode === "listing" ? "other" : "musical_instrument",
|
||||
});
|
||||
}
|
||||
|
||||
await loadMarketplace();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "فشل تنفيذ العملية",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!canManageMarketplace) {
|
||||
return (
|
||||
<div className="space-y-5 pb-8">
|
||||
<PageHeader
|
||||
title="Marketplace management"
|
||||
subtitle="Central marketplace moderation for listings, repair shops, and owner-managed creation flows."
|
||||
/>
|
||||
<NoPermissionState description="This page needs the marketplace.manage permission." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5 pb-8">
|
||||
<PageHeader
|
||||
title="إدارة الماركت"
|
||||
subtitle="إشراف مركزي على عناصر البيع والمتاجر وورش الصيانة، مع قدرة مباشرة للسوبر أدمن على الإنشاء باسم أي أدمن."
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={applyFilters}
|
||||
>
|
||||
تحديث البيانات
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>عناصر السوق العامة</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold text-foreground">
|
||||
{home?.summary.activeListings ?? 0}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>الآلات الموسيقية</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold text-foreground">
|
||||
{home?.summary.activeMusicalInstruments ?? 0}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>ورش ومتاجر الصيانة</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold text-foreground">
|
||||
{home?.summary.activeRepairShops ?? 0}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>إنشاء باسم الأدمن</CardTitle>
|
||||
<CardDescription>
|
||||
اختر الأدمن المالك ثم أنشئ متجرًا أو آلة أو عنصرًا عامًا أو حدّث ملف متجره. كل الرفع يتم عبر
|
||||
FormData للصور.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">الأدمن المالك</p>
|
||||
<Select value={ownerAdminId} onValueChange={setOwnerAdminId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="اختر الأدمن" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{admins.map((admin) => (
|
||||
<SelectItem key={admin._id} value={admin._id}>
|
||||
{getAdminLabel(admin)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">نوع العملية</p>
|
||||
<Select
|
||||
value={createMode}
|
||||
onValueChange={(value) => setCreateMode(value as CreateMode)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="اختر نوع العملية" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="listing">عنصر ماركت عام</SelectItem>
|
||||
<SelectItem value="instrument">آلة موسيقية</SelectItem>
|
||||
<SelectItem value="repair_shop">متجر/ورشة صيانة</SelectItem>
|
||||
<SelectItem value="shop_profile">ملف المتجر</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(createMode === "listing" || createMode === "instrument") && (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Input
|
||||
value={listingForm.title}
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({ ...current, title: event.target.value }))
|
||||
}
|
||||
placeholder="العنوان"
|
||||
/>
|
||||
<Input
|
||||
value={listingForm.instrumentType}
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({ ...current, instrumentType: event.target.value }))
|
||||
}
|
||||
placeholder="نوع الآلة أو المنتج"
|
||||
/>
|
||||
<Input
|
||||
value={listingForm.price}
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({ ...current, price: event.target.value }))
|
||||
}
|
||||
placeholder="السعر"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
<Input
|
||||
value={listingForm.quantity}
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({ ...current, quantity: event.target.value }))
|
||||
}
|
||||
placeholder="الكمية"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<Input
|
||||
value={listingForm.currency}
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({ ...current, currency: event.target.value.toUpperCase() }))
|
||||
}
|
||||
placeholder="العملة"
|
||||
/>
|
||||
<Select
|
||||
value={listingForm.condition}
|
||||
onValueChange={(value) =>
|
||||
setListingForm((current) => ({ ...current, condition: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="الحالة" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new">new</SelectItem>
|
||||
<SelectItem value="used">used</SelectItem>
|
||||
<SelectItem value="like_new">like_new</SelectItem>
|
||||
<SelectItem value="refurbished">refurbished</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{createMode === "listing" && (
|
||||
<Select
|
||||
value={listingForm.listingCategory}
|
||||
onValueChange={(value) =>
|
||||
setListingForm((current) => ({ ...current, listingCategory: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="فئة العنصر" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="accessory">accessory</SelectItem>
|
||||
<SelectItem value="audio_gear">audio_gear</SelectItem>
|
||||
<SelectItem value="sheet_music">sheet_music</SelectItem>
|
||||
<SelectItem value="other">other</SelectItem>
|
||||
<SelectItem value="musical_instrument">musical_instrument</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<label className="flex items-center justify-between rounded-lg border border-input bg-background/40 px-3 py-2 text-sm text-foreground">
|
||||
<span>نشط عند الإنشاء</span>
|
||||
<Switch
|
||||
checked={listingForm.isActive}
|
||||
onCheckedChange={(value) =>
|
||||
setListingForm((current) => ({ ...current, isActive: value }))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<div className="md:col-span-2">
|
||||
<Textarea
|
||||
value={listingForm.description}
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({ ...current, description: event.target.value }))
|
||||
}
|
||||
placeholder="الوصف"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<p className="text-sm font-medium text-foreground">صور العنصر</p>
|
||||
<Input
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={(event) =>
|
||||
setListingForm((current) => ({
|
||||
...current,
|
||||
imageFiles: Array.from(event.target.files ?? []),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
الملفات المختارة: {listingForm.imageFiles.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createMode === "repair_shop" && (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Input
|
||||
value={repairShopForm.name}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, name: event.target.value }))
|
||||
}
|
||||
placeholder="اسم المتجر أو الورشة"
|
||||
/>
|
||||
<Input
|
||||
value={repairShopForm.location}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, location: event.target.value }))
|
||||
}
|
||||
placeholder="الموقع"
|
||||
/>
|
||||
<Input
|
||||
value={repairShopForm.phone}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, phone: event.target.value }))
|
||||
}
|
||||
placeholder="الهاتف"
|
||||
/>
|
||||
<Input
|
||||
value={repairShopForm.whatsapp}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, whatsapp: event.target.value }))
|
||||
}
|
||||
placeholder="واتساب"
|
||||
/>
|
||||
<Input
|
||||
value={repairShopForm.latitude}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, latitude: event.target.value }))
|
||||
}
|
||||
placeholder="خط العرض"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
<Input
|
||||
value={repairShopForm.longitude}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, longitude: event.target.value }))
|
||||
}
|
||||
placeholder="خط الطول"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<Input
|
||||
value={repairShopForm.services}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, services: event.target.value }))
|
||||
}
|
||||
placeholder="الخدمات مفصولة بفواصل، مثال: Oud Repair, Violin Setup"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<Textarea
|
||||
value={repairShopForm.description}
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({ ...current, description: event.target.value }))
|
||||
}
|
||||
placeholder="وصف المتجر"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center justify-between rounded-lg border border-input bg-background/40 px-3 py-2 text-sm text-foreground">
|
||||
<span>نشط عند الإنشاء</span>
|
||||
<Switch
|
||||
checked={repairShopForm.isActive}
|
||||
onCheckedChange={(value) =>
|
||||
setRepairShopForm((current) => ({ ...current, isActive: value }))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<p className="text-sm font-medium text-foreground">صور المتجر</p>
|
||||
<Input
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={(event) =>
|
||||
setRepairShopForm((current) => ({
|
||||
...current,
|
||||
imageFiles: Array.from(event.target.files ?? []),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
الملفات المختارة: {repairShopForm.imageFiles.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createMode === "shop_profile" && (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Input
|
||||
value={shopProfileForm.shopName}
|
||||
onChange={(event) =>
|
||||
setShopProfileForm((current) => ({ ...current, shopName: event.target.value }))
|
||||
}
|
||||
placeholder="اسم المتجر"
|
||||
/>
|
||||
<Input
|
||||
value={shopProfileForm.shopLocation}
|
||||
onChange={(event) =>
|
||||
setShopProfileForm((current) => ({ ...current, shopLocation: event.target.value }))
|
||||
}
|
||||
placeholder="موقع المتجر"
|
||||
/>
|
||||
<Input
|
||||
value={shopProfileForm.shopLatitude}
|
||||
onChange={(event) =>
|
||||
setShopProfileForm((current) => ({ ...current, shopLatitude: event.target.value }))
|
||||
}
|
||||
placeholder="خط العرض"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
<Input
|
||||
value={shopProfileForm.shopLongitude}
|
||||
onChange={(event) =>
|
||||
setShopProfileForm((current) => ({ ...current, shopLongitude: event.target.value }))
|
||||
}
|
||||
placeholder="خط الطول"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<Textarea
|
||||
value={shopProfileForm.shopDescription}
|
||||
onChange={(event) =>
|
||||
setShopProfileForm((current) => ({
|
||||
...current,
|
||||
shopDescription: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="وصف المتجر"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<p className="text-sm font-medium text-foreground">صور ملف المتجر</p>
|
||||
<Input
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={(event) =>
|
||||
setShopProfileForm((current) => ({
|
||||
...current,
|
||||
shopImageFiles: Array.from(event.target.files ?? []),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
الملفات المختارة: {shopProfileForm.shopImageFiles.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button onClick={() => void handleCreate()} disabled={submitting}>
|
||||
{submitting ? "جارٍ التنفيذ..." : "تنفيذ العملية"}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
ملاحظة: متجر الصيانة يبقى واحدًا فقط لكل أدمن، بينما العناصر يمكن إضافتها بلا مشكلة بعد تجهيز
|
||||
ملف المتجر.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>فلاتر الإشراف</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-3">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="ابحث بالاسم أو المتجر"
|
||||
/>
|
||||
<Select value={listingCategory} onValueChange={setListingCategory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="الفئة" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">كل الفئات</SelectItem>
|
||||
<SelectItem value="musical_instrument">musical_instrument</SelectItem>
|
||||
<SelectItem value="accessory">accessory</SelectItem>
|
||||
<SelectItem value="audio_gear">audio_gear</SelectItem>
|
||||
<SelectItem value="sheet_music">sheet_music</SelectItem>
|
||||
<SelectItem value="other">other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={activeFilter} onValueChange={setActiveFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="الحالة" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">كل الحالات</SelectItem>
|
||||
<SelectItem value="active">نشط</SelectItem>
|
||||
<SelectItem value="inactive">موقوف</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-12">
|
||||
<Card className="xl:col-span-7">
|
||||
<CardHeader>
|
||||
<CardTitle>عناصر البيع</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!listings.length && !loading ? (
|
||||
<EmptyState
|
||||
title="لا توجد عناصر"
|
||||
description="لم يرجع الخادم أي عناصر بهذه الفلاتر."
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>العنوان</TableHead>
|
||||
<TableHead>الفئة</TableHead>
|
||||
<TableHead>المتجر</TableHead>
|
||||
<TableHead>السعر</TableHead>
|
||||
<TableHead>الحالة</TableHead>
|
||||
<TableHead>إجراءات</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{listings.map((listing) => (
|
||||
<TableRow key={listing._id}>
|
||||
<TableCell>{listing.title}</TableCell>
|
||||
<TableCell>{listing.listingCategory ?? "-"}</TableCell>
|
||||
<TableCell>{listing.storeName ?? "-"}</TableCell>
|
||||
<TableCell>{formatCurrency(listing.price, listing.currency ?? "SAR")}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={listing.isActive ? "success" : "danger"}>
|
||||
{listing.isActive ? "نشط" : "موقوف"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
const reason = promptReason(
|
||||
listing.isActive ? "listing deactivation" : "listing activation",
|
||||
);
|
||||
if (reason === null) return;
|
||||
try {
|
||||
await updateModerationListingStatus(
|
||||
listing._id,
|
||||
!listing.isActive,
|
||||
reason || undefined,
|
||||
);
|
||||
await loadMarketplace();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "فشل تحديث العنصر",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{listing.isActive ? "إيقاف" : "تفعيل"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteModerationListing(listing._id);
|
||||
await loadMarketplace();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "فشل حذف العنصر",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<PaginationControls
|
||||
pagination={getPagination(listingsResponse)}
|
||||
loading={loading}
|
||||
onPageChange={setListingsPage}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="xl:col-span-5">
|
||||
<CardHeader>
|
||||
<CardTitle>المتاجر وورش الصيانة</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!shops.length && !loading ? (
|
||||
<EmptyState
|
||||
title="لا توجد متاجر"
|
||||
description="لا توجد نتائج مطابقة حاليًا."
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{shops.map((shop) => (
|
||||
<div key={shop._id} className="rounded-xl border border-border/70 bg-secondary/20 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{shop.name}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{shop.location ?? "-"}</p>
|
||||
</div>
|
||||
<Badge variant={shop.isActive ? "success" : "danger"}>
|
||||
{shop.isActive ? "نشط" : "موقوف"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted-foreground">{shop.description ?? "-"}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
const reason = promptReason(
|
||||
shop.isActive ? "repair shop deactivation" : "repair shop activation",
|
||||
);
|
||||
if (reason === null) return;
|
||||
try {
|
||||
await updateModerationRepairShopStatus(
|
||||
shop._id,
|
||||
!shop.isActive,
|
||||
reason || undefined,
|
||||
);
|
||||
await loadMarketplace();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "فشل تحديث المتجر",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{shop.isActive ? "إيقاف" : "تفعيل"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteModerationRepairShop(shop._id);
|
||||
await loadMarketplace();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "فشل حذف المتجر",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<PaginationControls
|
||||
pagination={getPagination(shopsResponse)}
|
||||
loading={loading}
|
||||
onPageChange={setShopsPage}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم