/** * Difficulty Levels Page * Manage trip difficulty levels with ordering */ import React, { useState, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, ArrowUp, ArrowDown } from "lucide-react"; import { __ } from "../lib/i18n"; import { usePermissions } from "../hooks/usePermissions"; import { useToast } from "../components/ui/toast"; import { apiClient } from "../lib/api-client"; import { getErrorContext } from "../lib/errors"; import { Button } from "../components/ui/button"; import { PageHeader } from "../components/common/PageHeader"; import { Card, CardContent } from "../components/ui/card"; import { Table } from "../components/shared/Table"; import { Pagination, SearchFilterToolbar, BulkActionToolbar, } from "../components/shared"; import { getDefaultBulkStatusOptions } from "../components/shared/bulkStatusOptions"; import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; import { Edit, Trash2 } from "lucide-react"; import { IconSelector } from "../components/ui/icon-selector"; import type { IconPickerValue } from "../components/ui/icon-picker"; interface DifficultyLevel { id: number; name: string; slug: string; description: string; icon?: IconPickerValue | null; sorting: number; status: string; trip_count?: number; created_at: string; updated_at: string; created_by: number; updated_by: number; created_by_name?: string; updated_by_name?: string; } const DifficultyLevels: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [sortBy, setSortBy] = useState("sorting"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [page, setPage] = useState(1); const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]); const [bulkAction, setBulkAction] = useState(""); const [showColumnsDropdown, setShowColumnsDropdown] = useState(false); const [visibleColumns, setVisibleColumns] = useState(() => { const defaultColumns = { sorting: true, name: true, description: true, trips: true, status: true, created_at: false, }; const saved = localStorage.getItem("yatra_difficulty_levels_columns"); return saved ? { ...defaultColumns, ...JSON.parse(saved) } : defaultColumns; }); const { can } = usePermissions(); const { showToast } = useToast(); const queryClient = useQueryClient(); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; level: DifficultyLevel | null; }>({ isOpen: false, level: null, }); // Define table columns const columns = [ { key: "sorting", label: __("Order", "yatra"), sortable: true, width: "100px", visible: visibleColumns.sorting, render: (level: DifficultyLevel) => (
{level.sorting}
), }, { key: "name", label: __("Name", "yatra"), sortable: true, visible: visibleColumns.name, render: (level: DifficultyLevel) => (
{renderIcon(level.icon)}
{level.name}
{level.slug || "—"} ({__("ID:", "yatra")} {level.id})
), }, { key: "description", label: __("Description", "yatra"), visible: visibleColumns.description, render: (level: DifficultyLevel) => (
{level.description || "—"}
), }, { key: "trips", label: __("Trips", "yatra"), visible: visibleColumns.trips, render: (level: DifficultyLevel) => ( {typeof level.trip_count === "number" ? level.trip_count : 0} ), }, { key: "status", label: __("Status", "yatra"), sortable: true, visible: visibleColumns.status, render: (level: DifficultyLevel) => getStatusBadge(level.status), }, { key: "created_at", label: __("Created", "yatra"), sortable: true, visible: visibleColumns.created_at, render: (level: DifficultyLevel) => (
{formatDate(level.created_at)}
), }, ]; // Define table actions const actions = [ { key: "edit", label: __("Edit", "yatra"), icon: , onClick: (level: DifficultyLevel) => handleEdit(level), condition: () => can("yatra_edit_trips"), }, { key: "publish", label: __("Make Published", "yatra"), icon: , onClick: (level: DifficultyLevel) => statusMutation.mutate({ id: level.id, status: "publish" }), condition: (level: DifficultyLevel) => can("yatra_edit_trips") && level.status !== "publish", }, { key: "draft", label: __("Make Draft", "yatra"), icon: , onClick: (level: DifficultyLevel) => statusMutation.mutate({ id: level.id, status: "draft" }), condition: (level: DifficultyLevel) => can("yatra_edit_trips") && level.status !== "draft", }, { key: "delete", label: __("Delete", "yatra"), icon: , onClick: (level: DifficultyLevel) => setDeleteConfirm({ isOpen: true, level }), variant: "destructive" as const, condition: (level: DifficultyLevel) => can("yatra_edit_trips") && level.status === "trash", }, { key: "trash", label: __("Move to Trash", "yatra"), icon: , onClick: (level: DifficultyLevel) => statusMutation.mutate({ id: level.id, status: "trash" }), variant: "destructive" as const, condition: (level: DifficultyLevel) => can("yatra_edit_trips") && level.status !== "trash", }, ]; const queryParams = useMemo(() => { const params: Record = { page, per_page: 20, orderby: sortBy, order: sortOrder, }; if (searchTerm) { params.search = searchTerm; } if (statusFilter !== "all") { params.status = statusFilter; } return params; }, [searchTerm, statusFilter, sortBy, sortOrder, page]); // Fetch stable status counts from API (independent of filters) const { data: statsData } = useQuery({ queryKey: ["difficulty-levels-stats"], queryFn: async () => { try { const response = await apiClient.get("/difficulty-levels/stats"); return response; } catch (error: any) { return { all: 0, publish: 0, draft: 0, trash: 0 }; } }, enabled: can("yatra_view_trips"), }); const { data, isLoading, error } = useQuery({ queryKey: ["difficulty-levels", queryParams], queryFn: async () => { try { const response = await apiClient.get("/difficulty-levels", { params: queryParams, }); return response; } catch (error: any) { showToast( error?.message || __("Failed to load difficulty levels", "yatra"), "error", ); throw error; } }, enabled: can("yatra_view_trips"), }); const deleteMutation = useMutation({ mutationFn: async (id: number) => apiClient.delete(`/difficulty-levels/${id}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["difficulty-levels"] }); queryClient.invalidateQueries({ queryKey: ["difficulty-levels-stats"] }); showToast( __("Difficulty level deleted successfully", "yatra"), "success", ); }, onError: (error: any) => { showToast( error?.message || __("Failed to delete difficulty level", "yatra"), "error", ); }, }); const statusMutation = useMutation({ mutationFn: async ({ id, status }: { id: number; status: string }) => { const levelResponse = await apiClient.get(`/difficulty-levels/${id}`); const level = levelResponse as DifficultyLevel; return apiClient.put(`/difficulty-levels/${id}`, { name: level.name, slug: level.slug, description: level.description, icon: level.icon, sorting: level.sorting, status, }); }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ["difficulty-levels"] }); queryClient.invalidateQueries({ queryKey: ["difficulty-levels-stats"] }); const msgMap: Record = { trash: __("Difficulty level moved to trash", "yatra"), draft: __("Difficulty level marked as draft", "yatra"), publish: __("Difficulty level published", "yatra"), }; const msg = msgMap[variables.status] || __("Difficulty level updated successfully", "yatra"); showToast(msg, "success"); }, onError: (error: any, variables) => { const msgMapError: Record = { trash: __("Failed to move difficulty level to trash", "yatra"), draft: __("Failed to mark difficulty level as draft", "yatra"), publish: __("Failed to publish difficulty level", "yatra"), }; const fallback = msgMapError[variables.status] || __("Failed to update difficulty level", "yatra"); showToast(error?.message || fallback, "error"); }, }); const levels = data?.data || []; const total = data?.total || 0; const totalPages = Math.ceil(total / 20); const errorContext = getErrorContext(error); // Status counts from stats API (stable across filters) const statusCounts = useMemo(() => { if (statsData) { return { all: statsData.all ?? 0, publish: statsData.publish ?? 0, draft: statsData.draft ?? 0, trash: statsData.trash ?? 0, }; } return { all: 0, publish: 0, draft: 0, trash: 0, }; }, [statsData]); const formatDate = (dateString: string) => { if (!dateString) return __("N/A", "yatra"); try { const date = new Date(dateString); return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return dateString; } }; const getStatusBadge = (status: string) => { const map: Record = { publish: { className: "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400", label: __("Publish", "yatra"), }, draft: { className: "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400", label: __("Draft", "yatra"), }, trash: { className: "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400", label: __("Trash", "yatra"), }, }; const info = map[status] || map.draft; return ( {info.label} ); }; const renderIcon = (icon: IconPickerValue | null | undefined) => { if (!icon) { return (
); } if (icon.type === "image") { return ( { const target = e.target as HTMLImageElement; target.style.display = "none"; if (target.parentElement) { target.parentElement.innerHTML = '
'; } }} /> ); } return (
); }; const handleEdit = (level: DifficultyLevel) => { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=difficulty-levels&action=edit&id=${level.id}`; }; const handleCreate = () => { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=difficulty-levels&action=create`; }; const handleSort = (field: string) => { if (sortBy === field) { setSortOrder(sortOrder === "asc" ? "desc" : "asc"); } else { setSortBy(field); setSortOrder("asc"); } }; const getSortIcon = (field: string) => { if (sortBy !== field) return null; return sortOrder === "asc" ? ( ) : ( ); }; const handleResetFilters = () => { setSearchTerm(""); setStatusFilter("all"); setSortBy("sorting"); setSortOrder("asc"); setPage(1); }; const hasFilters = !!searchTerm || statusFilter !== "all" || sortBy !== "sorting" || sortOrder !== "asc"; const toggleColumn = (columnKey: string) => { const newVisibleColumns = { ...visibleColumns, [columnKey]: !visibleColumns[columnKey as keyof typeof visibleColumns], }; setVisibleColumns(newVisibleColumns); localStorage.setItem( "yatra_difficulty_levels_columns", JSON.stringify(newVisibleColumns), ); }; const handleBulkApply = () => { if (!bulkAction) { showToast(__("Select a bulk action first.", "yatra"), "warning"); return; } if (selectedIds.length === 0) { showToast( __("Select at least one difficulty level.", "yatra"), "warning", ); return; } const performBulk = async () => { try { if (bulkAction === "delete") { await Promise.all( selectedIds.map((id) => apiClient.delete(`/difficulty-levels/${id}`), ), ); showToast( __("Selected difficulty levels deleted successfully", "yatra"), "success", ); } else if ( bulkAction === "trash" || bulkAction === "draft" || bulkAction === "publish" ) { const status = bulkAction === "trash" ? "trash" : bulkAction === "draft" ? "draft" : "publish"; await Promise.all( selectedIds.map(async (id) => { try { const levelResponse = await apiClient.get( `/difficulty-levels/${id}`, ); const level = levelResponse; if (!level || !level.name) { return; } await apiClient.put(`/difficulty-levels/${id}`, { name: level.name, slug: level.slug, description: level.description, icon: level.icon, sorting: level.sorting, status, }); } catch { // If a single item fails, let the outer catch handle messaging throw new Error("bulk_item_failed"); } }), ); const msgMap: Record = { trash: __("Selected difficulty levels moved to trash", "yatra"), draft: __("Selected difficulty levels marked as draft", "yatra"), publish: __("Selected difficulty levels published", "yatra"), }; showToast(msgMap[bulkAction], "success"); } queryClient.invalidateQueries({ queryKey: ["difficulty-levels"] }); queryClient.invalidateQueries({ queryKey: ["difficulty-levels-stats"], }); setSelectedIds([]); setBulkAction(""); } catch (error: any) { const msgMapError: Record = { delete: __("Failed to delete selected difficulty levels", "yatra"), trash: __( "Failed to move selected difficulty levels to trash", "yatra", ), draft: __( "Failed to mark selected difficulty levels as draft", "yatra", ), publish: __("Failed to publish selected difficulty levels", "yatra"), }; showToast(error?.message || msgMapError[bulkAction], "error"); } }; void performBulk(); }; return (
setDeleteConfirm({ isOpen: false, level: null })} onConfirm={() => { if (deleteConfirm.level) { deleteMutation.mutate(deleteConfirm.level.id); setDeleteConfirm({ isOpen: false, level: null }); } }} title={__("Delete Difficulty Level", "yatra")} message={ deleteConfirm.level ? __( 'Are you sure you want to delete "{name}"? This action cannot be undone.', "yatra", ).replace("{name}", deleteConfirm.level.name) : __( "Are you sure you want to delete this difficulty level? This action cannot be undone.", "yatra", ) } confirmText={__("Delete", "yatra")} cancelText={__("Cancel", "yatra")} variant="danger" isLoading={deleteMutation.isPending} /> {__("Add New", "yatra")} } /> {/* Filters */} { setStatusFilter(value); setPage(1); }} statusOptions={[ { value: "all", label: __("All Status", "yatra") }, { value: "publish", label: __("Published", "yatra") }, { value: "draft", label: __("Draft", "yatra") }, ]} sortBy={sortBy} onSortByChange={(value) => { setSortBy(value); setSortOrder("asc"); setPage(1); }} sortOrder={sortOrder} onSortOrderChange={(order) => { setSortOrder(order); setPage(1); }} sortOptions={[ { value: "sorting", label: __("Order", "yatra") }, { value: "name", label: __("Name", "yatra") }, { value: "created_at", label: __("Created", "yatra") }, ]} onResetFilters={handleResetFilters} hasFilters={hasFilters} placeholder={__("Search difficulty levels", "yatra")} /> {!error && ( setSelectedIds([])} statusFilter={statusFilter} setStatusFilter={(value) => { setStatusFilter(value); setPage(1); setSelectedIds([]); setBulkAction(""); }} statusOptions={[ { key: "all", label: __("All", "yatra"), count: statusCounts.all }, { key: "publish", label: __("Published", "yatra"), count: statusCounts.publish, }, { key: "draft", label: __("Draft", "yatra"), count: statusCounts.draft, }, { key: "trash", label: __("Trash", "yatra"), count: statusCounts.trash, }, ]} showColumnsDropdown={showColumnsDropdown} setShowColumnsDropdown={setShowColumnsDropdown} columnOptions={[ { key: "sorting", label: __("Order", "yatra"), visible: visibleColumns.sorting, }, { key: "name", label: __("Name", "yatra"), visible: visibleColumns.name, }, { key: "slug", label: __("Slug", "yatra"), visible: visibleColumns.slug, }, { key: "description", label: __("Description", "yatra"), visible: visibleColumns.description, }, { key: "status", label: __("Status", "yatra"), visible: visibleColumns.status, }, { key: "created_at", label: __("Created", "yatra"), visible: visibleColumns.created_at, }, ]} onToggleColumn={toggleColumn} bulkMutationPending={false} totalItems={levels.length} bulkActionOptions={getDefaultBulkStatusOptions(statusFilter)} /> )} {/* Table */} queryClient.invalidateQueries({ queryKey: ["difficulty-levels"] }) } errorDetails={errorContext.details} errorRequestInfo={errorContext.requestInfo} emptyText={__("No difficulty levels found", "yatra")} emptyDescription={__( "Get started by creating your first difficulty level to categorize trips by their physical challenge level.", "Get started by creating your first difficulty level to categorize trips by their physical challenge level.", )} onCreateClick={handleCreate} onSort={handleSort} getSortIcon={getSortIcon} selectedItemIds={selectedIds} onSelectItem={(id: string | number, checked: boolean) => { if (checked) { setSelectedIds([...selectedIds, id]); } else { setSelectedIds( selectedIds.filter((selectedId) => selectedId !== id), ); } }} onSelectAll={(checked: boolean) => { if (checked) { setSelectedIds( levels.map((level: DifficultyLevel) => level.id), ); } else { setSelectedIds([]); } }} isAllSelected={ levels.length > 0 && selectedIds.length === levels.length } getItemId={(level: DifficultyLevel) => level.id} getItemStatus={(level: DifficultyLevel) => level.status} statusFilter={statusFilter} skeletonRows={5} capability="yatra_edit_trips" /> {/* Pagination */} {total > 0 && (
setPage(newPage)} itemName={__("difficulty levels", "yatra")} />
)} ); }; export default DifficultyLevels;