PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
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.2.6, at resources/js/pages/Destinations.tsx

988 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 className="w-5 h-5 text-blue-600 dark:text-blue-400"
585 />
586 )
587 ) : (
588 <div className="w-5 h-5 text-blue-600 dark:text-blue-400 flex items-center justify-center text-xs font-semibold">
589 {destination.name.charAt(0).toUpperCase()}
590 </div>
591 )}
592 </div>
593 {/* Text */}
594 <div>
595 <div className="flex items-center gap-1">
596 <a
597 href={`${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=edit&id=${destination.id}`}
598 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"
599 >
600 {destination.name}
601 </a>
602 {can("yatra_view_trips") &&
603 destination.status !== "trash" && (
604 <button
605 type="button"
606 onClick={(e) => {
607 e.stopPropagation();
608 handleView(destination);
609 }}
610 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"
611 title={__(
612 "View destination in new tab",
613 "yatra",
614 )}
615 >
616 <ExternalLink className="w-3.5 h-3.5" />
617 </button>
618 )}
619 </div>
620 <div className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
621 <span>{destination.slug}</span>
622 <span className="text-[11px] text-gray-400 dark:text-gray-500">
623 ({__("ID:", "yatra")} {destination.id})
624 </span>
625 </div>
626 </div>
627 </div>
628 ),
629 },
630 {
631 key: "trips",
632 label: __("Trips", "yatra"),
633 sortable: false,
634 visible: visibleColumns.trips,
635 render: (destination: Destination) => (
636 <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">
637 {typeof destination.trip_count === "number"
638 ? destination.trip_count
639 : 0}
640 </span>
641 ),
642 },
643 {
644 key: "description",
645 label: __("Description", "yatra"),
646 visible: visibleColumns.description,
647 render: (destination: Destination) => (
648 <span
649 className={`block max-w-[300px] truncate ${
650 destination.status === "trash" ||
651 statusFilter === "trash"
652 ? "text-gray-400 dark:text-gray-600"
653 : "text-gray-600 dark:text-gray-400"
654 }`}
655 title={destination.description || ""}
656 >
657 {destination.description ||
658 __("No description", "yatra")}
659 </span>
660 ),
661 },
662 {
663 key: "seo_title",
664 label: __("SEO Title", "yatra"),
665 visible: visibleColumns.seo_title,
666 render: (destination: Destination) => (
667 <span
668 className={
669 destination.status === "trash" ||
670 statusFilter === "trash"
671 ? "text-gray-400 dark:text-gray-600"
672 : "text-gray-600 dark:text-gray-400"
673 }
674 >
675 {destination.metadata?.seo_title ||
676 __("Not set", "yatra")}
677 </span>
678 ),
679 },
680 {
681 key: "seo_description",
682 label: __("SEO Description", "yatra"),
683 visible: visibleColumns.seo_description,
684 render: (destination: Destination) => (
685 <span
686 className={
687 destination.status === "trash" ||
688 statusFilter === "trash"
689 ? "text-gray-400 dark:text-gray-600"
690 : "text-gray-600 dark:text-gray-400"
691 }
692 title={destination.metadata?.seo_description || ""}
693 >
694 {destination.metadata?.seo_description
695 ? destination.metadata.seo_description.length > 50
696 ? destination.metadata.seo_description.substring(
697 0,
698 50,
699 ) + "..."
700 : destination.metadata.seo_description
701 : __("Not set", "yatra")}
702 </span>
703 ),
704 },
705 {
706 key: "seo_keywords",
707 label: __("SEO Keywords", "yatra"),
708 visible: visibleColumns.seo_keywords,
709 render: (destination: Destination) => (
710 <span
711 className={
712 destination.status === "trash" ||
713 statusFilter === "trash"
714 ? "text-gray-400 dark:text-gray-600"
715 : "text-gray-600 dark:text-gray-400"
716 }
717 title={destination.metadata?.seo_keywords || ""}
718 >
719 {destination.metadata?.seo_keywords
720 ? destination.metadata.seo_keywords.length > 30
721 ? destination.metadata.seo_keywords.substring(
722 0,
723 30,
724 ) + "..."
725 : destination.metadata.seo_keywords
726 : __("Not set", "yatra")}
727 </span>
728 ),
729 },
730 {
731 key: "status",
732 label: __("Status", "yatra"),
733 sortable: true,
734 visible: visibleColumns.status,
735 render: (destination: Destination) => (
736 <span
737 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
738 destination.status === "trash" ||
739 statusFilter === "trash"
740 ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
741 : destination.status === "publish"
742 ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
743 : "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
744 }`}
745 >
746 {destination.status === "trash" ||
747 statusFilter === "trash"
748 ? __("Trash", "yatra")
749 : destination.status === "publish"
750 ? __("Published", "yatra")
751 : __("Draft", "yatra")}
752 </span>
753 ),
754 },
755 {
756 key: "created_at",
757 label: __("Created Date", "yatra"),
758 sortable: true,
759 visible: visibleColumns.created_at,
760 render: (destination: Destination) => (
761 <span
762 className={
763 destination.status === "trash" ||
764 statusFilter === "trash"
765 ? "text-gray-400 dark:text-gray-600"
766 : "text-gray-600 dark:text-gray-400"
767 }
768 >
769 {new Date(destination.created_at).toLocaleDateString(
770 "en-US",
771 {
772 year: "numeric",
773 month: "short",
774 day: "numeric",
775 hour: "2-digit",
776 minute: "2-digit",
777 },
778 )}
779 </span>
780 ),
781 },
782 {
783 key: "updated_at",
784 label: __("Updated Date", "yatra"),
785 sortable: true,
786 visible: visibleColumns.updated_at,
787 render: (destination: Destination) => (
788 <span
789 className={
790 destination.status === "trash" ||
791 statusFilter === "trash"
792 ? "text-gray-400 dark:text-gray-600"
793 : "text-gray-600 dark:text-gray-400"
794 }
795 >
796 {new Date(destination.updated_at).toLocaleDateString(
797 "en-US",
798 {
799 year: "numeric",
800 month: "short",
801 day: "numeric",
802 hour: "2-digit",
803 minute: "2-digit",
804 },
805 )}
806 </span>
807 ),
808 },
809 {
810 key: "created_by_name",
811 label: __("Created By", "yatra"),
812 visible: visibleColumns.created_by_name,
813 render: (destination: Destination) => (
814 <span
815 className={
816 destination.status === "trash" ||
817 statusFilter === "trash"
818 ? "text-gray-400 dark:text-gray-600"
819 : "text-gray-600 dark:text-gray-400"
820 }
821 >
822 {destination.created_by_name ||
823 __("Unknown", "yatra")}
824 </span>
825 ),
826 },
827 {
828 key: "updated_by_name",
829 label: __("Updated By", "yatra"),
830 visible: visibleColumns.updated_by_name,
831 render: (destination: Destination) => (
832 <span
833 className={
834 destination.status === "trash" ||
835 statusFilter === "trash"
836 ? "text-gray-400 dark:text-gray-600"
837 : "text-gray-600 dark:text-gray-400"
838 }
839 >
840 {destination.updated_by_name ||
841 __("Unknown", "yatra")}
842 </span>
843 ),
844 },
845 ],
846 [visibleColumns],
847 )}
848 actions={[
849 {
850 key: "view",
851 label: __("View", "yatra"),
852 icon: <Eye className="w-4 h-4" />,
853 onClick: (destination: Destination) => {
854 // Open destination in new tab
855 const destinationUrl = `${window.yatraAdmin?.siteUrl || ""}/destination/${destination.slug}`;
856 window.open(destinationUrl, "_blank");
857 },
858 condition: () => true, // Always show view action
859 },
860 {
861 key: "edit",
862 label: __("Edit", "yatra"),
863 icon: <Edit className="w-4 h-4" />,
864 onClick: handleEdit,
865 condition: () => can("yatra_edit_trips"),
866 },
867 {
868 key: "restore",
869 label: __("Restore", "yatra"),
870 icon: <RotateCcw className="w-4 h-4" />,
871 onClick: (destination: Destination) => {
872 // Handle restore action
873 bulkMutation.mutate({
874 action: "restore",
875 ids: [destination.id],
876 });
877 },
878 condition: (destination: Destination) =>
879 (destination.status === "trash" ||
880 statusFilter === "trash") &&
881 can("yatra_edit_trips"),
882 },
883 {
884 key: "trash",
885 label: __("Move to Trash", "yatra"),
886 icon: <Trash2 className="w-4 h-4" />,
887 onClick: (destination: Destination) => {
888 bulkMutation.mutate({
889 action: "trash",
890 ids: [destination.id],
891 });
892 },
893 condition: (destination: Destination) =>
894 destination.status !== "trash" &&
895 statusFilter !== "trash" &&
896 can("yatra_edit_trips"),
897 },
898 {
899 key: "delete",
900 label: __("Delete Permanently", "yatra"),
901 icon: <Trash2 className="w-4 h-4" />,
902 onClick: handleDelete,
903 condition: (destination: Destination) =>
904 (destination.status === "trash" ||
905 statusFilter === "trash") &&
906 can("yatra_delete_trips"),
907 variant: "destructive",
908 },
909 ]}
910 isLoading={isLoading}
911 isError={!!error}
912 errorText={__("Error loading destinations", "yatra")}
913 errorDescription={__(
914 "We couldn’t connect to the destinations service. Please refresh or try again shortly.",
915 "yatra",
916 )}
917 onRetry={() =>
918 queryClient.invalidateQueries({ queryKey: ["destinations"] })
919 }
920 errorDetails={errorContext.details}
921 errorRequestInfo={errorContext.requestInfo}
922 emptyText={__("No destinations found", "yatra")}
923 emptyDescription={
924 hasFilters
925 ? __(
926 "Try adjusting your filters to see more results.",
927 "yatra",
928 )
929 : __(
930 "Get started by creating your first destination.",
931 "yatra",
932 )
933 }
934 onCreateClick={
935 can("yatra_edit_trips") ? handleCreateDestination : undefined
936 }
937 onSort={handleSort}
938 getSortIcon={getSortIcon}
939 selectedItemIds={selectedIds}
940 onSelectItem={(id: string | number, checked: boolean) => {
941 if (checked) {
942 setSelectedIds([...selectedIds, id]);
943 } else {
944 setSelectedIds(
945 selectedIds.filter((selectedId) => selectedId !== id),
946 );
947 }
948 }}
949 onSelectAll={(checked: boolean) => {
950 if (checked) {
951 setSelectedIds(destinations.map((d: Destination) => d.id));
952 } else {
953 setSelectedIds([]);
954 }
955 }}
956 isAllSelected={
957 destinations.length > 0 &&
958 selectedIds.length === destinations.length
959 }
960 getItemId={(destination: Destination) => destination.id}
961 getItemStatus={(destination: Destination) => destination.status}
962 statusFilter={statusFilter}
963 skeletonRows={5}
964 />
965 </CardContent>
966 </Card>
967 </>
968 </ConditionalRender>
969
970 {/* Pagination */}
971 {total > 0 && (
972 <div className="mt-4">
973 <Pagination
974 currentPage={page}
975 totalPages={totalPages}
976 totalItems={total}
977 itemsPerPage={10}
978 onPageChange={(newPage) => setPage(newPage)}
979 itemName={__("destinations", "yatra")}
980 />
981 </div>
982 )}
983 </div>
984 );
985 };
986
987 export default Destinations;
988