@infuro/cms-core 1.0.60 → 1.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin.js CHANGED
@@ -10,7 +10,7 @@ import { Slot } from '@radix-ui/react-slot';
10
10
  import { cva } from 'class-variance-authority';
11
11
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
12
12
  import * as LucideIcons from 'lucide-react';
13
- import { ChevronDown, User, Settings, LogOut, LayoutDashboard, Store, FolderTree, Layers, Building2, CalendarDays, ShoppingBag, Package, Receipt, BadgePercent, ShoppingCart, CreditCard, Users, Inbox, File, LinkIcon, ClipboardList, MessageSquare, Image as Image$1, Shield, FileText, Puzzle, X, Plus, MoreVertical, Upload, Download, Edit, Copy, RefreshCw, Trash2, Mail, Check, Save, Search, AlertCircle, MapPin, BarChart3, TrendingUp, UserCheck, Activity, Globe, Eye, MousePointer, ArrowLeftRight, RotateCcw, KeyRound, Loader2, CalendarClock, CheckSquare, Square, XSquare, Rss, ArrowLeft, Pencil, FolderPlus, LayoutGrid, List, Move, Archive, Edit2, ExternalLink, Power, Star, ChevronUp, Zap, Filter, CheckCircle2, ImageIcon, GripVertical, ChevronRight, ArrowRight, ArrowUpRight, IndianRupee, XCircle, Clock, Play, MessageCircle, Bot, FileUp, Tag, Box, SlidersHorizontal, Calculator, Link2 as Link2$1, Unlink, Gift, MoreHorizontal, ShieldCheck, EyeOff, Smartphone, QrCode, Car, Send, Settings2, DollarSign, Hash, Circle, ChevronLeft, Bell, Share2, HardDrive, Folder, Film } from 'lucide-react';
13
+ import { ChevronDown, User, Settings, LogOut, LayoutDashboard, Store, FolderTree, Layers, Building2, CalendarDays, ShoppingBag, Package, Receipt, BadgePercent, ShoppingCart, CreditCard, Users, Inbox, File, LinkIcon, ClipboardList, MessageSquare, Image as Image$1, Shield, FileText, Puzzle, X, ShieldAlert, Plus, MoreVertical, Upload, Download, Edit, Copy, RefreshCw, Trash2, Mail, Check, Save, Search, AlertCircle, MapPin, BarChart3, TrendingUp, UserCheck, Activity, Globe, Eye, MousePointer, ArrowLeftRight, RotateCcw, KeyRound, Loader2, CalendarClock, CheckSquare, Square, XSquare, Rss, ArrowLeft, Pencil, FolderPlus, LayoutGrid, List, Move, Archive, Edit2, ExternalLink, Power, Star, ChevronUp, Zap, Filter, CheckCircle2, ImageIcon, GripVertical, ChevronRight, ArrowRight, ArrowUpRight, IndianRupee, XCircle, Clock, Play, MessageCircle, Bot, FileUp, Tag, Box, SlidersHorizontal, Calculator, Link2 as Link2$1, Unlink, Gift, MoreHorizontal, ShieldCheck, EyeOff, Smartphone, QrCode, Car, Send, Settings2, DollarSign, Hash, Circle, ChevronLeft, Bell, Share2, HardDrive, Folder, Film } from 'lucide-react';
14
14
  import Image from 'next/image';
15
15
  import Link2 from 'next/link';
16
16
  import { usePathname, useSearchParams, useRouter } from 'next/navigation';
