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

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

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