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

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

769 lines 24.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Reviews Page
3 * Clean, minimal SaaS-style reviews management page
4 */
5
6 import React, { useState, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 Plus,
10 Search,
11 X,
12 ArrowUpDown,
13 ArrowUp,
14 ArrowDown,
15 Eye,
16 Edit,
17 Trash2,
18 Star,
19 } from "lucide-react";
20 import { __ } from "../lib/i18n";
21 import { formatDate as formatDateUtil } from "../lib/dateFormat";
22 import { usePermissions } from "../hooks/usePermissions";
23 import { apiClient } from "../lib/api-client";
24 import { apiService } from "../lib/api-client";
25 import { getErrorContext } from "../lib/errors";
26 import { Button } from "../components/ui/button";
27 import { Input } from "../components/ui/input";
28 import { Select } from "../components/ui/select";
29 import { PageHeader } from "../components/common/PageHeader";
30 import { Card, CardContent } from "../components/ui/card";
31 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
32 import {
33 Pagination,
34 Table as SharedTable,
35 BulkActionToolbar,
36 } from "../components/shared";
37
38 interface Review {
39 id: number;
40 trip_id: number;
41 trip_title: string;
42 trip_slug: string;
43 customer_name: string;
44 customer_email: string;
45 rating: number;
46 title: string;
47 content: string;
48 status: string;
49 verified: boolean;
50 created_at: string;
51 }
52
53 const Reviews: React.FC = () => {
54 const [searchTerm, setSearchTerm] = useState("");
55 const [statusFilter, setStatusFilter] = useState("all");
56 const [ratingFilter, setRatingFilter] = useState("all");
57 const [sortBy, setSortBy] = useState("created_at");
58 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
59 const [page, setPage] = useState(1);
60 const [deleteConfirm, setDeleteConfirm] = useState<{
61 isOpen: boolean;
62 review: Review | null;
63 }>({
64 isOpen: false,
65 review: null,
66 });
67 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
68 const [bulkAction, setBulkAction] = useState("");
69 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
70 const [visibleColumns, setVisibleColumns] = useState(() => {
71 if (typeof window === "undefined") {
72 return {
73 trip: true,
74 customer: true,
75 rating: true,
76 review: true,
77 status: true,
78 date: true,
79 };
80 }
81
82 const saved = window.localStorage.getItem("yatra-reviews-visible-columns");
83 return saved
84 ? JSON.parse(saved)
85 : {
86 trip: true,
87 customer: true,
88 rating: true,
89 review: true,
90 status: true,
91 date: true,
92 };
93 });
94 const queryClient = useQueryClient();
95 const { can } = usePermissions();
96
97 // Build query params
98 const queryParams = useMemo(() => {
99 const params: Record<string, any> = {
100 page,
101 per_page: 10,
102 orderby: sortBy,
103 order: sortOrder,
104 };
105
106 if (searchTerm) {
107 params.search = searchTerm;
108 }
109
110 if (statusFilter !== "all") {
111 params.status = statusFilter;
112 }
113
114 if (ratingFilter !== "all") {
115 params.rating = ratingFilter;
116 }
117
118 return params;
119 }, [searchTerm, statusFilter, ratingFilter, sortBy, sortOrder, page]);
120
121 // Fetch reviews from API
122 const { data, isLoading, error } = useQuery({
123 queryKey: ["reviews", queryParams],
124 queryFn: async () => {
125 const response = await apiClient.get("/reviews", { params: queryParams });
126 // Transform API response to match our interface
127 const reviews = (response?.data || []).map((item: any) => ({
128 id: item.id,
129 trip_id: item.trip_id,
130 trip_title: item.trip_title || "Unknown Trip",
131 trip_slug: item.trip_slug || "",
132 customer_name: item.customer_name || item.author_name || "Anonymous",
133 customer_email: item.customer_email || item.author_email || "",
134 rating: item.rating,
135 title: item.title || "",
136 comment: item.comment || item.content || "",
137 status: item.status || "pending",
138 verified: item.verified || false,
139 created_at: item.created_at || "",
140 }));
141
142 return {
143 data: reviews,
144 total: response?.total || 0,
145 page: response?.page || page,
146 per_page: response?.per_page || 10,
147 };
148 },
149 enabled: can("yatra_view_reviews"),
150 });
151
152 // Delete mutation
153 const deleteMutation = useMutation({
154 mutationFn: async (id: number) => {
155 return await apiClient.delete(`/reviews/${id}`);
156 },
157 onSuccess: () => {
158 queryClient.invalidateQueries({ queryKey: ["reviews"] });
159 },
160 });
161
162 const reviews: Review[] = data?.data || [];
163 const total = data?.total || 0;
164 const totalPages = Math.ceil(total / 10);
165 const errorContext = getErrorContext(error);
166
167 const formatDate = (dateString: string) => {
168 return formatDateUtil(dateString);
169 };
170
171 const renderStars = (rating: number) => {
172 return (
173 <div className="flex items-center gap-0.5">
174 {[1, 2, 3, 4, 5].map((star) => (
175 <Star
176 key={star}
177 className={`w-4 h-4 ${
178 star <= rating
179 ? "fill-yellow-400 text-yellow-400"
180 : "fill-gray-300 text-gray-300 dark:fill-gray-600 dark:text-gray-600"
181 }`}
182 />
183 ))}
184 <span className="ml-1 text-sm text-gray-600 dark:text-gray-400">
185 {rating}
186 </span>
187 </div>
188 );
189 };
190
191 const getStatusBadge = (status: string) => {
192 const statusMap: Record<string, { className: string; label: string }> = {
193 approved: {
194 className:
195 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
196 label: __("Approved", "yatra"),
197 },
198 pending: {
199 className:
200 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
201 label: __("Pending", "yatra"),
202 },
203 spam: {
204 className:
205 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
206 label: __("Spam", "yatra"),
207 },
208 trash: {
209 className:
210 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
211 label: __("Trash", "yatra"),
212 },
213 };
214
215 const statusInfo = statusMap[status] || {
216 className:
217 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
218 label: status,
219 };
220
221 return (
222 <span
223 className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${statusInfo.className}`}
224 >
225 {statusInfo.label}
226 </span>
227 );
228 };
229
230 const handleEdit = (review: Review) => {
231 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews&action=edit&id=${review.id}`;
232 };
233
234 const handleDelete = (review: Review) => {
235 setDeleteConfirm({ isOpen: true, review });
236 };
237
238 const confirmDelete = () => {
239 if (deleteConfirm.review) {
240 deleteMutation.mutate(deleteConfirm.review.id);
241 setDeleteConfirm({ isOpen: false, review: null });
242 }
243 };
244
245 const handleView = (review: Review) => {
246 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews&action=view&id=${review.id}`;
247 };
248
249 const handleCreateReview = () => {
250 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reviews&action=create`;
251 };
252
253 const handleResetFilters = () => {
254 setSearchTerm("");
255 setStatusFilter("all");
256 setRatingFilter("all");
257 setSortBy("created_at");
258 setSortOrder("desc");
259 setPage(1);
260 };
261
262 const handleSort = (field: string) => {
263 if (sortBy === field) {
264 setSortOrder(sortOrder === "asc" ? "desc" : "asc");
265 } else {
266 setSortBy(field);
267 setSortOrder("desc");
268 }
269 };
270
271 const getSortIcon = (field: string) => {
272 if (sortBy !== field) {
273 return <ArrowUpDown className="w-3.5 h-3.5 ml-1 text-gray-400" />;
274 }
275 return sortOrder === "asc" ? (
276 <ArrowUp className="w-3.5 h-3.5 ml-1 text-gray-600 dark:text-gray-300" />
277 ) : (
278 <ArrowDown className="w-3.5 h-3.5 ml-1 text-gray-600 dark:text-gray-300" />
279 );
280 };
281
282 const hasFilters =
283 searchTerm ||
284 statusFilter !== "all" ||
285 ratingFilter !== "all" ||
286 sortBy !== "created_at" ||
287 sortOrder !== "desc";
288
289 // Bulk action options depend on current status filter
290 const bulkActionOptions = useMemo(() => {
291 // In trash view, only allow permanent delete
292 if (statusFilter === "trash") {
293 return [{ value: "delete", label: __("Delete Permanently", "yatra") }];
294 }
295
296 // In spam view, allow move to trash or delete
297 if (statusFilter === "spam") {
298 return [
299 { value: "mark_trash", label: __("Move to Trash", "yatra") },
300 { value: "delete", label: __("Delete Permanently", "yatra") },
301 ];
302 }
303
304 // Default view: allow approve, pending, spam, trash, delete
305 return [
306 { value: "mark_approved", label: __("Mark as Approved", "yatra") },
307 { value: "mark_pending", label: __("Mark as Pending", "yatra") },
308 { value: "mark_spam", label: __("Mark as Spam", "yatra") },
309 { value: "mark_trash", label: __("Move to Trash", "yatra") },
310 { value: "delete", label: __("Delete Permanently", "yatra") },
311 ];
312 }, [statusFilter]);
313
314 // Bulk actions
315 const handleBulkApply = () => {
316 if (!bulkAction || selectedIds.length === 0) {
317 return;
318 }
319
320 const run = async () => {
321 try {
322 await apiService.bulkReviewsAction(bulkAction, selectedIds);
323
324 queryClient.invalidateQueries({ queryKey: ["reviews"] });
325 } catch (error) {
326 console.error("Bulk review action error", error);
327 } finally {
328 setSelectedIds([]);
329 setBulkAction("");
330 }
331 };
332
333 void run();
334 };
335
336 const toggleColumn = (columnKey: string) => {
337 const newVisible = {
338 ...visibleColumns,
339 [columnKey]: !visibleColumns[columnKey as keyof typeof visibleColumns],
340 };
341 setVisibleColumns(newVisible);
342 if (typeof window !== "undefined") {
343 window.localStorage.setItem(
344 "yatra-reviews-visible-columns",
345 JSON.stringify(newVisible),
346 );
347 }
348 };
349
350 const columnOptions = [
351 { key: "trip", label: __("Trip", "yatra"), visible: visibleColumns.trip },
352 {
353 key: "customer",
354 label: __("Customer", "yatra"),
355 visible: visibleColumns.customer,
356 },
357 {
358 key: "rating",
359 label: __("Rating", "yatra"),
360 visible: visibleColumns.rating,
361 },
362 {
363 key: "review",
364 label: __("Review", "yatra"),
365 visible: visibleColumns.review,
366 },
367 {
368 key: "status",
369 label: __("Status", "yatra"),
370 visible: visibleColumns.status,
371 },
372 { key: "date", label: __("Date", "yatra"), visible: visibleColumns.date },
373 ];
374
375 const handleApprove = async (review: Review) => {
376 await apiService.updateReviewStatus(review.id, "approved");
377 queryClient.invalidateQueries({ queryKey: ["reviews"] });
378 };
379
380 const handleMarkPending = async (review: Review) => {
381 await apiService.updateReviewStatus(review.id, "pending");
382 queryClient.invalidateQueries({ queryKey: ["reviews"] });
383 };
384
385 const handleMarkSpam = async (review: Review) => {
386 await apiService.updateReviewStatus(review.id, "spam");
387 queryClient.invalidateQueries({ queryKey: ["reviews"] });
388 };
389
390 const handleMarkTrash = async (review: Review) => {
391 await apiService.updateReviewStatus(review.id, "trash");
392 queryClient.invalidateQueries({ queryKey: ["reviews"] });
393 };
394
395 const actions = [
396 {
397 key: "view",
398 label: __("View", "yatra"),
399 icon: <Eye className="w-4 h-4" />,
400 onClick: (review: Review) => handleView(review),
401 condition: () => can("yatra_view_reviews"),
402 },
403 {
404 key: "approve",
405 label: __("Approve", "yatra"),
406 icon: <Star className="w-4 h-4" />,
407 onClick: (review: Review) => handleApprove(review),
408 condition: (review: Review) =>
409 can("yatra_edit_reviews") && review.status !== "approved",
410 },
411 {
412 key: "mark_pending",
413 label: __("Mark as Pending", "yatra"),
414 icon: <Edit className="w-4 h-4" />,
415 onClick: (review: Review) => handleMarkPending(review),
416 condition: (review: Review) =>
417 can("yatra_edit_reviews") && review.status !== "pending",
418 },
419 {
420 key: "edit",
421 label: __("Edit", "yatra"),
422 icon: <Edit className="w-4 h-4" />,
423 onClick: (review: Review) => handleEdit(review),
424 condition: () => can("yatra_edit_reviews"),
425 },
426 {
427 key: "mark_spam",
428 label: __("Mark as Spam", "yatra"),
429 icon: <Trash2 className="w-4 h-4" />,
430 onClick: (review: Review) => handleMarkSpam(review),
431 condition: (review: Review) =>
432 can("yatra_edit_reviews") && review.status !== "spam",
433 },
434 {
435 key: "mark_trash",
436 label: __("Move to Trash", "yatra"),
437 icon: <Trash2 className="w-4 h-4" />,
438 onClick: (review: Review) => handleMarkTrash(review),
439 condition: (review: Review) =>
440 can("yatra_edit_reviews") && review.status !== "trash",
441 },
442 {
443 key: "delete",
444 label: __("Delete Permanently", "yatra"),
445 icon: <Trash2 className="w-4 h-4" />,
446 onClick: (review: Review) => handleDelete(review),
447 variant: "destructive" as const,
448 condition: () => can("yatra_delete_reviews"),
449 },
450 ];
451
452 return (
453 <div className="space-y-3">
454 <ConfirmationDialog
455 isOpen={deleteConfirm.isOpen}
456 onClose={() => setDeleteConfirm({ isOpen: false, review: null })}
457 onConfirm={confirmDelete}
458 title={__("Delete Review", "yatra")}
459 message={
460 deleteConfirm.review
461 ? __(
462 'Are you sure you want to delete this review by "{name}"? This action cannot be undone.',
463 "yatra",
464 ).replace("{name}", deleteConfirm.review.customer_name)
465 : __(
466 "Are you sure you want to delete this review? This action cannot be undone.",
467 "yatra",
468 )
469 }
470 confirmText={__("Delete", "yatra")}
471 cancelText={__("Cancel", "yatra")}
472 variant="danger"
473 isLoading={deleteMutation.isPending}
474 />
475
476 <PageHeader
477 title={__("Reviews", "yatra")}
478 description={__("Manage customer reviews and ratings", "yatra")}
479 actionCapability="yatra_edit_reviews"
480 actions={
481 <Button
482 onClick={handleCreateReview}
483 className="flex items-center gap-2"
484 >
485 <Plus className="w-4 h-4" />
486 {__("Add New Review", "yatra")}
487 </Button>
488 }
489 />
490
491 {/* Filters, Search, and Sorting - Always Visible */}
492 <Card>
493 <CardContent className="p-3">
494 <div className="flex flex-col lg:flex-row gap-2 items-stretch lg:items-center">
495 {/* Search */}
496 <div className="relative min-w-0 w-full lg:w-12 lg:flex-[2]">
497 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
498 <Input
499 type="text"
500 placeholder={__("Search reviews...", "yatra")}
501 value={searchTerm}
502 onChange={(e) => setSearchTerm(e.target.value)}
503 className="pl-10 w-full"
504 />
505 </div>
506
507 {/* Status Filter */}
508 <Select
509 value={statusFilter}
510 onChange={(e) => setStatusFilter(e.target.value)}
511 className="w-full lg:flex-1"
512 >
513 <option value="all">{__("All Status", "yatra")}</option>
514 <option value="approved">{__("Approved", "yatra")}</option>
515 <option value="pending">{__("Pending", "yatra")}</option>
516 <option value="spam">{__("Spam", "yatra")}</option>
517 <option value="trash">{__("Trash", "yatra")}</option>
518 </Select>
519
520 {/* Rating Filter */}
521 <Select
522 value={ratingFilter}
523 onChange={(e) => setRatingFilter(e.target.value)}
524 className="w-full lg:flex-1"
525 >
526 <option value="all">{__("All Ratings", "yatra")}</option>
527 <option value="5">{__("5 Stars", "yatra")}</option>
528 <option value="4">{__("4 Stars", "yatra")}</option>
529 <option value="3">{__("3 Stars", "yatra")}</option>
530 <option value="2">{__("2 Stars", "yatra")}</option>
531 <option value="1">{__("1 Star", "yatra")}</option>
532 </Select>
533
534 {/* Sort By */}
535 <Select
536 value={sortBy}
537 onChange={(e) => setSortBy(e.target.value)}
538 className="w-full lg:flex-1"
539 >
540 <option value="created_at">{__("Date", "yatra")}</option>
541 <option value="trip_title">{__("Trip", "yatra")}</option>
542 <option value="customer_name">{__("Customer", "yatra")}</option>
543 <option value="rating">{__("Rating", "yatra")}</option>
544 <option value="status">{__("Status", "yatra")}</option>
545 </Select>
546
547 {/* Sort Order */}
548 <Button
549 variant="outline"
550 onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
551 className="h-11 px-4 flex items-center gap-1.5 flex-shrink-0"
552 title={
553 sortOrder === "asc"
554 ? __("Ascending", "yatra")
555 : __("Descending", "yatra")
556 }
557 >
558 {sortOrder === "asc" ? (
559 <ArrowUp className="w-4 h-4" />
560 ) : (
561 <ArrowDown className="w-4 h-4" />
562 )}
563 <span className="text-sm">
564 {sortOrder === "asc" ? __("Asc", "yatra") : __("Desc", "yatra")}
565 </span>
566 </Button>
567
568 {/* Reset Button */}
569 {hasFilters && (
570 <Button
571 variant="outline"
572 onClick={handleResetFilters}
573 className="flex items-center gap-2 h-11 flex-shrink-0"
574 >
575 <X className="w-4 h-4" />
576 {__("Reset", "yatra")}
577 </Button>
578 )}
579 </div>
580 </CardContent>
581 </Card>
582
583 {/* Bulk actions toolbar - shared component */}
584 <BulkActionToolbar
585 selectedIds={selectedIds}
586 bulkAction={bulkAction}
587 setBulkAction={setBulkAction}
588 onApply={handleBulkApply}
589 onClearSelection={() => setSelectedIds([])}
590 statusFilter={statusFilter}
591 setStatusFilter={(value: string) => {
592 setStatusFilter(value);
593 setPage(1);
594 setSelectedIds([]);
595 setBulkAction("");
596 }}
597 statusOptions={[
598 { key: "all", label: __("All", "yatra"), count: total },
599 {
600 key: "approved",
601 label: __("Approved", "yatra"),
602 count: reviews.filter((r) => r.status === "approved").length,
603 },
604 {
605 key: "pending",
606 label: __("Pending", "yatra"),
607 count: reviews.filter((r) => r.status === "pending").length,
608 },
609 {
610 key: "spam",
611 label: __("Spam", "yatra"),
612 count: reviews.filter((r) => r.status === "spam").length,
613 },
614 {
615 key: "trash",
616 label: __("Trash", "yatra"),
617 count: reviews.filter((r) => r.status === "trash").length,
618 },
619 ]}
620 showColumnsDropdown={showColumnsDropdown}
621 setShowColumnsDropdown={setShowColumnsDropdown}
622 columnOptions={columnOptions}
623 onToggleColumn={toggleColumn}
624 bulkMutationPending={deleteMutation.isPending}
625 totalItems={reviews.length}
626 bulkActionOptions={bulkActionOptions}
627 />
628
629 <Card>
630 <CardContent className="p-0 overflow-x-auto">
631 <SharedTable
632 data={reviews}
633 columns={[
634 visibleColumns.trip && {
635 key: "trip",
636 label: __("Trip", "yatra"),
637 render: (review: Review) =>
638 review.trip_id ? (
639 <a
640 href={`${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${review.trip_id}`}
641 className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
642 >
643 {review.trip_title}
644 </a>
645 ) : (
646 <span className="font-medium text-gray-500 dark:text-gray-400">
647 {review.trip_title}
648 </span>
649 ),
650 },
651 visibleColumns.customer && {
652 key: "customer",
653 label: __("Customer", "yatra"),
654 render: (review: Review) => (
655 <div>
656 <div className="font-medium text-gray-900 dark:text-white">
657 {review.customer_name}
658 </div>
659 <div className="text-xs text-gray-500 dark:text-gray-400">
660 {review.customer_email}
661 </div>
662 {review.verified && (
663 <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400 mt-1">
664 {__("Verified", "yatra")}
665 </span>
666 )}
667 </div>
668 ),
669 },
670 visibleColumns.rating && {
671 key: "rating",
672 label: __("Rating", "yatra"),
673 render: (review: Review) => renderStars(review.rating),
674 },
675 visibleColumns.review && {
676 key: "review",
677 label: __("Review", "yatra"),
678 render: (review: Review) => (
679 <div>
680 <div className="font-medium text-gray-900 dark:text-white mb-1">
681 {review.title}
682 </div>
683 <div className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
684 {review.content}
685 </div>
686 </div>
687 ),
688 },
689 visibleColumns.status && {
690 key: "status",
691 label: __("Status", "yatra"),
692 render: (review: Review) => getStatusBadge(review.status),
693 },
694 visibleColumns.date && {
695 key: "date",
696 label: __("Date", "yatra"),
697 render: (review: Review) => (
698 <span className="text-gray-500 dark:text-gray-400 text-sm">
699 {formatDate(review.created_at)}
700 </span>
701 ),
702 },
703 ].filter(Boolean)}
704 actions={actions}
705 isLoading={isLoading}
706 isError={!!error}
707 errorText={__("Error loading reviews", "yatra")}
708 errorDescription={__(
709 "We couldn’t connect to the reviews service. Please refresh or try again shortly.",
710 "We couldn’t connect to the reviews service. Please refresh or try again shortly.",
711 )}
712 onRetry={() =>
713 queryClient.invalidateQueries({ queryKey: ["reviews"] })
714 }
715 errorDetails={errorContext.details}
716 errorRequestInfo={errorContext.requestInfo}
717 emptyText={__("No reviews found", "yatra")}
718 emptyDescription={__(
719 "When customers submit reviews, they will appear here.",
720 "yatra",
721 )}
722 onSort={handleSort}
723 getSortIcon={getSortIcon}
724 selectedItemIds={selectedIds}
725 onSelectItem={(id: string | number, checked: boolean) => {
726 if (checked) {
727 setSelectedIds((prev) =>
728 prev.includes(id) ? prev : [...prev, id],
729 );
730 } else {
731 setSelectedIds((prev) =>
732 prev.filter((existingId) => existingId !== id),
733 );
734 }
735 }}
736 onSelectAll={(checked: boolean) => {
737 if (checked) {
738 setSelectedIds(reviews.map((r) => r.id));
739 } else {
740 setSelectedIds([]);
741 }
742 }}
743 isAllSelected={
744 reviews.length > 0 && selectedIds.length === reviews.length
745 }
746 getItemId={(review: Review) => review.id}
747 capability="yatra_view_reviews"
748 skeletonRows={5}
749 />
750 </CardContent>
751 </Card>
752
753 {/* Pagination - shared component, no extra white background */}
754 {total > 0 && (
755 <Pagination
756 currentPage={page}
757 totalPages={totalPages}
758 totalItems={total}
759 itemsPerPage={10}
760 onPageChange={setPage}
761 itemName={__("Review", "yatra")}
762 />
763 )}
764 </div>
765 );
766 };
767
768 export default Reviews;
769