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

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

864 lines 31.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Additional Services Page
3 *
4 * Premium module for managing trip add-ons and extras.
5 * This page only loads when Yatra Pro is active and module is enabled.
6 * Premium upgrade content is handled by premium-pages/AdditionalServices.tsx
7 *
8 * @package Yatra
9 * @since 3.0.0
10 */
11
12 import React, { useState, useMemo, useEffect } from "react";
13 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
14 import { Card, CardContent } from "../components/ui/card";
15 import { Badge } from "../components/ui/badge";
16 import {
17 Pagination,
18 SearchFilterToolbar,
19 BulkActionToolbar,
20 Table as SharedTable,
21 } from "../components/shared";
22 import { getDefaultBulkStatusOptions } from "../components/shared/bulkStatusOptions";
23 import { usePermissions } from "../hooks/usePermissions";
24 import { IconSelector } from "../components/ui/icon-selector";
25 import type { IconPickerValue } from "../components/ui/icon-picker";
26 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
27 import { ConditionalRender } from "../components/ui/conditional-render";
28 import { useToast } from "../components/ui/toast";
29 import { apiClient } from "../lib/api-client";
30 import { __ } from "../lib/i18n";
31 import { formatYatraMoney } from "../lib/currency-display";
32 import { Button } from "../components/ui/button";
33 import { PageHeader } from "../components/common/PageHeader";
34 import {
35 Edit,
36 Trash2,
37 Package,
38 CheckCircle,
39 FileText,
40 RotateCcw,
41 Plus,
42 } from "lucide-react";
43 import PremiumUpgradeCard from "./premium-pages/AdditionalServices";
44
45 // Types
46 interface TripInfo {
47 id: number;
48 title: string;
49 }
50
51 interface AdditionalService {
52 id: number;
53 name: string;
54 description: string;
55 price: number;
56 price_type: "fixed" | "percentage";
57 price_per: "person" | "booking" | "day";
58 icon?: IconPickerValue | null;
59 status: "publish" | "draft" | "trash";
60 sort_order: number;
61 applicable_to?: "all" | "specific_trips";
62 trip_ids?: number[];
63 trips?: TripInfo[];
64 trips_count: number;
65 bookings_count: number;
66 total_revenue: number;
67 is_required?: boolean | number | string;
68 created_at: string;
69 updated_at: string;
70 }
71
72 interface ServicesResponse {
73 data: AdditionalService[];
74 total: number;
75 page: number;
76 per_page: number;
77 }
78
79 // Check if module is available (Pro active)
80 const isModuleAvailable = (): boolean => {
81 const yatraAdmin = (window as any)?.yatraAdmin;
82 return Boolean(yatraAdmin?.isPro);
83 };
84
85 // Main Component
86 const AdditionalServices: React.FC = () => {
87 const moduleAvailable = isModuleAvailable();
88
89 // Show premium upgrade content if module is not available
90 if (!moduleAvailable) {
91 return <PremiumUpgradeCard />;
92 }
93
94 const [searchTerm, setSearchTerm] = useState("");
95 const [statusFilter, setStatusFilter] = useState("all");
96 const [sortBy, setSortBy] = useState("name");
97 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
98 const [page, setPage] = useState(1);
99 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
100 const [bulkAction, setBulkAction] = useState("");
101 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
102 const [permanentDeleteConfirm, setPermanentDeleteConfirm] = useState<{
103 isOpen: boolean;
104 service: AdditionalService | null;
105 }>({
106 isOpen: false,
107 service: null,
108 });
109
110 // Column visibility state with localStorage
111 const [visibleColumns, setVisibleColumns] = useState(() => {
112 const saved = localStorage.getItem("yatra-additional-services-columns");
113 return saved
114 ? JSON.parse(saved)
115 : {
116 name: true,
117 price: true,
118 price_type: true,
119 price_per: true,
120 trips: true,
121 status: true,
122 created_at: false,
123 updated_at: false,
124 };
125 });
126
127 const { showToast } = useToast();
128 const queryClient = useQueryClient();
129 const { can } = usePermissions();
130
131 // Toggle column visibility
132 const toggleColumn = (columnKey: string) => {
133 const newVisibleColumns = {
134 ...visibleColumns,
135 [columnKey]: !visibleColumns[columnKey],
136 };
137 setVisibleColumns(newVisibleColumns);
138 localStorage.setItem(
139 "yatra-additional-services-columns",
140 JSON.stringify(newVisibleColumns),
141 );
142 };
143
144 // Build query params
145 const queryParams = useMemo(() => {
146 const params: Record<string, any> = {
147 page,
148 per_page: 10,
149 orderby: sortBy,
150 order: sortOrder,
151 };
152 if (searchTerm) params.search = searchTerm;
153 if (statusFilter !== "all") params.status = statusFilter;
154 return params;
155 }, [searchTerm, statusFilter, sortBy, sortOrder, page]);
156
157 // Fetch status counts
158 const { data: statsData } = useQuery({
159 queryKey: ["additional-services-stats"],
160 queryFn: async () => {
161 try {
162 const response = await apiClient.get("/additional-services/stats");
163 return response;
164 } catch {
165 return { all: 0, publish: 0, draft: 0, trash: 0 };
166 }
167 },
168 enabled: isModuleAvailable(),
169 });
170
171 // Fetch services
172 const { data, isLoading, error } = useQuery<ServicesResponse>({
173 queryKey: ["additional-services", queryParams],
174 queryFn: async () => {
175 const response = await apiClient.get("/additional-services", {
176 params: queryParams,
177 });
178 return response;
179 },
180 enabled: isModuleAvailable(),
181 });
182
183 const services = data?.data || [];
184 const total = data?.total || 0;
185 const totalPages = Math.ceil(total / 10);
186
187 // Status counts
188 const statusCounts = useMemo(() => {
189 if (statsData) {
190 return {
191 all: statsData.all ?? 0,
192 publish: statsData.publish ?? 0,
193 draft: statsData.draft ?? 0,
194 trash: statsData.trash ?? 0,
195 };
196 }
197 return { all: 0, publish: 0, draft: 0, trash: 0 };
198 }, [statsData]);
199
200 // Bulk actions mutation
201 const bulkMutation = useMutation({
202 mutationFn: async ({
203 action,
204 ids,
205 }: {
206 action: string;
207 ids: (string | number)[];
208 }) => {
209 return await apiClient.post("/additional-services/bulk", { action, ids });
210 },
211 onSuccess: () => {
212 queryClient.invalidateQueries({ queryKey: ["additional-services"] });
213 queryClient.invalidateQueries({
214 queryKey: ["additional-services-stats"],
215 });
216 showToast(__("Action completed successfully"), "success");
217 setSelectedIds([]);
218 setBulkAction("");
219 },
220 onError: () => {
221 showToast(__("Failed to perform action"), "error");
222 },
223 });
224
225 const navigateToForm = (action: "create" | "edit", id?: number) => {
226 const params = new URLSearchParams(window.location.search);
227 params.set("subpage", "trips");
228 params.set("tab", "additional-services");
229 params.set("action", action);
230 if (id) params.set("id", id.toString());
231 window.history.pushState({}, "", `${window.location.pathname}?${params}`);
232 window.dispatchEvent(new PopStateEvent("popstate"));
233 };
234
235 const formatPrice = (
236 price: number | string,
237 priceType: string,
238 pricePer: string,
239 ) => {
240 const currency = (window as any)?.yatraAdmin?.currency || "USD";
241 const numPrice = Number(price) || 0;
242 if (priceType === "percentage") return `${numPrice}%`;
243 const perLabel =
244 { person: __("/person"), booking: __("/booking"), day: __("/day") }[
245 pricePer
246 ] || "";
247 return `${formatYatraMoney(numPrice, currency, { zeroAsUnknown: false })}${perLabel}`;
248 };
249
250 const handleEdit = (service: AdditionalService) => {
251 navigateToForm("edit", service.id);
252 };
253
254 const handlePermanentDelete = (service: AdditionalService) => {
255 setPermanentDeleteConfirm({ isOpen: true, service });
256 };
257
258 const confirmPermanentDelete = () => {
259 if (permanentDeleteConfirm.service) {
260 bulkMutation.mutate({
261 action: "delete",
262 ids: [permanentDeleteConfirm.service.id],
263 });
264 setPermanentDeleteConfirm({ isOpen: false, service: null });
265 }
266 };
267
268 const handleSort = (field: string) => {
269 if (sortBy === field) {
270 setSortOrder(sortOrder === "asc" ? "desc" : "asc");
271 } else {
272 setSortBy(field);
273 setSortOrder("asc");
274 }
275 };
276
277 const getSortIcon = (field: string) => {
278 if (sortBy !== field) {
279 return null;
280 }
281 return sortOrder === "asc" ? "↑" : "↓";
282 };
283
284 const handleBulkApply = () => {
285 if (!bulkAction) {
286 showToast(__("Select a bulk action first."), "warning");
287 return;
288 }
289 if (selectedIds.length === 0) {
290 showToast(__("Select at least one service."), "warning");
291 return;
292 }
293 bulkMutation.mutate({ action: bulkAction, ids: selectedIds });
294 };
295
296 const handleResetFilters = () => {
297 setSearchTerm("");
298 setStatusFilter("all");
299 setSortBy("name");
300 setSortOrder("asc");
301 setPage(1);
302 setSelectedIds([]);
303 setBulkAction("");
304 };
305
306 const hasFilters =
307 searchTerm ||
308 statusFilter !== "all" ||
309 sortBy !== "name" ||
310 sortOrder !== "asc";
311
312 // Keep selection in sync with current page data
313 useEffect(() => {
314 setSelectedIds((prev) =>
315 prev.filter((id) => services.some((s: AdditionalService) => s.id === id)),
316 );
317 }, [services]);
318
319 const viewFilters = [
320 { key: "all", label: __("All"), count: statusCounts.all ?? 0 },
321 {
322 key: "publish",
323 label: __("Published"),
324 count: statusCounts.publish ?? 0,
325 },
326 { key: "draft", label: __("Draft"), count: statusCounts.draft ?? 0 },
327 { key: "trash", label: __("Trash"), count: statusCounts.trash ?? 0 },
328 ];
329
330 return (
331 <div className="space-y-3">
332 <PageHeader
333 title={__("Additional Services", "yatra")}
334 description={__(
335 "Manage add-ons and extras that customers can select when booking",
336 "yatra",
337 )}
338 actionCapability="yatra_edit_trips"
339 actions={
340 <Button
341 onClick={() => navigateToForm("create")}
342 className="flex items-center gap-2"
343 >
344 <Plus className="w-4 h-4" />
345 {__("Add New Service", "yatra")}
346 </Button>
347 }
348 />
349
350 {/* Permanent Delete Confirmation Dialog */}
351 <ConfirmationDialog
352 isOpen={permanentDeleteConfirm.isOpen}
353 onClose={() =>
354 setPermanentDeleteConfirm({ isOpen: false, service: null })
355 }
356 onConfirm={confirmPermanentDelete}
357 title={__("Delete Service Permanently")}
358 message={
359 permanentDeleteConfirm.service
360 ? __(
361 'Are you sure you want to permanently delete "{name}"? This action cannot be undone.',
362 ).replace("{name}", permanentDeleteConfirm.service.name)
363 : __("Are you sure you want to permanently delete this service?")
364 }
365 confirmText={__("Delete Permanently")}
366 cancelText={__("Cancel")}
367 variant="danger"
368 isLoading={bulkMutation.isPending}
369 />
370
371 {/* Filters Card */}
372 <Card>
373 <CardContent className="p-3">
374 <SearchFilterToolbar
375 searchTerm={searchTerm}
376 onSearchChange={setSearchTerm}
377 statusFilter={statusFilter}
378 onStatusChange={(value) => {
379 setStatusFilter(value);
380 setPage(1);
381 setSelectedIds([]);
382 setBulkAction("");
383 }}
384 statusOptions={[
385 { value: "all", label: __("All Status") },
386 { value: "publish", label: __("Published") },
387 { value: "draft", label: __("Draft") },
388 { value: "trash", label: __("Trash") },
389 ]}
390 sortBy={sortBy}
391 onSortByChange={setSortBy}
392 sortOrder={sortOrder}
393 onSortOrderChange={setSortOrder}
394 sortOptions={[
395 { value: "name", label: __("Name") },
396 { value: "price", label: __("Price") },
397 { value: "price_type", label: __("Price Type") },
398 { value: "price_per", label: __("Price Per") },
399 { value: "status", label: __("Status") },
400 { value: "created_at", label: __("Created At") },
401 { value: "updated_at", label: __("Updated At") },
402 ]}
403 onResetFilters={handleResetFilters}
404 hasFilters={!!hasFilters}
405 placeholder={__("Search services...")}
406 />
407 </CardContent>
408 </Card>
409
410 <ConditionalRender capability="yatra_view_trips">
411 {error ? (
412 <Card>
413 <CardContent className="p-8 text-center text-red-500">
414 {__("Error loading services")}
415 </CardContent>
416 </Card>
417 ) : (
418 <>
419 <BulkActionToolbar
420 selectedIds={selectedIds}
421 bulkAction={bulkAction}
422 setBulkAction={setBulkAction}
423 onApply={handleBulkApply}
424 onClearSelection={() => setSelectedIds([])}
425 statusFilter={statusFilter}
426 setStatusFilter={(value) => {
427 setStatusFilter(value);
428 setPage(1);
429 setSelectedIds([]);
430 setBulkAction("");
431 }}
432 statusOptions={viewFilters}
433 showColumnsDropdown={showColumnsDropdown}
434 setShowColumnsDropdown={setShowColumnsDropdown}
435 columnOptions={[
436 {
437 key: "name",
438 label: __("Service"),
439 visible: visibleColumns.name,
440 },
441 {
442 key: "price",
443 label: __("Price"),
444 visible: visibleColumns.price,
445 },
446 {
447 key: "price_type",
448 label: __("Price Type"),
449 visible: visibleColumns.price_type,
450 },
451 {
452 key: "price_per",
453 label: __("Price Per"),
454 visible: visibleColumns.price_per,
455 },
456 {
457 key: "trips",
458 label: __("Trips"),
459 visible: visibleColumns.trips,
460 },
461 {
462 key: "status",
463 label: __("Status"),
464 visible: visibleColumns.status,
465 },
466 {
467 key: "created_at",
468 label: __("Created Date"),
469 visible: visibleColumns.created_at,
470 },
471 {
472 key: "updated_at",
473 label: __("Updated Date"),
474 visible: visibleColumns.updated_at,
475 },
476 ]}
477 onToggleColumn={toggleColumn}
478 bulkMutationPending={bulkMutation.isPending}
479 totalItems={services.length}
480 bulkActionOptions={getDefaultBulkStatusOptions(statusFilter)}
481 />
482
483 <Card className="overflow-visible">
484 <CardContent className="p-0 overflow-visible">
485 <SharedTable
486 data={services}
487 columns={[
488 {
489 key: "name",
490 label: __("Service"),
491 sortable: true,
492 visible: visibleColumns.name,
493 render: (service: AdditionalService) => (
494 <div className="flex items-center gap-3">
495 {/* Icon/Image */}
496 <div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0">
497 {service.icon ? (
498 service.icon.type === "image" ? (
499 <img
500 src={service.icon.value}
501 alt={service.name}
502 className="w-full h-full object-cover rounded-lg"
503 />
504 ) : (
505 <IconSelector
506 iconName={service.icon.value}
507 provider={service.icon.provider ?? "yatra"}
508 className="w-5 h-5 text-purple-600 dark:text-purple-400"
509 />
510 )
511 ) : (
512 <Package className="w-5 h-5 text-purple-600 dark:text-purple-400" />
513 )}
514 </div>
515 {/* Text */}
516 <div>
517 <div className="flex items-center gap-2">
518 <a
519 href="#"
520 onClick={(e) => {
521 e.preventDefault();
522 handleEdit(service);
523 }}
524 className="font-medium text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 hover:underline transition-colors cursor-pointer"
525 >
526 {service.name}
527 </a>
528 {(service.is_required === true ||
529 service.is_required === 1 ||
530 service.is_required === "1") && (
531 <Badge
532 variant="outline"
533 className="text-xs bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-700"
534 >
535 {__("Required")}
536 </Badge>
537 )}
538 </div>
539 <div className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
540 <span className="text-[11px] text-gray-400 dark:text-gray-500">
541 ({__("ID:", "yatra")} {service.id})
542 </span>
543 </div>
544 </div>
545 </div>
546 ),
547 },
548 {
549 key: "price",
550 label: __("Price"),
551 sortable: true,
552 visible: visibleColumns.price,
553 render: (service: AdditionalService) => (
554 <span
555 className={
556 service.status === "trash"
557 ? "text-gray-400 dark:text-gray-600"
558 : "font-medium text-gray-900 dark:text-white"
559 }
560 >
561 {formatPrice(
562 service.price,
563 service.price_type,
564 service.price_per,
565 )}
566 </span>
567 ),
568 },
569 {
570 key: "price_type",
571 label: __("Price Type"),
572 sortable: true,
573 visible: visibleColumns.price_type,
574 render: (service: AdditionalService) => (
575 <span
576 className={
577 service.status === "trash"
578 ? "text-gray-400 dark:text-gray-600"
579 : "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300"
580 }
581 >
582 {service.price_type === "fixed"
583 ? __("Fixed")
584 : __("Percentage")}
585 </span>
586 ),
587 },
588 {
589 key: "price_per",
590 label: __("Price Per"),
591 sortable: true,
592 visible: visibleColumns.price_per,
593 render: (service: AdditionalService) => (
594 <span
595 className={
596 service.status === "trash"
597 ? "text-gray-400 dark:text-gray-600"
598 : "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"
599 }
600 >
601 {{
602 person: __("Person"),
603 booking: __("Booking"),
604 day: __("Day"),
605 }[service.price_per] || service.price_per}
606 </span>
607 ),
608 },
609 {
610 key: "trips",
611 label: __("Trips"),
612 sortable: false,
613 visible: visibleColumns.trips,
614 render: (service: AdditionalService) => (
615 <div className="flex flex-col gap-1">
616 {service.applicable_to === "all" ? (
617 <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">
618 {__("All Trips")}
619 </span>
620 ) : service.trips && service.trips.length > 0 ? (
621 <div className="flex flex-wrap gap-1">
622 {service.trips.slice(0, 2).map((trip) => (
623 <span
624 key={trip.id}
625 className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 max-w-[120px] truncate"
626 title={trip.title}
627 >
628 {trip.title}
629 </span>
630 ))}
631 {service.trips.length > 2 && (
632 <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
633 +{service.trips.length - 2} {__("more")}
634 </span>
635 )}
636 </div>
637 ) : (
638 <span className="text-gray-400 dark:text-gray-600 text-xs">
639 {__("No trips assigned")}
640 </span>
641 )}
642 </div>
643 ),
644 },
645 {
646 key: "status",
647 label: __("Status"),
648 sortable: true,
649 visible: visibleColumns.status,
650 render: (service: AdditionalService) => (
651 <span
652 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
653 service.status === "trash"
654 ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
655 : service.status === "publish"
656 ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
657 : "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
658 }`}
659 >
660 {service.status === "trash"
661 ? __("Trash")
662 : service.status === "publish"
663 ? __("Published")
664 : __("Draft")}
665 </span>
666 ),
667 },
668 {
669 key: "created_at",
670 label: __("Created Date"),
671 sortable: true,
672 visible: visibleColumns.created_at,
673 render: (service: AdditionalService) => (
674 <span
675 className={
676 service.status === "trash"
677 ? "text-gray-400 dark:text-gray-600"
678 : "text-gray-600 dark:text-gray-400"
679 }
680 >
681 {new Date(service.created_at).toLocaleDateString(
682 "en-US",
683 {
684 year: "numeric",
685 month: "short",
686 day: "numeric",
687 },
688 )}
689 </span>
690 ),
691 },
692 {
693 key: "updated_at",
694 label: __("Updated Date"),
695 sortable: true,
696 visible: visibleColumns.updated_at,
697 render: (service: AdditionalService) => (
698 <span
699 className={
700 service.status === "trash"
701 ? "text-gray-400 dark:text-gray-600"
702 : "text-gray-600 dark:text-gray-400"
703 }
704 >
705 {new Date(service.updated_at).toLocaleDateString(
706 "en-US",
707 {
708 year: "numeric",
709 month: "short",
710 day: "numeric",
711 },
712 )}
713 </span>
714 ),
715 },
716 ]}
717 actions={[
718 {
719 key: "edit",
720 label: __("Edit"),
721 icon: <Edit className="w-4 h-4" />,
722 onClick: handleEdit,
723 condition: () => can("yatra_edit_trips"),
724 },
725 {
726 key: "publish",
727 label: __("Publish"),
728 icon: <CheckCircle className="w-4 h-4" />,
729 onClick: (service: AdditionalService) => {
730 bulkMutation.mutate({
731 action: "publish",
732 ids: [service.id],
733 });
734 },
735 condition: (service: AdditionalService) =>
736 service.status !== "publish" &&
737 service.status !== "trash" &&
738 can("yatra_edit_trips"),
739 },
740 {
741 key: "draft",
742 label: __("Set as Draft"),
743 icon: <FileText className="w-4 h-4" />,
744 onClick: (service: AdditionalService) => {
745 bulkMutation.mutate({
746 action: "draft",
747 ids: [service.id],
748 });
749 },
750 condition: (service: AdditionalService) =>
751 service.status !== "draft" &&
752 service.status !== "trash" &&
753 can("yatra_edit_trips"),
754 },
755 {
756 key: "restore",
757 label: __("Restore"),
758 icon: <RotateCcw className="w-4 h-4" />,
759 onClick: (service: AdditionalService) => {
760 bulkMutation.mutate({
761 action: "restore",
762 ids: [service.id],
763 });
764 },
765 condition: (service: AdditionalService) =>
766 (service.status === "trash" ||
767 statusFilter === "trash") &&
768 can("yatra_edit_trips"),
769 },
770 {
771 key: "trash",
772 label: __("Move to Trash"),
773 icon: <Trash2 className="w-4 h-4" />,
774 onClick: (service: AdditionalService) => {
775 bulkMutation.mutate({
776 action: "trash",
777 ids: [service.id],
778 });
779 },
780 condition: (service: AdditionalService) =>
781 service.status !== "trash" &&
782 statusFilter !== "trash" &&
783 can("yatra_edit_trips"),
784 },
785 {
786 key: "delete",
787 label: __("Delete Permanently"),
788 icon: <Trash2 className="w-4 h-4" />,
789 onClick: handlePermanentDelete,
790 condition: (service: AdditionalService) =>
791 (service.status === "trash" ||
792 statusFilter === "trash") &&
793 can("yatra_delete_trips"),
794 variant: "destructive",
795 },
796 ]}
797 isLoading={isLoading}
798 isError={!!error}
799 errorText={__("Error loading services")}
800 emptyText={__("No services found")}
801 emptyDescription={
802 hasFilters
803 ? __("Try adjusting your filters to see more results.")
804 : __("Get started by creating your first service.")
805 }
806 onCreateClick={
807 can("yatra_edit_trips")
808 ? () => navigateToForm("create")
809 : undefined
810 }
811 onSort={handleSort}
812 getSortIcon={getSortIcon}
813 selectedItemIds={selectedIds}
814 onSelectItem={(id: string | number, checked: boolean) => {
815 if (checked) {
816 setSelectedIds([...selectedIds, id]);
817 } else {
818 setSelectedIds(
819 selectedIds.filter((selectedId) => selectedId !== id),
820 );
821 }
822 }}
823 onSelectAll={(checked: boolean) => {
824 if (checked) {
825 setSelectedIds(
826 services.map((s: AdditionalService) => s.id),
827 );
828 } else {
829 setSelectedIds([]);
830 }
831 }}
832 isAllSelected={
833 services.length > 0 &&
834 selectedIds.length === services.length
835 }
836 getItemId={(service: AdditionalService) => service.id}
837 getItemStatus={(service: AdditionalService) => service.status}
838 statusFilter={statusFilter}
839 skeletonRows={5}
840 />
841 </CardContent>
842 </Card>
843
844 {total > 0 && (
845 <div className="mt-4">
846 <Pagination
847 currentPage={page}
848 totalPages={totalPages}
849 totalItems={total}
850 itemsPerPage={10}
851 onPageChange={(newPage) => setPage(newPage)}
852 itemName={__("services")}
853 />
854 </div>
855 )}
856 </>
857 )}
858 </ConditionalRender>
859 </div>
860 );
861 };
862
863 export default AdditionalServices;
864