@@ -861,7 +861,7 @@ var init_admin_config_context = __esm({
861
861
  var CMS_VERSION;
862
862
  var init_cms_version = __esm({
863
863
  "src/lib/cms-version.ts"() {
864
- CMS_VERSION = "1.0.60" ;
864
+ CMS_VERSION = "1.0.61" ;
865
865
  }
866
866
  });
867
867
  function useCatalogCategories(enabled = true) {
@@ -2192,6 +2192,11 @@ function FileUpload({ onUploadSuccess }) {
2192
2192
  if (!selectedFile) return;
2193
2193
  setUploading(true);
2194
2194
  setProgress(10);
2195
+ console.info("[upload] file-upload start", {
2196
+ name: selectedFile.name,
2197
+ type: selectedFile.type,
2198
+ size: selectedFile.size
2199
+ });
2195
2200
  const formData = new FormData();
2196
2201
  formData.append("file", selectedFile);
2197
2202
  try {
@@ -2200,20 +2205,47 @@ function FileUpload({ onUploadSuccess }) {
2200
2205
  body: formData
2201
2206
  });
2202
2207
  setProgress(60);
2203
- const data = await response.json();
2208
+ const data = await response.json().catch((parseErr) => {
2209
+ console.error("[upload] file-upload JSON parse failed", {
2210
+ status: response.status,
2211
+ parseErr
2212
+ });
2213
+ return {};
2214
+ });
2215
+ console.info("[upload] file-upload response", {
2216
+ status: response.status,
2217
+ ok: response.ok,
2218
+ url: data.url ?? data.filePath ?? null,
2219
+ error: data.error ?? null,
2220
+ details: data.details ?? null
2221
+ });
2204
2222
  if (response.ok) {
2205
- setFileUrl(data.url);
2223
+ const url = data.url ?? data.filePath;
2224
+ setFileUrl(url);
2206
2225
  setProgress(100);
2207
- onUploadSuccess(data.url);
2226
+ console.info("[upload] file-upload ok", {
2227
+ url,
2228
+ name: selectedFile.name
2229
+ });
2230
+ onUploadSuccess(url);
2208
2231
  } else {
2209
2232
  console.error("[upload] API error", {
2210
2233
  status: response.status,
2211
2234
  error: data.error,
2212
- details: data.details
2235
+ details: data.details,
2236
+ name: selectedFile.name,
2237
+ type: selectedFile.type,
2238
+ size: selectedFile.size
2213
2239
  });
2214
2240
  }
2215
2241
  } catch (error) {
2216
- console.error("Upload error:", error);
2242
+ console.error("[upload] file-upload failed", {
2243
+ name: selectedFile.name,
2244
+ type: selectedFile.type,
2245
+ size: selectedFile.size,
2246
+ error: error instanceof Error ? error.message : String(error),
2247
+ stack: error instanceof Error ? error.stack : void 0
2248
+ });
2217
2249
  }
2218
2250
  setUploading(false);
2219
2251
  }, "handleUpload");
@@ -2345,28 +2377,64 @@ function ImageOrUrlField({ label, value, onChange, placeholder = "https://\u2026
2345
2377
  return;
2346
2378
  }
2347
2379
  setIsUploading(true);
2380
+ console.info("[upload] ImageOrUrlField start", {
2381
+ name: file.name,
2382
+ type: file.type,
2383
+ size: file.size,
2384
+ isVideo: isVideo2,
2385
+ maxSizeMb
2386
+ });
2348
2387
  try {
2349
2388
  const formData = new FormData();
2350
2389
  formData.append("file", file);
2390
+ console.info("[upload] ImageOrUrlField POST /api/upload", {
2391
+ name: file.name,
2392
+ type: file.type,
2393
+ size: file.size
2394
+ });
2351
2395
  const response = await fetch("/api/upload", {
2352
2396
  method: "POST",
2353
2397
  body: formData
2354
2398
  });
2355
- const data = await response.json().catch(() => ({}));
2399
+ const data = await response.json().catch((parseErr) => {
2400
+ console.error("[upload] ImageOrUrlField JSON parse failed", {
2401
+ status: response.status,
2402
+ parseErr
2403
+ });
2404
+ return {};
2405
+ });
2406
+ console.info("[upload] ImageOrUrlField response", {
2407
+ status: response.status,
2408
+ ok: response.ok,
2409
+ filePath: data.filePath ?? null,
2410
+ error: data.error ?? null,
2411
+ details: data.details ?? null
2412
+ });
2356
2413
  if (!response.ok) {
2357
2414
  console.error("[upload] API error", {
2358
2415
  status: response.status,
2359
2416
  error: data.error,
2360
- details: data.details
2417
+ details: data.details,
2418
+ fileName: file.name,
2419
+ fileType: file.type,
2420
+ fileSize: file.size
2361
2421
  });
2362
2422
  throw new Error(data.details || data.error || "Upload failed");
2363
2423
  }
2364
2424
  console.info("[upload] API ok", {
2365
- filePath: data.filePath
2425
+ filePath: data.filePath,
2426
+ name: file.name,
2427
+ type: file.type
2366
2428
  });
2367
2429
  onChange(data.filePath ?? "");
2368
2430
  } catch (err) {
2369
- console.error("[upload] client failed", err);
2431
+ console.error("[upload] client failed", {
2432
+ name: file.name,
2433
+ type: file.type,
2434
+ size: file.size,
2435
+ error: err instanceof Error ? err.message : String(err),
2436
+ stack: err instanceof Error ? err.stack : void 0
2437
+ });
2370
2438
  setError(err instanceof Error ? err.message : "Upload failed");
2371
2439
  } finally {
2372
2440
  setIsUploading(false);
@@ -5065,6 +5133,9 @@ function AdminCRUD({ title, apiEndpoint, columns, addEditPageUrl, customViewPage
5065
5133
  });
5066
5134
  if (cancelled) return;
5067
5135
  if (!res.ok) {
5136
+ if (res.status === 403) {
5137
+ throw new Error("You don't have access to this section");
5138
+ }
5068
5139
  throw new Error(`HTTP error! status: ${res.status}`);
5069
5140
  }
5070
5141
  const result = await res.json();
@@ -5481,15 +5552,26 @@ function AdminCRUD({ title, apiEndpoint, columns, addEditPageUrl, customViewPage
5481
5552
  }, "Loading...")));
5482
5553
  }
5483
5554
  if (error) {
5555
+ const isForbidden = error.toLowerCase().includes("don't have access") || error.toLowerCase().includes("forbidden") || error.includes("403");
5484
5556
  return /* @__PURE__ */ React.createElement("div", {
5485
5557
  className: embedded ? "p-4" : "rounded-lg bg-white p-6 shadow-md"
5486
5558
  }, /* @__PURE__ */ React.createElement("div", {
5487
- className: "text-center py-8"
5488
- }, /* @__PURE__ */ React.createElement("p", {
5559
+ className: "text-center py-10"
5560
+ }, isForbidden ? /* @__PURE__ */ React.createElement("div", {
5561
+ className: "max-w-md mx-auto"
5562
+ }, /* @__PURE__ */ React.createElement("div", {
5563
+ className: "mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 mb-3"
5564
+ }, /* @__PURE__ */ React.createElement(ShieldAlert, {
5565
+ className: "h-6 w-6 text-red-600"
5566
+ })), /* @__PURE__ */ React.createElement("h3", {
5567
+ className: "text-base font-semibold text-gray-900 mb-1"
5568
+ }, "You don't have access"), /* @__PURE__ */ React.createElement("p", {
5569
+ className: "text-sm text-gray-500"
5570
+ }, "You don't have permission to view or manage this tab.")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", {
5489
5571
  className: "text-red-600 mb-4"
5490
5572
  }, "Error loading data: ", error), /* @__PURE__ */ React.createElement(Button, {
5491
5573
  onClick: /* @__PURE__ */ __name(() => window.location.reload(), "onClick")
5492
- }, "Retry")));
5574
+ }, "Retry"))));
5493
5575
  }
5494
5576
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", {
5495
5577
  className: shellCls
