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

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

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