PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / Destinations.tsx

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

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