@@ -6853,26 +6935,63 @@ function ImageUpload({ value, onChange, onRemove, label, required = false, class
6853
6935
  return;
6854
6936
  }
6855
6937
  setIsUploading(true);
6938
+ console.info("[upload] client image-upload start", {
6939
+ name: file.name,
6940
+ type: file.type,
6941
+ size: file.size,
6942
+ maxSizeMb: maxSize
6943
+ });
6856
6944
  try {
6857
6945
  const formData = new FormData();
6858
6946
  formData.append("file", file);
6947
+ console.info("[upload] client POST /api/upload", {
6948
+ name: file.name,
6949
+ type: file.type,
6950
+ size: file.size
6951
+ });
6859
6952
  const response = await fetch("/api/upload", {
6860
6953
  method: "POST",
6861
6954
  body: formData
6862
6955
  });
6863
- const data = await response.json();
6956
+ const data = await response.json().catch((parseErr) => {
6957
+ console.error("[upload] client failed to parse JSON response", {
6958
+ status: response.status,
6959
+ parseErr
6960
+ });
6961
+ return {};
6962
+ });
6963
+ console.info("[upload] client response", {
6964
+ status: response.status,
6965
+ ok: response.ok,
6966
+ filePath: data.filePath ?? null,
6967
+ error: data.error ?? null,
6968
+ details: data.details ?? null
6969
+ });
6864
6970
  if (!response.ok) {
6865
6971
  console.error("[upload] API error", {
6866
6972
  status: response.status,
6867
6973
  error: data.error,
6868
- details: data.details
6974
+ details: data.details,
6975
+ fileName: file.name,
6976
+ fileType: file.type,
6977
+ fileSize: file.size
6869
6978
  });
6870
6979
  throw new Error(data.details || data.error || "Upload failed");
6871
6980
  }
6981
+ console.info("[upload] client image-upload ok", {
6982
+ filePath: data.filePath,
6983
+ name: file.name
6984
+ });
6872
6985
  onChange(data.filePath);
6873
6986
  setUploadProgress(100);
6874
6987
  } catch (error2) {
6875
- console.error("Upload error:", error2);
6988
+ console.error("[upload] client image-upload failed", {
6989
+ name: file.name,
6990
+ type: file.type,
6991
+ size: file.size,
6992
+ error: error2 instanceof Error ? error2.message : String(error2),
6993
+ stack: error2 instanceof Error ? error2.stack : void 0
6994
+ });
6876
6995
  setError(error2 instanceof Error ? error2.message : "Failed to upload image");
6877
6996
  } finally {
6878
6997
  setIsUploading(false);
@@ -33251,6 +33370,252 @@ var init_EventProductsSection = __esm({
33251
33370
  __name(EventProductsSection, "EventProductsSection");
33252
33371
  }
33253
33372
  });
33373
+ function AttachComboModal({ open, onOpenChange, excludeComboIds = [], onAttach }) {
33374
+ const [attachingId, setAttachingId] = useState(null);
33375
+ const [error, setError] = useState(null);
33376
+ const comboColumns = useMemo(() => STORE_CRUD_CONFIGS.combos.columns.filter((c) => c.field !== "eventId"), []);
33377
+ const comboFilterColumns = useMemo(() => STORE_CRUD_CONFIGS.combos.columns.filter((c) => c.field !== "eventId"), []);
33378
+ const comboFilters = useMemo(() => STORE_CRUD_CONFIGS.combos.filters?.filter((f) => f.param !== "eventId"), []);
33379
+ const extraListParams = useMemo(() => {
33380
+ const params = {};
33381
+ if (excludeComboIds.length > 0) {
33382
+ params.excludeIds = excludeComboIds.join(",");
33383
+ }
33384
+ return params;
33385
+ }, [
33386
+ excludeComboIds
33387
+ ]);
33388
+ const handleAttach = /* @__PURE__ */ __name(async (comboId) => {
33389
+ setError(null);
33390
+ setAttachingId(comboId);
33391
+ try {
33392
+ await onAttach(comboId);
33393
+ onOpenChange(false);
33394
+ } catch (err) {
33395
+ setError(err instanceof Error ? err.message : "Failed to attach combo");
33396
+ } finally {
33397
+ setAttachingId(null);
33398
+ }
33399
+ }, "handleAttach");
33400
+ return /* @__PURE__ */ React.createElement(Dialog, {
33401
+ open,
33402
+ onOpenChange
33403
+ }, /* @__PURE__ */ React.createElement(DialogContent, {
33404
+ className: "max-w-4xl max-h-[90vh] overflow-y-auto"
33405
+ }, /* @__PURE__ */ React.createElement(DialogHeader, null, /* @__PURE__ */ React.createElement(DialogTitle, null, "Attach Event Combo"), /* @__PURE__ */ React.createElement(DialogDescription, null, "Choose a combo to link to this event.")), error ? /* @__PURE__ */ React.createElement("p", {
33406
+ className: "text-sm text-red-600 bg-red-50 rounded-md px-3 py-2"
33407
+ }, error) : null, /* @__PURE__ */ React.createElement(AdminCRUD, {
33408
+ key: `attach-combos-${excludeComboIds.join(",")}-${open}`,
33409
+ title: "Combos",
33410
+ apiEndpoint: "/api/combos",
33411
+ columns: comboColumns,
33412
+ filterColumns: comboFilterColumns,
33413
+ addEditPageUrl: "",
33414
+ defaultSortField: STORE_CRUD_CONFIGS.combos.defaultSortField,
33415
+ defaultSortOrder: STORE_CRUD_CONFIGS.combos.defaultSortOrder,
33416
+ filters: comboFilters,
33417
+ extraListParams,
33418
+ embedded: true,
33419
+ disableRowClick: true,
33420
+ emptyState: {
33421
+ title: "No combos available",
33422
+ description: excludeComboIds.length ? "All existing combos are already attached to this event, or none exist yet." : "Create a combo first, then attach it here."
33423
+ },
33424
+ customRowActions: /* @__PURE__ */ __name((item) => {
33425
+ const id = Number(item.id);
33426
+ const busy = attachingId === id;
33427
+ return /* @__PURE__ */ React.createElement(Button, {
33428
+ type: "button",
33429
+ size: "sm",
33430
+ variant: "outline",
33431
+ disabled: !Number.isFinite(id) || busy,
33432
+ className: "h-7 text-xs",
33433
+ onClick: /* @__PURE__ */ __name(() => void handleAttach(id), "onClick")
33434
+ }, /* @__PURE__ */ React.createElement(Link2$1, {
33435
+ className: "h-3.5 w-3.5 mr-1"
33436
+ }), busy ? "Attaching\u2026" : "Attach");
33437
+ }, "customRowActions")
33438
+ })));
33439
+ }
33440
+ var init_AttachComboModal = __esm({
33441
+ "src/components/Admin/AttachComboModal.tsx"() {
33442
+ "use client";
33443
+ init_dialog();
33444
+ init_button();
33445
+ init_CRUD();
33446
+ init_store_crud_configs();
33447
+ __name(AttachComboModal, "AttachComboModal");
33448
+ }
33449
+ });
33450
+ function EventCombosSection({ eventId, returnUrl, readOnly = false }) {
33451
+ const [rows, setRows] = useState([]);
33452
+ const [loading, setLoading] = useState(true);
33453
+ const [attachModalOpen, setAttachModalOpen] = useState(false);
33454
+ const [listRefreshKey, setListRefreshKey] = useState(0);
33455
+ const [detachingId, setDetachingId] = useState(null);
33456
+ const comboColumns = useMemo(() => STORE_CRUD_CONFIGS.combos.columns.filter((c) => c.field !== "eventId"), []);
33457
+ const comboFilterColumns = useMemo(() => STORE_CRUD_CONFIGS.combos.columns.filter((c) => c.field !== "eventId"), []);
33458
+ const comboFilters = useMemo(() => STORE_CRUD_CONFIGS.combos.filters?.filter((f) => f.param !== "eventId"), []);
33459
+ const loadRows = useCallback(async () => {
33460
+ setLoading(true);
33461
+ try {
33462
+ const res = await fetch(`/api/combos?eventId=${encodeURIComponent(eventId)}&limit=200`);
33463
+ if (!res.ok) throw new Error("Failed to load");
33464
+ const data = await res.json();
33465
+ const list = Array.isArray(data.data) ? data.data : [];
33466
+ setRows(list);
33467
+ } catch {
33468
+ setRows([]);
33469
+ } finally {
33470
+ setLoading(false);
33471
+ }
33472
+ }, [
33473
+ eventId
33474
+ ]);
33475
+ useEffect(() => {
33476
+ void loadRows();
33477
+ }, [
33478
+ loadRows
33479
+ ]);
33480
+ const attachedComboIds = useMemo(() => rows.map((r) => Number(r.id)).filter((id) => Number.isFinite(id) && id > 0), [
33481
+ rows
33482
+ ]);
33483
+ const attachToEvent = /* @__PURE__ */ __name(async (comboId) => {
33484
+ const res = await fetch(`/api/combos/${comboId}`, {
33485
+ method: "PUT",
33486
+ headers: {
33487
+ "Content-Type": "application/json"
33488
+ },
33489
+ body: JSON.stringify({
33490
+ eventId: Number(eventId)
33491
+ })
33492
+ });
33493
+ if (!res.ok) {
33494
+ const data = await res.json().catch(() => ({}));
33495
+ throw new Error(data.error ?? "Failed to attach combo");
33496
+ }
33497
+ await loadRows();
33498
+ setListRefreshKey((k) => k + 1);
33499
+ }, "attachToEvent");
33500
+ const detachFromEvent = /* @__PURE__ */ __name(async (comboId) => {
33501
+ if (!window.confirm("Remove this combo from the event?")) return;
33502
+ setDetachingId(comboId);
33503
+ try {
33504
+ const res = await fetch(`/api/combos/${comboId}`, {
33505
+ method: "PUT",
33506
+ headers: {
33507
+ "Content-Type": "application/json"
33508
+ },
33509
+ body: JSON.stringify({
33510
+ eventId: null
33511
+ })
33512
+ });
33513
+ if (res.ok) {
33514
+ await loadRows();
33515
+ setListRefreshKey((k) => k + 1);
33516
+ }
33517
+ } finally {
33518
+ setDetachingId(null);
33519
+ }
33520
+ }, "detachFromEvent");
33521
+ const comboCreateHref = !readOnly ? `/admin/combos/create?eventId=${encodeURIComponent(eventId)}${returnUrl ? `&from=${encodeURIComponent(returnUrl)}` : ""}` : null;
33522
+ if (loading) {
33523
+ return /* @__PURE__ */ React.createElement("section", {
33524
+ className: "border border-gray-200 rounded-lg p-8 text-center text-sm text-gray-500"
33525
+ }, "Loading combos\u2026");
33526
+ }
33527
+ if (readOnly && rows.length === 0) {
33528
+ return /* @__PURE__ */ React.createElement("section", {
33529
+ className: "border border-gray-200 rounded-lg p-6"
33530
+ }, /* @__PURE__ */ React.createElement("h2", {
33531
+ className: "text-sm font-semibold text-gray-900"
33532
+ }, "Event Combos"), /* @__PURE__ */ React.createElement("p", {
33533
+ className: "mt-2 text-sm text-gray-600"
33534
+ }, "No combos are linked to this event yet."));
33535
+ }
33536
+ return /* @__PURE__ */ React.createElement("div", {
33537
+ className: "space-y-4"
33538
+ }, /* @__PURE__ */ React.createElement("section", {
33539
+ className: "border border-gray-200 rounded-lg overflow-hidden bg-white"
33540
+ }, /* @__PURE__ */ React.createElement("div", {
33541
+ className: "flex flex-wrap items-center justify-between gap-3 px-4 py-3 border-b border-gray-200"
33542
+ }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", {
33543
+ className: "text-sm font-semibold text-gray-900"
33544
+ }, "Event Combos"), /* @__PURE__ */ React.createElement("p", {
33545
+ className: "text-xs text-gray-500"
33546
+ }, "Event Combos linked to this event.")), !readOnly ? /* @__PURE__ */ React.createElement("div", {
33547
+ className: "flex flex-wrap items-center gap-2"
33548
+ }, /* @__PURE__ */ React.createElement(Button, {
33549
+ type: "button",
33550
+ variant: "outline",
33551
+ size: "sm",
33552
+ className: "h-8 text-xs",
33553
+ onClick: /* @__PURE__ */ __name(() => setAttachModalOpen(true), "onClick")
33554
+ }, /* @__PURE__ */ React.createElement(Link2$1, {
33555
+ className: "h-3.5 w-3.5 mr-1"
33556
+ }), "Attach event combo"), comboCreateHref ? /* @__PURE__ */ React.createElement(Link2, {
33557
+ href: comboCreateHref,
33558
+ className: "inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 h-8"
33559
+ }, /* @__PURE__ */ React.createElement(Plus, {
33560
+ className: "h-3.5 w-3.5"
33561
+ }), "New event combo") : null) : null), /* @__PURE__ */ React.createElement("div", {
33562
+ className: "[&>div]:shadow-none [&>div]:rounded-none"
33563
+ }, rows.length === 0 ? /* @__PURE__ */ React.createElement("div", {
33564
+ className: "px-4 py-8 text-center text-sm text-gray-500"
33565
+ }, "No combos attached to this event yet.") : /* @__PURE__ */ React.createElement(AdminCRUD, {
33566
+ key: `event-combos-${eventId}-${attachedComboIds.join(",")}-${listRefreshKey}`,
33567
+ title: "Event Combos",
33568
+ apiEndpoint: "/api/combos",
33569
+ columns: comboColumns,
33570
+ filterColumns: comboFilterColumns,
33571
+ addEditPageUrl: "/admin/combos",
33572
+ defaultSortField: STORE_CRUD_CONFIGS.combos.defaultSortField,
33573
+ defaultSortOrder: STORE_CRUD_CONFIGS.combos.defaultSortOrder,
33574
+ filters: comboFilters,
33575
+ extraListParams: {
33576
+ eventId: String(eventId)
33577
+ },
33578
+ embedded: true,
33579
+ disableRowClick: readOnly,
33580
+ emptyState: {
33581
+ title: "No combos attached",
33582
+ description: "Attach or create a combo for this event.",
33583
+ action: !readOnly && comboCreateHref ? /* @__PURE__ */ React.createElement(Link2, {
33584
+ href: comboCreateHref,
33585
+ className: "inline-flex items-center rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-800"
33586
+ }, "Create combo") : void 0
33587
+ },
33588
+ customRowActions: readOnly ? void 0 : (item) => {
33589
+ const comboId = Number(item.id);
33590
+ const busy = detachingId === comboId;
33591
+ return /* @__PURE__ */ React.createElement(Button, {
33592
+ type: "button",
33593
+ size: "sm",
33594
+ variant: "outline",
33595
+ disabled: !Number.isFinite(comboId) || busy,
33596
+ className: "h-7 text-xs border-red-200 text-red-600 hover:text-red-700",
33597
+ onClick: /* @__PURE__ */ __name(() => void detachFromEvent(comboId), "onClick")
33598
+ }, /* @__PURE__ */ React.createElement(Unlink, {
33599
+ className: "h-3.5 w-3.5 mr-1"
33600
+ }), busy ? "Removing\u2026" : "Detach");
33601
+ }
33602
+ }))), attachModalOpen ? /* @__PURE__ */ React.createElement(AttachComboModal, {
33603
+ open: attachModalOpen,
33604
+ onOpenChange: setAttachModalOpen,
33605
+ excludeComboIds: attachedComboIds,
33606
+ onAttach: attachToEvent
33607
+ }) : null);
33608
+ }
33609
+ var init_EventCombosSection = __esm({
33610
+ "src/admin/pages/EventCombosSection.tsx"() {
33611
+ "use client";
33612
+ init_button();
33613
+ init_CRUD();
33614
+ init_AttachComboModal();
33615
+ init_store_crud_configs();
33616
+ __name(EventCombosSection, "EventCombosSection");
33617
+ }
33618
+ });
33254
33619
 
