PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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.4, at resources/js/pages/Attributes.tsx

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