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

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

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