33255
33620
  // src/lib/social-media-links.ts
33256
33621
  function isValidHttpUrl(value) {
@@ -34744,10 +35109,13 @@ function EventEditPage({ eventId }) {
34744
35109
  onClick: goToNextTab,
34745
35110
  className: "rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
34746
35111
  }, "Next") : /* @__PURE__ */ React.createElement("div", null)), showEventProducts ? /* @__PURE__ */ React.createElement("div", {
34747
- className: "border-t border-gray-200 px-4 py-4 sm:px-6"
35112
+ className: "border-t border-gray-200 px-4 py-4 sm:px-6 space-y-6"
34748
35113
  }, /* @__PURE__ */ React.createElement(EventProductsSection, {
34749
35114
  eventId: eventRecordId,
34750
35115
  returnUrl: eventEditUrl
35116
+ }), /* @__PURE__ */ React.createElement(EventCombosSection, {
35117
+ eventId: eventRecordId,
35118
+ returnUrl: eventEditUrl
34751
35119
  })) : null);
34752
35120
  }
34753
35121
  var ADDITIONAL_VENUE_MAX_CHARS, DEFAULT_EVENT_TIMEZONE, DEFAULT_EVENT_CURRENCY, EVENT_SOCIAL_KEYS, isCreate4, sectionCls4, labelCls6, inputCls6, ENTITY_TYPE_OPTIONS;
