PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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.2.7, at resources/js/pages/AdditionalServices.tsx

863 lines 31.6 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 { Button } from "../components/ui/button";
32 import { PageHeader } from "../components/common/PageHeader";
33 import {
34 Edit,
35 Trash2,
36 Package,
37 CheckCircle,
38 FileText,
39 RotateCcw,
40 Plus,
41 } from "lucide-react";
42 import PremiumUpgradeCard from "./premium-pages/AdditionalServices";
43
44 // Types
45 interface TripInfo {
46 id: number;
47 title: string;
48 }
49
50 interface AdditionalService {
51 id: number;
52 name: string;
53 description: string;
54 price: number;
55 price_type: "fixed" | "percentage";
56 price_per: "person" | "booking" | "day";
57 icon?: IconPickerValue | null;
58 status: "publish" | "draft" | "trash";
59 sort_order: number;
60 applicable_to?: "all" | "specific_trips";
61 trip_ids?: number[];
62 trips?: TripInfo[];
63 trips_count: number;
64 bookings_count: number;
65 total_revenue: number;
66 is_required?: boolean | number | string;
67 created_at: string;
68 updated_at: string;
69 }
70
71 interface ServicesResponse {
72 data: AdditionalService[];
73 total: number;
74 page: number;
75 per_page: number;
76 }
77
78 // Check if module is available (Pro active)
79 const isModuleAvailable = (): boolean => {
80 const yatraAdmin = (window as any)?.yatraAdmin;
81 return Boolean(yatraAdmin?.isPro);
82 };
83
84 // Main Component
85 const AdditionalServices: React.FC = () => {
86 const moduleAvailable = isModuleAvailable();
87
88 // Show premium upgrade content if module is not available
89 if (!moduleAvailable) {
90 return <PremiumUpgradeCard />;
91 }
92
93 const [searchTerm, setSearchTerm] = useState("");
94 const [statusFilter, setStatusFilter] = useState("all");
95 const [sortBy, setSortBy] = useState("name");
96 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
97 const [page, setPage] = useState(1);
98 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
99 const [bulkAction, setBulkAction] = useState("");
100 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
101 const [permanentDeleteConfirm, setPermanentDeleteConfirm] = useState<{
102 isOpen: boolean;
103 service: AdditionalService | null;
104 }>({
105 isOpen: false,
106 service: null,
107 });
108
109 // Column visibility state with localStorage
110 const [visibleColumns, setVisibleColumns] = useState(() => {
111 const saved = localStorage.getItem("yatra-additional-services-columns");
112 return saved
113 ? JSON.parse(saved)
114 : {
115 name: true,
116 price: true,
117 price_type: true,
118 price_per: true,
119 trips: true,
120 status: true,
121 created_at: false,
122 updated_at: false,
123 };
124 });
125
126 const { showToast } = useToast();
127 const queryClient = useQueryClient();
128 const { can } = usePermissions();
129
130 // Toggle column visibility
131 const toggleColumn = (columnKey: string) => {
132 const newVisibleColumns = {
133 ...visibleColumns,
134 [columnKey]: !visibleColumns[columnKey],
135 };
136 setVisibleColumns(newVisibleColumns);
137 localStorage.setItem(
138 "yatra-additional-services-columns",
139 JSON.stringify(newVisibleColumns),
140 );
141 };
142
143 // Build query params
144 const queryParams = useMemo(() => {
145 const params: Record<string, any> = {
146 page,
147 per_page: 10,
148 orderby: sortBy,
149 order: sortOrder,
150 };
151 if (searchTerm) params.search = searchTerm;
152 if (statusFilter !== "all") params.status = statusFilter;
153 return params;
154 }, [searchTerm, statusFilter, sortBy, sortOrder, page]);
155
156 // Fetch status counts
157 const { data: statsData } = useQuery({
158 queryKey: ["additional-services-stats"],
159 queryFn: async () => {
160 try {
161 const response = await apiClient.get("/additional-services/stats");
162 return response;
163 } catch {
164 return { all: 0, publish: 0, draft: 0, trash: 0 };
165 }
166 },
167 enabled: isModuleAvailable(),
168 });
169
170 // Fetch services
171 const { data, isLoading, error } = useQuery<ServicesResponse>({
172 queryKey: ["additional-services", queryParams],
173 queryFn: async () => {
174 const response = await apiClient.get("/additional-services", {
175 params: queryParams,
176 });
177 return response;
178 },
179 enabled: isModuleAvailable(),
180 });
181
182 const services = data?.data || [];
183 const total = data?.total || 0;
184 const totalPages = Math.ceil(total / 10);
185
186 // Status counts
187 const statusCounts = useMemo(() => {
188 if (statsData) {
189 return {
190 all: statsData.all ?? 0,
191 publish: statsData.publish ?? 0,
192 draft: statsData.draft ?? 0,
193 trash: statsData.trash ?? 0,
194 };
195 }
196 return { all: 0, publish: 0, draft: 0, trash: 0 };
197 }, [statsData]);
198
199 // Bulk actions mutation
200 const bulkMutation = useMutation({
201 mutationFn: async ({
202 action,
203 ids,
204 }: {
205 action: string;
206 ids: (string | number)[];
207 }) => {
208 return await apiClient.post("/additional-services/bulk", { action, ids });
209 },
210 onSuccess: () => {
211 queryClient.invalidateQueries({ queryKey: ["additional-services"] });
212 queryClient.invalidateQueries({
213 queryKey: ["additional-services-stats"],
214 });
215 showToast(__("Action completed successfully"), "success");
216 setSelectedIds([]);
217 setBulkAction("");
218 },
219 onError: () => {
220 showToast(__("Failed to perform action"), "error");
221 },
222 });
223
224 const navigateToForm = (action: "create" | "edit", id?: number) => {
225 const params = new URLSearchParams(window.location.search);
226 params.set("subpage", "trips");
227 params.set("tab", "additional-services");
228 params.set("action", action);
229 if (id) params.set("id", id.toString());
230 window.history.pushState({}, "", `${window.location.pathname}?${params}`);
231 window.dispatchEvent(new PopStateEvent("popstate"));
232 };
233
234 const formatPrice = (
235 price: number | string,
236 priceType: string,
237 pricePer: string,
238 ) => {
239 const currency = (window as any)?.yatraAdmin?.currency || "USD";
240 const symbol = currency === "USD" ? "$" : currency;
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 `${symbol}${numPrice.toFixed(2)}${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 className="w-5 h-5 text-purple-600 dark:text-purple-400"
508 />
509 )
510 ) : (
511 <Package className="w-5 h-5 text-purple-600 dark:text-purple-400" />
512 )}
513 </div>
514 {/* Text */}
515 <div>
516 <div className="flex items-center gap-2">
517 <a
518 href="#"
519 onClick={(e) => {
520 e.preventDefault();
521 handleEdit(service);
522 }}
523 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"
524 >
525 {service.name}
526 </a>
527 {(service.is_required === true ||
528 service.is_required === 1 ||
529 service.is_required === "1") && (
530 <Badge
531 variant="outline"
532 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"
533 >
534 {__("Required")}
535 </Badge>
536 )}
537 </div>
538 <div className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
539 <span className="text-[11px] text-gray-400 dark:text-gray-500">
540 ({__("ID:", "yatra")} {service.id})
541 </span>
542 </div>
543 </div>
544 </div>
545 ),
546 },
547 {
548 key: "price",
549 label: __("Price"),
550 sortable: true,
551 visible: visibleColumns.price,
552 render: (service: AdditionalService) => (
553 <span
554 className={
555 service.status === "trash"
556 ? "text-gray-400 dark:text-gray-600"
557 : "font-medium text-gray-900 dark:text-white"
558 }
559 >
560 {formatPrice(
561 service.price,
562 service.price_type,
563 service.price_per,
564 )}
565 </span>
566 ),
567 },
568 {
569 key: "price_type",
570 label: __("Price Type"),
571 sortable: true,
572 visible: visibleColumns.price_type,
573 render: (service: AdditionalService) => (
574 <span
575 className={
576 service.status === "trash"
577 ? "text-gray-400 dark:text-gray-600"
578 : "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"
579 }
580 >
581 {service.price_type === "fixed"
582 ? __("Fixed")
583 : __("Percentage")}
584 </span>
585 ),
586 },
587 {
588 key: "price_per",
589 label: __("Price Per"),
590 sortable: true,
591 visible: visibleColumns.price_per,
592 render: (service: AdditionalService) => (
593 <span
594 className={
595 service.status === "trash"
596 ? "text-gray-400 dark:text-gray-600"
597 : "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"
598 }
599 >
600 {{
601 person: __("Person"),
602 booking: __("Booking"),
603 day: __("Day"),
604 }[service.price_per] || service.price_per}
605 </span>
606 ),
607 },
608 {
609 key: "trips",
610 label: __("Trips"),
611 sortable: false,
612 visible: visibleColumns.trips,
613 render: (service: AdditionalService) => (
614 <div className="flex flex-col gap-1">
615 {service.applicable_to === "all" ? (
616 <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">
617 {__("All Trips")}
618 </span>
619 ) : service.trips && service.trips.length > 0 ? (
620 <div className="flex flex-wrap gap-1">
621 {service.trips.slice(0, 2).map((trip) => (
622 <span
623 key={trip.id}
624 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"
625 title={trip.title}
626 >
627 {trip.title}
628 </span>
629 ))}
630 {service.trips.length > 2 && (
631 <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">
632 +{service.trips.length - 2} {__("more")}
633 </span>
634 )}
635 </div>
636 ) : (
637 <span className="text-gray-400 dark:text-gray-600 text-xs">
638 {__("No trips assigned")}
639 </span>
640 )}
641 </div>
642 ),
643 },
644 {
645 key: "status",
646 label: __("Status"),
647 sortable: true,
648 visible: visibleColumns.status,
649 render: (service: AdditionalService) => (
650 <span
651 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
652 service.status === "trash"
653 ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
654 : service.status === "publish"
655 ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
656 : "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
657 }`}
658 >
659 {service.status === "trash"
660 ? __("Trash")
661 : service.status === "publish"
662 ? __("Published")
663 : __("Draft")}
664 </span>
665 ),
666 },
667 {
668 key: "created_at",
669 label: __("Created Date"),
670 sortable: true,
671 visible: visibleColumns.created_at,
672 render: (service: AdditionalService) => (
673 <span
674 className={
675 service.status === "trash"
676 ? "text-gray-400 dark:text-gray-600"
677 : "text-gray-600 dark:text-gray-400"
678 }
679 >
680 {new Date(service.created_at).toLocaleDateString(
681 "en-US",
682 {
683 year: "numeric",
684 month: "short",
685 day: "numeric",
686 },
687 )}
688 </span>
689 ),
690 },
691 {
692 key: "updated_at",
693 label: __("Updated Date"),
694 sortable: true,
695 visible: visibleColumns.updated_at,
696 render: (service: AdditionalService) => (
697 <span
698 className={
699 service.status === "trash"
700 ? "text-gray-400 dark:text-gray-600"
701 : "text-gray-600 dark:text-gray-400"
702 }
703 >
704 {new Date(service.updated_at).toLocaleDateString(
705 "en-US",
706 {
707 year: "numeric",
708 month: "short",
709 day: "numeric",
710 },
711 )}
712 </span>
713 ),
714 },
715 ]}
716 actions={[
717 {
718 key: "edit",
719 label: __("Edit"),
720 icon: <Edit className="w-4 h-4" />,
721 onClick: handleEdit,
722 condition: () => can("yatra_edit_trips"),
723 },
724 {
725 key: "publish",
726 label: __("Publish"),
727 icon: <CheckCircle className="w-4 h-4" />,
728 onClick: (service: AdditionalService) => {
729 bulkMutation.mutate({
730 action: "publish",
731 ids: [service.id],
732 });
733 },
734 condition: (service: AdditionalService) =>
735 service.status !== "publish" &&
736 service.status !== "trash" &&
737 can("yatra_edit_trips"),
738 },
739 {
740 key: "draft",
741 label: __("Set as Draft"),
742 icon: <FileText className="w-4 h-4" />,
743 onClick: (service: AdditionalService) => {
744 bulkMutation.mutate({
745 action: "draft",
746 ids: [service.id],
747 });
748 },
749 condition: (service: AdditionalService) =>
750 service.status !== "draft" &&
751 service.status !== "trash" &&
752 can("yatra_edit_trips"),
753 },
754 {
755 key: "restore",
756 label: __("Restore"),
757 icon: <RotateCcw className="w-4 h-4" />,
758 onClick: (service: AdditionalService) => {
759 bulkMutation.mutate({
760 action: "restore",
761 ids: [service.id],
762 });
763 },
764 condition: (service: AdditionalService) =>
765 (service.status === "trash" ||
766 statusFilter === "trash") &&
767 can("yatra_edit_trips"),
768 },
769 {
770 key: "trash",
771 label: __("Move to Trash"),
772 icon: <Trash2 className="w-4 h-4" />,
773 onClick: (service: AdditionalService) => {
774 bulkMutation.mutate({
775 action: "trash",
776 ids: [service.id],
777 });
778 },
779 condition: (service: AdditionalService) =>
780 service.status !== "trash" &&
781 statusFilter !== "trash" &&
782 can("yatra_edit_trips"),
783 },
784 {
785 key: "delete",
786 label: __("Delete Permanently"),
787 icon: <Trash2 className="w-4 h-4" />,
788 onClick: handlePermanentDelete,
789 condition: (service: AdditionalService) =>
790 (service.status === "trash" ||
791 statusFilter === "trash") &&
792 can("yatra_delete_trips"),
793 variant: "destructive",
794 },
795 ]}
796 isLoading={isLoading}
797 isError={!!error}
798 errorText={__("Error loading services")}
799 emptyText={__("No services found")}
800 emptyDescription={
801 hasFilters
802 ? __("Try adjusting your filters to see more results.")
803 : __("Get started by creating your first service.")
804 }
805 onCreateClick={
806 can("yatra_edit_trips")
807 ? () => navigateToForm("create")
808 : undefined
809 }
810 onSort={handleSort}
811 getSortIcon={getSortIcon}
812 selectedItemIds={selectedIds}
813 onSelectItem={(id: string | number, checked: boolean) => {
814 if (checked) {
815 setSelectedIds([...selectedIds, id]);
816 } else {
817 setSelectedIds(
818 selectedIds.filter((selectedId) => selectedId !== id),
819 );
820 }
821 }}
822 onSelectAll={(checked: boolean) => {
823 if (checked) {
824 setSelectedIds(
825 services.map((s: AdditionalService) => s.id),
826 );
827 } else {
828 setSelectedIds([]);
829 }
830 }}
831 isAllSelected={
832 services.length > 0 &&
833 selectedIds.length === services.length
834 }
835 getItemId={(service: AdditionalService) => service.id}
836 getItemStatus={(service: AdditionalService) => service.status}
837 statusFilter={statusFilter}
838 skeletonRows={5}
839 />
840 </CardContent>
841 </Card>
842
843 {total > 0 && (
844 <div className="mt-4">
845 <Pagination
846 currentPage={page}
847 totalPages={totalPages}
848 totalItems={total}
849 itemsPerPage={10}
850 onPageChange={(newPage) => setPage(newPage)}
851 itemName={__("services")}
852 />
853 </div>
854 )}
855 </>
856 )}
857 </ConditionalRender>
858 </div>
859 );
860 };
861
862 export default AdditionalServices;
863