| 1 |
import { useMemo } from "react"; |
| 2 |
import { useQuery } from "@tanstack/react-query"; |
| 3 |
import { __, sprintf } from "../lib/i18n"; |
| 4 |
import { apiClient, apiService } from "../lib/api-client"; |
| 5 |
import type { MultiSelectOption } from "../components/ui/multi-select"; |
| 6 |
import type { FormConditionTargets } from "../components/settings/booking-form-types"; |
| 7 |
|
| 8 |
/** |
| 9 |
* Trip targeting shared by the booking-form Conditions popup and the email |
| 10 |
* template overrides: one MultiSelect holds trip types, categories (nested) |
| 11 |
* and individual trips, encoded as "type:x" / "category:ID" / "trip:ID". |
| 12 |
* Mirrors the Pro-side matching (trip › category incl. ancestors › trip type). |
| 13 |
*/ |
| 14 |
export type TripTargets = FormConditionTargets; |
| 15 |
|
| 16 |
// ---- picker: trips / categories / trip types in one MultiSelect ----------- |
| 17 |
|
| 18 |
const TRIP_TYPE_OPTIONS: { value: string; label: string }[] = [ |
| 19 |
{ value: "single_day", label: __("Single Day Trip", "yatra") }, |
| 20 |
{ value: "multi_day", label: __("Multi-Day Trip", "yatra") }, |
| 21 |
]; |
| 22 |
|
| 23 |
export const encodeTargets = (t?: FormConditionTargets): string[] => [ |
| 24 |
...(t?.trip_types ?? []).map((v) => `type:${v}`), |
| 25 |
...(t?.categories ?? []).map((id) => `category:${id}`), |
| 26 |
...(t?.trips ?? []).map((id) => `trip:${id}`), |
| 27 |
]; |
| 28 |
|
| 29 |
export const decodeTargets = ( |
| 30 |
values: (string | number)[], |
| 31 |
): FormConditionTargets => { |
| 32 |
const out: FormConditionTargets = {}; |
| 33 |
values.forEach((raw) => { |
| 34 |
const [kind, rest] = String(raw).split(":", 2); |
| 35 |
if (kind === "trip" && Number(rest) > 0) { |
| 36 |
(out.trips ||= []).push(Number(rest)); |
| 37 |
} else if (kind === "category" && Number(rest) > 0) { |
| 38 |
(out.categories ||= []).push(Number(rest)); |
| 39 |
} else if (kind === "type" && rest) { |
| 40 |
(out.trip_types ||= []).push(rest); |
| 41 |
} |
| 42 |
}); |
| 43 |
return out; |
| 44 |
}; |
| 45 |
|
| 46 |
export const hasTargets = (t?: FormConditionTargets): boolean => |
| 47 |
!!t && |
| 48 |
((t.trips?.length ?? 0) > 0 || |
| 49 |
(t.categories?.length ?? 0) > 0 || |
| 50 |
(t.trip_types?.length ?? 0) > 0); |
| 51 |
|
| 52 |
interface CategoryNode { |
| 53 |
id: number; |
| 54 |
name: string; |
| 55 |
subcategories?: CategoryNode[]; |
| 56 |
} |
| 57 |
|
| 58 |
const flattenCategories = ( |
| 59 |
nodes: CategoryNode[], |
| 60 |
depth = 0, |
| 61 |
): MultiSelectOption[] => |
| 62 |
nodes.flatMap((c) => [ |
| 63 |
{ |
| 64 |
value: `category:${c.id}`, |
| 65 |
label: `${__("Category", "yatra")}: ${"— ".repeat(depth)}${c.name}`, |
| 66 |
}, |
| 67 |
...(Array.isArray(c.subcategories) |
| 68 |
? flattenCategories(c.subcategories, depth + 1) |
| 69 |
: []), |
| 70 |
]); |
| 71 |
|
| 72 |
/** Trips + categories the picker can target; fetched only while `enabled`. */ |
| 73 |
export const useTripTargets = (enabled: boolean) => { |
| 74 |
const trips = useQuery({ |
| 75 |
queryKey: ["booking-form-conditions", "trips"], |
| 76 |
queryFn: async () => { |
| 77 |
const res: any = await apiService.getTrips({ |
| 78 |
per_page: 500, |
| 79 |
orderby: "title", |
| 80 |
order: "ASC", |
| 81 |
}); |
| 82 |
const list = Array.isArray(res?.data) |
| 83 |
? res.data |
| 84 |
: Array.isArray(res) |
| 85 |
? res |
| 86 |
: []; |
| 87 |
return list |
| 88 |
.filter((t: any) => t && t.id && t.status !== "trash") |
| 89 |
.map((t: any) => ({ |
| 90 |
value: `trip:${t.id}`, |
| 91 |
label: `${__("Trip", "yatra")}: ${t.title || `#${t.id}`}`, |
| 92 |
})) as MultiSelectOption[]; |
| 93 |
}, |
| 94 |
enabled, |
| 95 |
staleTime: 5 * 60 * 1000, |
| 96 |
}); |
| 97 |
|
| 98 |
const categories = useQuery({ |
| 99 |
queryKey: ["booking-form-conditions", "categories"], |
| 100 |
queryFn: async () => { |
| 101 |
const res: any = await apiClient.get("/trip-categories", { |
| 102 |
params: { |
| 103 |
per_page: 100, |
| 104 |
hierarchical: true, |
| 105 |
orderby: "name", |
| 106 |
order: "ASC", |
| 107 |
}, |
| 108 |
}); |
| 109 |
const list = Array.isArray(res?.data) |
| 110 |
? res.data |
| 111 |
: Array.isArray(res) |
| 112 |
? res |
| 113 |
: []; |
| 114 |
return flattenCategories(list as CategoryNode[]); |
| 115 |
}, |
| 116 |
enabled, |
| 117 |
staleTime: 5 * 60 * 1000, |
| 118 |
}); |
| 119 |
|
| 120 |
const options = useMemo<MultiSelectOption[]>( |
| 121 |
() => [ |
| 122 |
...TRIP_TYPE_OPTIONS.map((t) => ({ |
| 123 |
value: `type:${t.value}`, |
| 124 |
label: `${__("Trip type", "yatra")}: ${t.label}`, |
| 125 |
})), |
| 126 |
...(categories.data ?? []), |
| 127 |
...(trips.data ?? []), |
| 128 |
], |
| 129 |
[trips.data, categories.data], |
| 130 |
); |
| 131 |
|
| 132 |
return { options, isLoading: trips.isLoading || categories.isLoading }; |
| 133 |
}; |
| 134 |
|
| 135 |
/** Human summary of a condition's targets, for the page and the popup header. */ |
| 136 |
export const describeTargets = ( |
| 137 |
t: FormConditionTargets | undefined, |
| 138 |
options: MultiSelectOption[], |
| 139 |
): string => { |
| 140 |
const names = encodeTargets(t).map((v) => { |
| 141 |
const opt = options.find((o) => String(o.value) === v); |
| 142 |
if (opt) return opt.label.replace(/^[^:]+: /, ""); |
| 143 |
const [kind, rest] = v.split(":", 2); |
| 144 |
if (kind === "type") { |
| 145 |
return TRIP_TYPE_OPTIONS.find((x) => x.value === rest)?.label || rest; |
| 146 |
} |
| 147 |
return kind === "trip" |
| 148 |
? sprintf(__("Trip #%s", "yatra"), rest) |
| 149 |
: sprintf(__("Category #%s", "yatra"), rest); |
| 150 |
}); |
| 151 |
return names.join(", "); |
| 152 |
}; |
| 153 |
|