@@ -34762,6 +35130,7 @@ var init_EventEditPage = __esm({
34762
35130
  init_media_url();
34763
35131
  init_JoditRichText();
34764
35132
  init_EventProductsSection();
35133
+ init_EventCombosSection();
34765
35134
  init_admin_config_context();
34766
35135
  init_dialog();
34767
35136
  init_button();
@@ -34799,7 +35168,13 @@ function formatDateTimeLocal(value) {
34799
35168
  if (value == null) return "";
34800
35169
  const date = new Date(String(value));
34801
35170
  if (isNaN(date.getTime())) return "";
34802
- return date.toISOString().slice(0, 16);
35171
+ const pad = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "pad");
35172
+ const y = date.getFullYear();
35173
+ const m = pad(date.getMonth() + 1);
35174
+ const d = pad(date.getDate());
35175
+ const hh = pad(date.getHours());
35176
+ const mm = pad(date.getMinutes());
35177
+ return `${y}-${m}-${d}T${hh}:${mm}`;
34803
35178
  }
34804
35179
  async function fetchEventDefaultCurrency(eventId) {
34805
35180
  try {
@@ -34846,13 +35221,14 @@ function ComboEditPage({ comboId }) {
34846
35221
  const create = isCreate5(comboId);
34847
35222
  const duplicateFrom = searchParams.get("duplicateFrom")?.trim();
34848
35223
  const duplicateSourceId = create && duplicateFrom && /^\d+$/.test(duplicateFrom) ? duplicateFrom : null;
35224
+ const initialEventId = create ? searchParams.get("eventId")?.trim() || "" : "";
34849
35225
  const [loading, setLoading] = useState(!create || !!duplicateSourceId);
34850
35226
  const [saving, setSaving] = useState(false);
34851
35227
  const [errors, setErrors] = useState([]);
34852
35228
  const [name, setName] = useState("");
34853
35229
  const [slug, setSlug] = useState("");
34854
35230
  const [desc, setDesc] = useState("");
34855
- const [eventId, setEventId] = useState("");
35231
+ const [eventId, setEventId] = useState(initialEventId);
34856
35232
  const [priceStr, setPriceStr] = useState("");
34857
35233
  const [defaultCurrency, setDefaultCurrency] = useState("INR");
34858
35234
  const [minSelectableItems, setMinSelectableItems] = useState(1);
@@ -35089,15 +35465,9 @@ function ComboEditPage({ comboId }) {
35089
35465
  ]);
35090
35466
  return;
35091
35467
  }
35092
- if (!startsAt) {
35468
+ if (startsAt && endsAt && new Date(endsAt).getTime() < new Date(startsAt).getTime()) {
35093
35469
  setErrors([
35094
- "Starts at is required"
35095
- ]);
35096
- return;
35097
- }
35098
- if (!endsAt) {
35099
- setErrors([
35100
- "Ends at is required"
35470
+ "Ends at cannot be earlier than Starts at"
35101
35471
  ]);
35102
35472
  return;
35103
35473
  }
@@ -35165,8 +35535,8 @@ function ComboEditPage({ comboId }) {
35165
35535
  minSelectableItems,
35166
35536
  maxSelectableItems,
35167
35537
  status,
35168
- startsAt: startsAt || null,
35169
- endsAt: endsAt || null,
35538
+ startsAt: startsAt ? new Date(startsAt).toISOString() : null,
35539
+ endsAt: endsAt ? new Date(endsAt).toISOString() : null,
35170
35540
  comboItems: [
35171
35541
  ...fixedItems.map((i) => ({
35172
35542
  productId: Number(i.productId),
@@ -35390,21 +35760,23 @@ function ComboEditPage({ comboId }) {
35390
35760
  className: "grid grid-cols-2 gap-4"
35391
35761
  }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
35392
35762
  className: "block text-xs font-medium text-gray-600 mb-1"
35393
- }, "Starts at *"), /* @__PURE__ */ React.createElement("input", {
35763
+ }, "Starts at (optional)"), /* @__PURE__ */ React.createElement("input", {
35394
35764
  type: "datetime-local",
35395
35765
  value: startsAt,
35396
35766
  onChange: /* @__PURE__ */ __name((e) => setStartsAt(e.target.value), "onChange"),
35397
- className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm",
35398
- required: true
35399
- })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
35767
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
35768
+ }), /* @__PURE__ */ React.createElement("p", {
35769
+ className: "text-xs text-gray-400 mt-1"
35770
+ }, "Leave empty to be available immediately.")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
35400
35771
  className: "block text-xs font-medium text-gray-600 mb-1"
35401
- }, "Ends at *"), /* @__PURE__ */ React.createElement("input", {
35772
+ }, "Ends at (optional)"), /* @__PURE__ */ React.createElement("input", {
35402
35773
  type: "datetime-local",
35403
35774
  value: endsAt,
35404
35775
  onChange: /* @__PURE__ */ __name((e) => setEndsAt(e.target.value), "onChange"),
35405
- className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm",
35406
- required: true
35407
- })))))),
35776
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
35777
+ }), /* @__PURE__ */ React.createElement("p", {
35778
+ className: "text-xs text-gray-400 mt-1"
35779
+ }, "Leave empty if no expiration date.")))))),
35408
35780
  sidebar: /* @__PURE__ */ React.createElement(React.Fragment, null)
