PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / Destinations.tsx

Destinations.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/pages/Destinations.tsx

989 lines 36.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Destinations Page
3 * Clean, minimal SaaS-style destinations management page
4 */
5
6 import React, { useState, useMemo, useEffect } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 Plus,
10 ArrowUpDown,
11 ArrowUp,
12 ArrowDown,
13 RotateCcw,
14 Edit,
15 Trash2,
16 Eye,
17 ExternalLink,
18 } from "lucide-react";
19 import {
20 Pagination,
21 SearchFilterToolbar,
22 BulkActionToolbar,
23 Table as SharedTable,
24 } from "../components/shared";
25 import { getDefaultBulkStatusOptions } from "../components/shared/bulkStatusOptions";
26 import { __ } from "../lib/i18n";
27 import { usePermissions } from "../hooks/usePermissions";
28 import { useToast } from "../components/ui/toast";
29 import { fetchSettings } from "../api/settings-api";
30 import { apiClient } from "../lib/api-client";
31 import { getErrorContext } from "../lib/errors";
32 import {
33 buildYatraSinglePublicUrls,
34 isWordPressPlainPermalink,
35 } from "../lib/frontend-permalink-urls";
36 import { Button } from "../components/ui/button";
37 import { PageHeader } from "../components/common/PageHeader";
38 import { Card, CardContent } from "../components/ui/card";
39 import { ConditionalRender } from "../components/ui/conditional-render";
40 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
41 import { IconSelector } from "../components/ui/icon-selector";
42 import type { IconPickerValue } from "../components/ui/icon-picker";
43
44 interface Destination {
45 id: string | number;
46 name: string;
47 slug: string;
48 description: string;
49 icon?: IconPickerValue | null;
50 status: string;
51 trip_count?: number;
52 metadata?: {
53 seo_title?: string;
54 seo_description?: string;
55 seo_keywords?: string;
56 [key: string]: any;
57 };
58 created_at: string;
59 updated_at: string;
60 created_by: string;
61 updated_by: string;
62 created_by_name?: string;
63 updated_by_name?: string;
64 }
65
66 const Destinations: React.FC = () => {
67 const [searchTerm, setSearchTerm] = useState("");
68 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
69 const [sortBy, setSortBy] = useState("id");
70 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
71 const [page, setPage] = useState(1);
72 const [statusFilter, setStatusFilter] = useState("all");
73 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
74 const [bulkAction, setBulkAction] = useState("");
75 const [visibleColumns, setVisibleColumns] = useState(() => {
76 const defaultColumns = {
77 name: true,
78 description: true,
79 trips: true,
80 status: true,
81 seo_title: false,
82 seo_description: false,
83 seo_keywords: false,
84 created_at: false,
85 updated_at: false,
86 created_by_name: false,
87 updated_by_name: false,
88 };
89 const saved = localStorage.getItem("yatra_destinations_visible_columns_v2");
90 return saved ? { ...defaultColumns, ...JSON.parse(saved) } : defaultColumns;
91 });
92 const [deleteConfirm, setDeleteConfirm] = useState<{
93 isOpen: boolean;
94 destination: Destination | null;
95 }>({
96 isOpen: false,
97 destination: null,
98 });
99 const queryClient = useQueryClient();
100 const { can } = usePermissions();
101 const { showToast } = useToast();
102
103 // Close dropdown when clicking outside
104 useEffect(() => {
105 const handleClickOutside = (event: MouseEvent) => {
106 const target = event.target as HTMLElement;
107 if (
108 !target.closest("[data-columns-trigger]") &&
109 !target.closest("[data-columns-content]")
110 ) {
111 setShowColumnsDropdown(false);
112 }
113 };
114
115 document.addEventListener("mousedown", handleClickOutside);
116 return () => document.removeEventListener("mousedown", handleClickOutside);
117 }, []);
118
119 // Build query params
120 const queryParams = useMemo(() => {
121 const params: Record<string, any> = {
122 page,
123 per_page: 10,
124 orderby: sortBy,
125 order: sortOrder,
126 };
127
128 if (searchTerm) {
129 params.search = searchTerm;
130 }
131
132 if (statusFilter !== "all") {
133 params.status = statusFilter;
134 }
135
136 return params;
137 }, [searchTerm, statusFilter, sortBy, sortOrder, page]);
138
139 // Fetch status counts from API
140 const { data: statsData } = useQuery({
141 queryKey: ["destinations-stats"],
142 queryFn: async () => {
143 try {
144 const response = await apiClient.get("/destinations/stats");
145 return response;
146 } catch (error: any) {
147 return { all: 0, publish: 0, draft: 0, trash: 0 };
148 }
149 },
150 enabled: can("yatra_view_trips"),
151 });
152
153 // Fetch destinations from API
154 const { data, isLoading, error } = useQuery({
155 queryKey: ["destinations", queryParams],
156 queryFn: async () => {
157 try {
158 const response = await apiClient.get("/destinations", {
159 params: queryParams,
160 });
161 return response;
162 } catch (error: any) {
163 showToast(
164 error?.message || __("Failed to load destinations", "yatra"),
165 "error",
166 );
167 throw error;
168 }
169 },
170 enabled: can("yatra_view_trips"),
171 });
172
173 // Fetch settings for permalink handling
174 const { data: settings } = useQuery({
175 queryKey: ["settings"],
176 queryFn: async () => {
177 try {
178 const response = await fetchSettings();
179 return response;
180 } catch (error) {
181 return null;
182 }
183 },
184 enabled: can("manage_yatra"),
185 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
186 });
187
188 // Delete mutation
189 const deleteMutation = useMutation({
190 mutationFn: async (id: string | number) => {
191 return await apiClient.delete(`/destinations/${id}`);
192 },
193 onSuccess: () => {
194 queryClient.invalidateQueries({ queryKey: ["destinations"] });
195 queryClient.invalidateQueries({ queryKey: ["destinations-stats"] });
196 showToast(__("Destination deleted successfully", "yatra"), "success");
197 setDeleteConfirm({ isOpen: false, destination: null });
198 },
199 onError: (error: any) => {
200 showToast(
201 error?.message || __("Failed to delete destination", "yatra"),
202 "error",
203 );
204 },
205 });
206
207 const destinations = data?.data || [];
208 const total = data?.total || 0;
209 const totalPages = Math.ceil(total / 10);
210 const errorContext = getErrorContext(error);
211
212 const handleEdit = (destination: Destination) => {
213 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=edit&id=${destination.id}`;
214 };
215
216 const handleView = async (destination: Destination) => {
217 const destinationSlug = destination.slug || "";
218 const { plainUrl, prettyUrl } = buildYatraSinglePublicUrls({
219 entity: "destination",
220 slug: destinationSlug,
221 bases: settings as Record<string, unknown> | null,
222 });
223
224 if (isWordPressPlainPermalink()) {
225 if (!destinationSlug) {
226 showToast(__("Destination slug is missing", "yatra"), "error");
227 return;
228 }
229 window.open(plainUrl, "_blank", "noopener,noreferrer");
230 return;
231 }
232
233 let apiPermalink =
234 (destination as any)?.permalink || (destination as any)?.url;
235
236 if (!apiPermalink && destination.id) {
237 try {
238 const detail = await apiClient.get(`/destinations/${destination.id}`);
239 apiPermalink =
240 (detail as any)?.permalink || (detail as any)?.url || apiPermalink;
241 } catch (e) {
242 // fall through to prettyUrl
243 }
244 }
245
246 if (apiPermalink) {
247 window.open(apiPermalink, "_blank", "noopener,noreferrer");
248 return;
249 }
250
251 if (!destinationSlug) {
252 showToast(__("Destination slug is missing", "yatra"), "error");
253 return;
254 }
255
256 window.open(prettyUrl, "_blank", "noopener,noreferrer");
257 };
258
259 const handleDelete = (destination: Destination) => {
260 setDeleteConfirm({ isOpen: true, destination });
261 };
262
263 const confirmDelete = () => {
264 if (deleteConfirm.destination) {
265 deleteMutation.mutate(deleteConfirm.destination.id);
266 }
267 };
268
269 const handleCreateDestination = () => {
270 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=create`;
271 };
272
273 const handleResetFilters = () => {
274 setSearchTerm("");
275 setStatusFilter("all");
276 setSortBy("id");
277 setSortOrder("desc");
278 setPage(1);
279 };
280
281 const handleSort = (field: string) => {
282 if (sortBy === field) {
283 setSortOrder(sortOrder === "asc" ? "desc" : "asc");
284 } else {
285 setSortBy(field);
286 setSortOrder(field === "id" ? "desc" : "asc");
287 }
288 };
289
290 const getSortIcon = (field: string) => {
291 if (sortBy !== field) {
292 return <ArrowUpDown className="w-3.5 h-3.5 ml-1 text-gray-400" />;
293 }
294 return sortOrder === "asc" ? (
295 <ArrowUp className="w-3.5 h-3.5 ml-1 text-gray-600 dark:text-gray-300" />
296 ) : (
297 <ArrowDown className="w-3.5 h-3.5 ml-1 text-gray-600 dark:text-gray-300" />
298 );
299 };
300
301 // Toggle column visibility
302 const toggleColumn = (columnKey: string) => {
303 const newVisibleColumns = {
304 ...visibleColumns,
305 [columnKey]: !visibleColumns[columnKey as keyof typeof visibleColumns],
306 };
307
308 setVisibleColumns(newVisibleColumns);
309 localStorage.setItem(
310 "yatra_destinations_visible_columns_v2",
311 JSON.stringify(newVisibleColumns),
312 );
313 };
314
315 // Status counts from stats API
316 const statusCounts = useMemo(() => {
317 if (statsData) {
318 return {
319 all: statsData.all ?? 0,
320 publish: statsData.publish ?? 0,
321 draft: statsData.draft ?? 0,
322 trash: statsData.trash ?? 0,
323 };
324 }
325 return {
326 all: 0,
327 publish: 0,
328 draft: 0,
329 trash: 0,
330 };
331 }, [statsData]);
332
333 // Bulk actions mutation
334 const bulkMutation = useMutation({
335 mutationFn: async ({
336 action,
337 ids,
338 }: {
339 action: string;
340 ids: (string | number)[];
341 }) => {
342 return await apiClient.post("/destinations/bulk", { action, ids });
343 },
344 onSuccess: () => {
345 queryClient.invalidateQueries({ queryKey: ["destinations"] });
346 queryClient.invalidateQueries({ queryKey: ["destinations-stats"] });
347 showToast(__("Bulk action completed successfully", "yatra"), "success");
348 setSelectedIds([]);
349 setBulkAction("");
350 },
351 onError: (error: any) => {
352 showToast(
353 error?.message || __("Failed to perform bulk action", "yatra"),
354 "error",
355 );
356 },
357 });
358
359 const handleBulkApply = () => {
360 if (!bulkAction) {
361 showToast(__("Select a bulk action first.", "yatra"), "warning");
362 return;
363 }
364
365 if (selectedIds.length === 0) {
366 showToast(__("Select at least one destination.", "yatra"), "warning");
367 return;
368 }
369
370 // Execute bulk action (confirmation is now handled in BulkActionToolbar)
371 bulkMutation.mutate({ action: bulkAction, ids: selectedIds });
372 };
373
374 const viewFilters = [
375 { key: "all", label: __("All", "yatra"), count: statusCounts.all ?? 0 },
376 {
377 key: "publish",
378 label: __("Published", "yatra"),
379 count: statusCounts.publish ?? 0,
380 },
381 {
382 key: "draft",
383 label: __("Draft", "yatra"),
384 count: statusCounts.draft ?? 0,
385 },
386 {
387 key: "trash",
388 label: __("Trash", "yatra"),
389 count: statusCounts.trash ?? 0,
390 },
391 ];
392
393 const hasFilters =
394 searchTerm ||
395 statusFilter !== "all" ||
396 sortBy !== "id" ||
397 sortOrder !== "desc";
398
399 return (
400 <div className="space-y-3">
401 {/* Delete Confirmation Dialog */}
402 <ConfirmationDialog
403 isOpen={deleteConfirm.isOpen}
404 onClose={() => setDeleteConfirm({ isOpen: false, destination: null })}
405 onConfirm={confirmDelete}
406 title={__("Delete Destination", "yatra")}
407 message={
408 deleteConfirm.destination
409 ? __(
410 'Are you sure you want to delete "{name}"? This action cannot be undone.',
411 "yatra",
412 ).replace("{name}", deleteConfirm.destination.name)
413 : __(
414 "Are you sure you want to delete this destination? This action cannot be undone.",
415 "yatra",
416 )
417 }
418 confirmText={__("Delete", "yatra")}
419 cancelText={__("Cancel", "yatra")}
420 variant="danger"
421 isLoading={deleteMutation.isPending}
422 />
423
424 <PageHeader
425 title={__("Destinations", "yatra")}
426 description={__("Manage your travel destinations", "yatra")}
427 actionCapability="yatra_edit_trips"
428 actions={
429 <Button
430 onClick={handleCreateDestination}
431 className="flex items-center gap-2"
432 >
433 <Plus className="w-4 h-4" />
434 {__("Add New Destination", "yatra")}
435 </Button>
436 }
437 />
438
439 {/* Filters, Search, and Sorting - Always Visible */}
440 <Card>
441 <CardContent className="p-3">
442 <SearchFilterToolbar
443 searchTerm={searchTerm}
444 onSearchChange={setSearchTerm}
445 statusFilter={statusFilter}
446 onStatusChange={(value) => {
447 setStatusFilter(value);
448 setPage(1);
449 setSelectedIds([]);
450 setBulkAction("");
451 }}
452 statusOptions={[
453 { value: "all", label: __("All Status", "yatra") },
454 { value: "publish", label: __("Published", "yatra") },
455 { value: "draft", label: __("Draft", "yatra") },
456 { value: "trash", label: __("Trash", "yatra") },
457 ]}
458 sortBy={sortBy}
459 onSortByChange={setSortBy}
460 sortOrder={sortOrder}
461 onSortOrderChange={setSortOrder}
462 sortOptions={[
463 { value: "name", label: __("Name", "yatra") },
464 { value: "status", label: __("Status", "yatra") },
465 { value: "created_at", label: __("Created At", "yatra") },
466 { value: "updated_at", label: __("Updated At", "yatra") },
467 ]}
468 onResetFilters={handleResetFilters}
469 hasFilters={!!hasFilters}
470 placeholder={__("Search destinations...", "yatra")}
471 />
472 </CardContent>
473 </Card>
474
475 {!error && (
476 <BulkActionToolbar
477 selectedIds={selectedIds}
478 bulkAction={bulkAction}
479 setBulkAction={setBulkAction}
480 onApply={handleBulkApply}
481 onClearSelection={() => setSelectedIds([])}
482 statusFilter={statusFilter}
483 setStatusFilter={(value) => {
484 setStatusFilter(value);
485 setPage(1);
486 setSelectedIds([]);
487 setBulkAction("");
488 }}
489 statusOptions={viewFilters}
490 showColumnsDropdown={showColumnsDropdown}
491 setShowColumnsDropdown={setShowColumnsDropdown}
492 columnOptions={[
493 {
494 key: "name",
495 label: __("Destination", "yatra"),
496 visible: visibleColumns.name,
497 },
498 {
499 key: "description",
500 label: __("Description", "yatra"),
501 visible: visibleColumns.description,
502 },
503 {
504 key: "trips",
505 label: __("Trips", "yatra"),
506 visible: visibleColumns.trips,
507 },
508 {
509 key: "seo_title",
510 label: __("SEO Title", "yatra"),
511 visible: visibleColumns.seo_title,
512 },
513 {
514 key: "seo_description",
515 label: __("SEO Description", "yatra"),
516 visible: visibleColumns.seo_description,
517 },
518 {
519 key: "seo_keywords",
520 label: __("SEO Keywords", "yatra"),
521 visible: visibleColumns.seo_keywords,
522 },
523 {
524 key: "status",
525 label: __("Status", "yatra"),
526 visible: visibleColumns.status,
527 },
528 {
529 key: "created_at",
530 label: __("Created Date", "yatra"),
531 visible: visibleColumns.created_at,
532 },
533 {
534 key: "updated_at",
535 label: __("Updated Date", "yatra"),
536 visible: visibleColumns.updated_at,
537 },
538 {
539 key: "created_by_name",
540 label: __("Created By", "yatra"),
541 visible: visibleColumns.created_by_name,
542 },
543 {
544 key: "updated_by_name",
545 label: __("Updated By", "yatra"),
546 visible: visibleColumns.updated_by_name,
547 },
548 ]}
549 onToggleColumn={toggleColumn}
550 bulkMutationPending={bulkMutation.isPending}
551 totalItems={destinations.length}
552 bulkActionOptions={getDefaultBulkStatusOptions(statusFilter)}
553 />
554 )}
555
556 <ConditionalRender capability="yatra_view_trips">
557 <>
558 <Card className="overflow-visible">
559 <CardContent className="p-0 overflow-visible">
560 <SharedTable
561 key={`destinations-table-${JSON.stringify(visibleColumns)}`}
562 data={destinations}
563 columns={useMemo(
564 () => [
565 {
566 key: "name",
567 label: __("Destination", "yatra"),
568 sortable: true,
569 visible: visibleColumns.name,
570 render: (destination: Destination) => (
571 <div className="flex items-center gap-3">
572 {/* Icon/Image */}
573 <div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0">
574 {destination.icon ? (
575 destination.icon.type === "image" ? (
576 <img
577 src={destination.icon.value}
578 alt={destination.name}
579 className="w-full h-full object-cover rounded-lg"
580 />
581 ) : (
582 <IconSelector
583 iconName={destination.icon.value}
584 provider={destination.icon.provider ?? "yatra"}
585 className="w-5 h-5 text-blue-600 dark:text-blue-400"
586 />
587 )
588 ) : (
589 <div className="w-5 h-5 text-blue-600 dark:text-blue-400 flex items-center justify-center text-xs font-semibold">
590 {destination.name.charAt(0).toUpperCase()}
591 </div>
592 )}
593 </div>
594 {/* Text */}
595 <div>
596 <div className="flex items-center gap-1">
597 <a
598 href={`${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=edit&id=${destination.id}`}
599 className="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors cursor-pointer"
600 >
601 {destination.name}
602 </a>
603 {can("yatra_view_trips") &&
604 destination.status !== "trash" && (
605 <button
606 type="button"
607 onClick={(e) => {
608 e.stopPropagation();
609 handleView(destination);
610 }}
611 className="ml-1 inline-flex items-center justify-center rounded-full p-1 text-gray-400 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-800"
612 title={__(
613 "View destination in new tab",
614 "yatra",
615 )}
616 >
617 <ExternalLink className="w-3.5 h-3.5" />
618 </button>
619 )}
620 </div>
621 <div className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
622 <span>{destination.slug}</span>
623 <span className="text-[11px] text-gray-400 dark:text-gray-500">
624 ({__("ID:", "yatra")} {destination.id})
625 </span>
626 </div>
627 </div>
628 </div>
629 ),
630 },
631 {
632 key: "trips",
633 label: __("Trips", "yatra"),
634 sortable: false,
635 visible: visibleColumns.trips,
636 render: (destination: Destination) => (
637 <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
638 {typeof destination.trip_count === "number"
639 ? destination.trip_count
640 : 0}
641 </span>
642 ),
643 },
644 {
645 key: "description",
646 label: __("Description", "yatra"),
647 visible: visibleColumns.description,
648 render: (destination: Destination) => (
649 <span
650 className={`block max-w-[300px] truncate ${
651 destination.status === "trash" ||
652 statusFilter === "trash"
653 ? "text-gray-400 dark:text-gray-600"
654 : "text-gray-600 dark:text-gray-400"
655 }`}
656 title={destination.description || ""}
657 >
658 {destination.description ||
659 __("No description", "yatra")}
660 </span>
661 ),
662 },
663 {
664 key: "seo_title",
665 label: __("SEO Title", "yatra"),
666 visible: visibleColumns.seo_title,
667 render: (destination: Destination) => (
668 <span
669 className={
670 destination.status === "trash" ||
671 statusFilter === "trash"
672 ? "text-gray-400 dark:text-gray-600"
673 : "text-gray-600 dark:text-gray-400"
674 }
675 >
676 {destination.metadata?.seo_title ||
677 __("Not set", "yatra")}
678 </span>
679 ),
680 },
681 {
682 key: "seo_description",
683 label: __("SEO Description", "yatra"),
684 visible: visibleColumns.seo_description,
685 render: (destination: Destination) => (
686 <span
687 className={
688 destination.status === "trash" ||
689 statusFilter === "trash"
690 ? "text-gray-400 dark:text-gray-600"
691 : "text-gray-600 dark:text-gray-400"
692 }
693 title={destination.metadata?.seo_description || ""}
694 >
695 {destination.metadata?.seo_description
696 ? destination.metadata.seo_description.length > 50
697 ? destination.metadata.seo_description.substring(
698 0,
699 50,
700 ) + "..."
701 : destination.metadata.seo_description
702 : __("Not set", "yatra")}
703 </span>
704 ),
705 },
706 {
707 key: "seo_keywords",
708 label: __("SEO Keywords", "yatra"),
709 visible: visibleColumns.seo_keywords,
710 render: (destination: Destination) => (
711 <span
712 className={
713 destination.status === "trash" ||
714 statusFilter === "trash"
715 ? "text-gray-400 dark:text-gray-600"
716 : "text-gray-600 dark:text-gray-400"
717 }
718 title={destination.metadata?.seo_keywords || ""}
719 >
720 {destination.metadata?.seo_keywords
721 ? destination.metadata.seo_keywords.length > 30
722 ? destination.metadata.seo_keywords.substring(
723 0,
724 30,
725 ) + "..."
726 : destination.metadata.seo_keywords
727 : __("Not set", "yatra")}
728 </span>
729 ),
730 },
731 {
732 key: "status",
733 label: __("Status", "yatra"),
734 sortable: true,
735 visible: visibleColumns.status,
736 render: (destination: Destination) => (
737 <span
738 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
739 destination.status === "trash" ||
740 statusFilter === "trash"
741 ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
742 : destination.status === "publish"
743 ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
744 : "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
745 }`}
746 >
747 {destination.status === "trash" ||
748 statusFilter === "trash"
749 ? __("Trash", "yatra")
750 : destination.status === "publish"
751 ? __("Published", "yatra")
752 : __("Draft", "yatra")}
753 </span>
754 ),
755 },
756 {
757 key: "created_at",
758 label: __("Created Date", "yatra"),
759 sortable: true,
760 visible: visibleColumns.created_at,
761 render: (destination: Destination) => (
762 <span
763 className={
764 destination.status === "trash" ||
765 statusFilter === "trash"
766 ? "text-gray-400 dark:text-gray-600"
767 : "text-gray-600 dark:text-gray-400"
768 }
769 >
770 {new Date(destination.created_at).toLocaleDateString(
771 "en-US",
772 {
773 year: "numeric",
774 month: "short",
775 day: "numeric",
776 hour: "2-digit",
777 minute: "2-digit",
778 },
779 )}
780 </span>
781 ),
782 },
783 {
784 key: "updated_at",
785 label: __("Updated Date", "yatra"),
786 sortable: true,
787 visible: visibleColumns.updated_at,
788 render: (destination: Destination) => (
789 <span
790 className={
791 destination.status === "trash" ||
792 statusFilter === "trash"
793 ? "text-gray-400 dark:text-gray-600"
794 : "text-gray-600 dark:text-gray-400"
795 }
796 >
797 {new Date(destination.updated_at).toLocaleDateString(
798 "en-US",
799 {
800 year: "numeric",
801 month: "short",
802 day: "numeric",
803 hour: "2-digit",
804 minute: "2-digit",
805 },
806 )}
807 </span>
808 ),
809 },
810 {
811 key: "created_by_name",
812 label: __("Created By", "yatra"),
813 visible: visibleColumns.created_by_name,
814 render: (destination: Destination) => (
815 <span
816 className={
817 destination.status === "trash" ||
818 statusFilter === "trash"
819 ? "text-gray-400 dark:text-gray-600"
820 : "text-gray-600 dark:text-gray-400"
821 }
822 >
823 {destination.created_by_name ||
824 __("Unknown", "yatra")}
825 </span>
826 ),
827 },
828 {
829 key: "updated_by_name",
830 label: __("Updated By", "yatra"),
831 visible: visibleColumns.updated_by_name,
832 render: (destination: Destination) => (
833 <span
834 className={
835 destination.status === "trash" ||
836 statusFilter === "trash"
837 ? "text-gray-400 dark:text-gray-600"
838 : "text-gray-600 dark:text-gray-400"
839 }
840 >
841 {destination.updated_by_name ||
842 __("Unknown", "yatra")}
843 </span>
844 ),
845 },
846 ],
847 [visibleColumns],
848 )}
849 actions={[
850 {
851 key: "view",
852 label: __("View", "yatra"),
853 icon: <Eye className="w-4 h-4" />,
854 onClick: (destination: Destination) => {
855 // Open destination in new tab
856 const destinationUrl = `${window.yatraAdmin?.siteUrl || ""}/destination/${destination.slug}`;
857 window.open(destinationUrl, "_blank");
858 },
859 condition: () => true, // Always show view action
860 },
861 {
862 key: "edit",
863 label: __("Edit", "yatra"),
864 icon: <Edit className="w-4 h-4" />,
865 onClick: handleEdit,
866 condition: () => can("yatra_edit_trips"),
867 },
868 {
869 key: "restore",
870 label: __("Restore", "yatra"),
871 icon: <RotateCcw className="w-4 h-4" />,
872 onClick: (destination: Destination) => {
873 // Handle restore action
874 bulkMutation.mutate({
875 action: "restore",
876 ids: [destination.id],
877 });
878 },
879 condition: (destination: Destination) =>
880 (destination.status === "trash" ||
881 statusFilter === "trash") &&
882 can("yatra_edit_trips"),
883 },
884 {
885 key: "trash",
886 label: __("Move to Trash", "yatra"),
887 icon: <Trash2 className="w-4 h-4" />,
888 onClick: (destination: Destination) => {
889 bulkMutation.mutate({
890 action: "trash",
891 ids: [destination.id],
892 });
893 },
894 condition: (destination: Destination) =>
895 destination.status !== "trash" &&
896 statusFilter !== "trash" &&
897 can("yatra_edit_trips"),
898 },
899 {
900 key: "delete",
901 label: __("Delete Permanently", "yatra"),
902 icon: <Trash2 className="w-4 h-4" />,
903 onClick: handleDelete,
904 condition: (destination: Destination) =>
905 (destination.status === "trash" ||
906 statusFilter === "trash") &&
907 can("yatra_delete_trips"),
908 variant: "destructive",
909 },
910 ]}
911 isLoading={isLoading}
912 isError={!!error}
913 errorText={__("Error loading destinations", "yatra")}
914 errorDescription={__(
915 "We couldn’t connect to the destinations service. Please refresh or try again shortly.",
916 "yatra",
917 )}
918 onRetry={() =>
919 queryClient.invalidateQueries({ queryKey: ["destinations"] })
920 }
921 errorDetails={errorContext.details}
922 errorRequestInfo={errorContext.requestInfo}
923 emptyText={__("No destinations found", "yatra")}
924 emptyDescription={
925 hasFilters
926 ? __(
927 "Try adjusting your filters to see more results.",
928 "yatra",
929 )
930 : __(
931 "Get started by creating your first destination.",
932 "yatra",
933 )
934 }
935 onCreateClick={
936 can("yatra_edit_trips") ? handleCreateDestination : undefined
937 }
938 onSort={handleSort}
939 getSortIcon={getSortIcon}
940 selectedItemIds={selectedIds}
941 onSelectItem={(id: string | number, checked: boolean) => {
942 if (checked) {
943 setSelectedIds([...selectedIds, id]);
944 } else {
945 setSelectedIds(
946 selectedIds.filter((selectedId) => selectedId !== id),
947 );
948 }
949 }}
950 onSelectAll={(checked: boolean) => {
951 if (checked) {
952 setSelectedIds(destinations.map((d: Destination) => d.id));
953 } else {
954 setSelectedIds([]);
955 }
956 }}
957 isAllSelected={
958 destinations.length > 0 &&
959 selectedIds.length === destinations.length
960 }
961 getItemId={(destination: Destination) => destination.id}
962 getItemStatus={(destination: Destination) => destination.status}
963 statusFilter={statusFilter}
964 skeletonRows={5}
965 />
966 </CardContent>
967 </Card>
968 </>
969 </ConditionalRender>
970
971 {/* Pagination */}
972 {total > 0 && (
973 <div className="mt-4">
974 <Pagination
975 currentPage={page}
976 totalPages={totalPages}
977 totalItems={total}
978 itemsPerPage={10}
979 onPageChange={(newPage) => setPage(newPage)}
980 itemName={__("destinations", "yatra")}
981 />
982 </div>
983 )}
984 </div>
985 );
986 };
987
988 export default Destinations;
989