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 / Activities.tsx

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

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