35409
35781
  }));
35410
35782
  }
@@ -35520,6 +35892,130 @@ function VendorEditPage({ vendorId }) {
35520
35892
  const [inviteDialog, setInviteDialog] = useState(null);
35521
35893
  const [sendingInviteEmail, setSendingInviteEmail] = useState(false);
35522
35894
  const [linkCopied, setLinkCopied] = useState(false);
35895
+ const VENDOR_CREATE_DRAFT_KEY = "cms_vendor_create_draft";
35896
+ useEffect(() => {
35897
+ if (!create || typeof window === "undefined") return;
35898
+ try {
35899
+ const saved = sessionStorage.getItem(VENDOR_CREATE_DRAFT_KEY);
35900
+ if (saved) {
35901
+ const draft = JSON.parse(saved);
35902
+ if (draft && typeof draft === "object") {
35903
+ if (draft.name) setName(draft.name);
35904
+ if (draft.legalName) setLegalName(draft.legalName);
35905
+ if (draft.slug) setSlug(draft.slug);
35906
+ if (draft.businessType) setBusinessType(draft.businessType);
35907
+ if (draft.description) setDescription(draft.description);
35908
+ if (draft.website) setWebsite(draft.website);
35909
+ if (draft.logo) setLogo(draft.logo);
35910
+ if (draft.email) setEmail(draft.email);
35911
+ if (draft.storePhoneCode) setStorePhoneCode(draft.storePhoneCode);
35912
+ if (draft.storePhoneNum) setStorePhoneNum(draft.storePhoneNum);
35913
+ if (draft.addressLine1) setAddressLine1(draft.addressLine1);
35914
+ if (draft.addressLine2) setAddressLine2(draft.addressLine2);
35915
+ if (draft.city) setCity(draft.city);
35916
+ if (draft.state) setState(draft.state);
35917
+ if (draft.country) setCountry(draft.country);
35918
+ if (draft.postalCode) setPostalCode(draft.postalCode);
35919
+ if (draft.gstin) setGstin(draft.gstin);
35920
+ if (typeof draft.active === "boolean") setActive(draft.active);
35921
+ if (draft.registrationStatus) setRegistrationStatus(draft.registrationStatus);
35922
+ if (draft.ownerName) setOwnerName(draft.ownerName);
35923
+ if (draft.ownerEmail) setOwnerEmail(draft.ownerEmail);
35924
+ if (draft.ownerPhoneCode) setOwnerPhoneCode(draft.ownerPhoneCode);
35925
+ if (draft.ownerPhoneNum) setOwnerPhoneNum(draft.ownerPhoneNum);
35926
+ if (draft.ownerDesignation) setOwnerDesignation(draft.ownerDesignation);
35927
+ if (draft.ownerAadhaarNo) setOwnerAadhaarNo(draft.ownerAadhaarNo);
35928
+ if (draft.ownerPanNo) setOwnerPanNo(draft.ownerPanNo);
35929
+ if (draft.ownerAddressLine1) setOwnerAddressLine1(draft.ownerAddressLine1);
35930
+ if (draft.ownerAddressLine2) setOwnerAddressLine2(draft.ownerAddressLine2);
35931
+ if (draft.ownerCity) setOwnerCity(draft.ownerCity);
35932
+ if (draft.ownerState) setOwnerState(draft.ownerState);
35933
+ if (draft.ownerPostalCode) setOwnerPostalCode(draft.ownerPostalCode);
35934
+ if (typeof draft.termsAccepted === "boolean") setTermsAccepted(draft.termsAccepted);
35935
+ }
35936
+ }
35937
+ } catch {
35938
+ }
35939
+ }, [
35940
+ create
35941
+ ]);
35942
+ useEffect(() => {
35943
+ if (!create || typeof window === "undefined") return;
35944
+ const hasAnyData = Boolean(name || legalName || gstin || ownerName || ownerEmail || ownerPhoneNum || ownerAadhaarNo || ownerPanNo || ownerAddressLine1);
35945
+ if (!hasAnyData) return;
35946
+ try {
35947
+ const draft = {
35948
+ name,
35949
+ legalName,
35950
+ slug,
35951
+ businessType,
35952
+ description,
35953
+ website,
35954
+ logo,
35955
+ email,
35956
+ storePhoneCode,
35957
+ storePhoneNum,
35958
+ addressLine1,
35959
+ addressLine2,
35960
+ city,
35961
+ state,
35962
+ country,
35963
+ postalCode,
35964
+ gstin,
35965
+ active,
35966
+ registrationStatus,
35967
+ ownerName,
35968
+ ownerEmail,
35969
+ ownerPhoneCode,
35970
+ ownerPhoneNum,
35971
+ ownerDesignation,
35972
+ ownerAadhaarNo,
35973
+ ownerPanNo,
35974
+ ownerAddressLine1,
35975
+ ownerAddressLine2,
35976
+ ownerCity,
35977
+ ownerState,
35978
+ ownerPostalCode,
35979
+ termsAccepted
35980
+ };
35981
+ sessionStorage.setItem(VENDOR_CREATE_DRAFT_KEY, JSON.stringify(draft));
35982
+ } catch {
35983
+ }
35984
+ }, [
35985
+ create,
35986
+ name,
35987
+ legalName,
35988
+ slug,
35989
+ businessType,
35990
+ description,
35991
+ website,
35992
+ logo,
35993
+ email,
35994
+ storePhoneCode,
35995
+ storePhoneNum,
35996
+ addressLine1,
35997
+ addressLine2,
35998
+ city,
35999
+ state,
36000
+ country,
36001
+ postalCode,
36002
+ gstin,
36003
+ active,
36004
+ registrationStatus,
36005
+ ownerName,
36006
+ ownerEmail,
36007
+ ownerPhoneCode,
36008
+ ownerPhoneNum,
36009
+ ownerDesignation,
36010
+ ownerAadhaarNo,
36011
+ ownerPanNo,
36012
+ ownerAddressLine1,
36013
+ ownerAddressLine2,
36014
+ ownerCity,
36015
+ ownerState,
36016
+ ownerPostalCode,
36017
+ termsAccepted
36018
+ ]);
35523
36019
  const vendorPayload = /* @__PURE__ */ __name(() => ({
35524
36020
  name: name.trim(),
35525
36021
  legalName: legalName.trim() || null,
@@ -35763,12 +36259,15 @@ function VendorEditPage({ vendorId }) {
35763
36259
  setSaving(false);
35764
36260
  return;
35765
36261
  }
36262
+ const abortController = new AbortController();
36263
+ const timeoutId = setTimeout(() => abortController.abort(), 15e3);
35766
36264
  try {
35767
36265
  const res = await fetch("/api/admin/vendors/onboard", {
35768
36266
  method: "POST",
35769
36267
  headers: {
35770
36268
  "Content-Type": "application/json"
35771
36269
  },
36270
+ signal: abortController.signal,
35772
36271
  body: JSON.stringify({
35773
36272
  activation: "invite",
35774
36273
  sendOwnerEmail: false,
@@ -35789,15 +36288,20 @@ function VendorEditPage({ vendorId }) {
35789
36288
  }
35790
36289
  })
35791
36290
  });
35792
- const data = await res.json();
36291
+ clearTimeout(timeoutId);
36292
+ const data = await res.json().catch(() => ({}));
35793
36293
  if (!res.ok) {
35794
36294
  setErrors([
35795
36295
  data.error || "Onboarding failed"
35796
36296
  ]);
35797
36297
  return;
35798
36298
  }
36299
+ try {
36300
+ sessionStorage.removeItem(VENDOR_CREATE_DRAFT_KEY);
36301
+ } catch {
36302
+ }
35799
36303
  const vendorIdNum = Number(data.vendor?.id);
35800
- const inviteLink = typeof data.inviteLink === "string" && data.inviteLink.trim() ? data.inviteLink.trim() : "";
36304
+ const inviteLink = typeof data.inviteLink === "string" && data.inviteLink?.trim() ? data.inviteLink.trim() : "";
35801
36305
  if (!Number.isFinite(vendorIdNum) || vendorIdNum <= 0 || !inviteLink) {
35802
36306
  toast.success(data.message || "Vendor created");
35803
36307
  router.push(listReturnUrl);
@@ -35809,11 +36313,18 @@ function VendorEditPage({ vendorId }) {
35809
36313
  vendorId: vendorIdNum,
35810
36314
  inviteLink
35811
36315
  });
35812
- } catch {
35813
- setErrors([
35814
- "Request failed"
35815
- ]);
36316
+ } catch (err) {
36317
+ if (err instanceof DOMException && err.name === "AbortError") {
36318
+ setErrors([
36319
+ "Request timed out. Your entered data is preserved. Please click submit again."
36320
+ ]);
36321
+ } else {
36322
+ setErrors([
36323
+ err instanceof Error && err.message || "Request failed"
36324
+ ]);
36325
+ }
35816
36326
  } finally {
36327
+ clearTimeout(timeoutId);
35817
36328
  setSaving(false);
35818
36329
  }
35819
36330
  }, "handleCreate");
@@ -35852,12 +36363,15 @@ function VendorEditPage({ vendorId }) {
35852
36363
  setSaving(false);
35853
36364
  return;
35854
36365
  }
36366
+ const abortController = new AbortController();
36367
+ const timeoutId = setTimeout(() => abortController.abort(), 15e3);
35855
36368
  try {
35856
36369
  const res = await fetch(`/api/vendors/${vendorId}`, {
35857
36370
  method: "PUT",
35858
36371
  headers: {
35859
36372
  "Content-Type": "application/json"
35860
36373
  },
36374
+ signal: abortController.signal,
35861
36375
  body: JSON.stringify({
35862
36376
  ...vendorPayload(),
35863
36377
  slug: slug.trim(),
@@ -35873,6 +36387,7 @@ function VendorEditPage({ vendorId }) {
35873
36387
  })
35874
36388
  });
35875
36389
  if (!res.ok) {
36390
+ clearTimeout(timeoutId);
35876
36391
  const data = await res.json().catch(() => ({}));
35877
36392
  setErrors([
35878
36393
  data.error || "Failed to save"
@@ -35886,6 +36401,7 @@ function VendorEditPage({ vendorId }) {
35886
36401
  headers: {
35887
36402
  "Content-Type": "application/json"
35888
36403
  },
36404
+ signal: abortController.signal,
35889
36405
  body: JSON.stringify({
35890
36406
  name: ownerName.trim(),
35891
36407
  email: ownerEmail.trim(),
@@ -35893,6 +36409,7 @@ function VendorEditPage({ vendorId }) {
35893
36409
  })
35894
36410
  });
35895
36411
  if (!userRes.ok) {
36412
+ clearTimeout(timeoutId);
35896
36413
  const data = await userRes.json().catch(() => ({}));
35897
36414
  setErrors([
35898
36415
  data.error || "Vendor saved, but updating owner details failed"
@@ -35900,13 +36417,21 @@ function VendorEditPage({ vendorId }) {
35900
36417
  return;
35901
36418
  }
35902
36419
  }
36420
+ clearTimeout(timeoutId);
35903
36421
  toast.success("Vendor saved");
35904
36422
  router.push(listReturnUrl);
35905
- } catch {
35906
- setErrors([
35907
- "Failed to save"
35908
- ]);
36423
+ } catch (err) {
36424
+ if (err instanceof DOMException && err.name === "AbortError") {
36425
+ setErrors([
36426
+ "Request timed out. Please try saving again."
36427
+ ]);
36428
+ } else {
36429
+ setErrors([
36430
+ err instanceof Error && err.message || "Failed to save"
36431
+ ]);
36432
+ }
35909
36433
  } finally {
36434
+ clearTimeout(timeoutId);
35910
36435
  setSaving(false);
35911
36436
  }
35912
36437
  }, "handleSave");
@@ -41312,6 +41837,8 @@ var init_AdminPageResolver = __esm({
41312
41837
  submissions: {
41313
41838
  title: "Submissions",
41314
41839
  apiEndpoint: "/api/form-submissions",
41840
+ defaultSortField: "createdAt",
41841
+ defaultSortOrder: "desc",
41315
41842
  columns: [
41316
41843
  {
41317
41844
  field: "form.name",
@@ -41857,6 +42384,10 @@ function EventDetailPage({ eventId }) {
41857
42384
  eventId: String(event.id),
41858
42385
  returnUrl: detailUrl,
41859
42386
  readOnly: true
42387
+ }), /* @__PURE__ */ React.createElement(EventCombosSection, {
42388
+ eventId: String(event.id),
42389
+ returnUrl: detailUrl,
42390
+ readOnly: true
41860
42391
  }))
41861
42392
  }));
41862
42393
  }
@@ -41872,6 +42403,7 @@ var init_EventDetailPage = __esm({
41872
42403
  init_DetailPageLayout();
41873
42404
  init_DetailPageHeader();
41874
42405
  init_EventProductsSection();
42406
+ init_EventCombosSection();
41875
42407
  sectionCls6 = "border border-gray-200 rounded-lg p-4 bg-gray-50/50";
41876
42408
  labelCls7 = "text-xs font-medium text-gray-500";
41877
42409
  valueCls = "text-sm text-gray-900 mt-0.5";