PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
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 / account / utils.ts

utils.ts in Yatra – Travel Booking & Tour Operator Software 3.0.5.1, at resources/js/pages/account/utils.ts

314 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { __, sprintf } from "../../lib/i18n";
2 import {
3 applyCurrencyPosition,
4 formatYatraMoney,
5 } from "../../lib/currency-display";
6
7 function toBrowserLocaleTag(
8 locale: string | undefined | null,
9 ): string | undefined {
10 const raw = String(locale || "").trim();
11 if (!raw) return undefined;
12 // WP locales are often like "en_US"; Intl expects BCP47 like "en-US".
13 return raw.replace(/_/g, "-");
14 }
15
16 function getAccountLocale(): string | undefined {
17 if (typeof window === "undefined") return undefined;
18 const raw = (window as unknown as { yatraAccountPage?: { locale?: string } })
19 .yatraAccountPage?.locale;
20 return toBrowserLocaleTag(raw);
21 }
22
23 export const formatDate = (value: string | undefined | null) => {
24 if (!value) {
25 return __("N/A", "yatra");
26 }
27
28 try {
29 const date = new Date(value);
30
31 // Check if date is valid
32 if (isNaN(date.getTime())) {
33 return __("Invalid date", "yatra");
34 }
35
36 return date.toLocaleDateString(getAccountLocale(), {
37 year: "numeric",
38 month: "long",
39 day: "numeric",
40 });
41 } catch (error) {
42 return __("Invalid date", "yatra");
43 }
44 };
45
46 /** Leading Y-m-d from API datetime strings for stable range comparison. */
47 function extractYmd(value: string | undefined | null): string | null {
48 if (!value) {
49 return null;
50 }
51 const m = String(value)
52 .trim()
53 .match(/^(\d{4}-\d{2}-\d{2})/);
54 return m ? m[1] : null;
55 }
56
57 /**
58 * Format trip window for the account UI: one localized date, or start–end when different.
59 * translators: %1$s: trip start date, %2$s: trip end date (localized).
60 */
61 export const formatTravelDateRange = (
62 travelDate?: string | null,
63 endDate?: string | null,
64 ): string => {
65 const startKey = extractYmd(travelDate);
66 const endKey = extractYmd(endDate);
67 if (!startKey) {
68 return formatDate(travelDate);
69 }
70 if (!endKey || endKey === startKey) {
71 return formatDate(travelDate);
72 }
73 return sprintf(
74 // translators: 1: start date, 2: end date. Joined with an en-dash for travel date ranges.
75 __("%1$s – %2$s", "yatra"),
76 formatDate(travelDate),
77 formatDate(endDate),
78 );
79 };
80
81 /**
82 * Translate a known booking/payment/ticket status to a localized label.
83 *
84 * The previous account-page code did `__(booking.status, booking.status)` which
85 * mis-uses the textdomain argument: WP's i18n runtime treats the second arg as
86 * the textdomain (defaulting to 'default'), so it never finds the translation
87 * and silently returns the raw English key like 'confirmed'/'pending'. Worse,
88 * because the first argument is a runtime variable, the WP-CLI i18n extractor
89 * skips it — those strings never reach the .pot file and Loco never gets a
90 * chance to translate them.
91 *
92 * Mapping each known status to a static `__('Foo', 'yatra')` call fixes both
93 * problems at once: extractor sees the literals, translators can localize, and
94 * unknown values fall back to a title-cased rendition of the raw key.
95 */
96 export const getStatusLabel = (status: string | undefined | null): string => {
97 const raw = typeof status === "string" ? status.toLowerCase().trim() : "";
98 switch (raw) {
99 case "pending":
100 return __("Pending", "yatra");
101 case "confirmed":
102 return __("Confirmed", "yatra");
103 case "processing":
104 return __("Processing", "yatra");
105 case "completed":
106 return __("Completed", "yatra");
107 case "cancelled":
108 return __("Cancelled", "yatra");
109 case "refunded":
110 return __("Refunded", "yatra");
111 case "failed":
112 return __("Failed", "yatra");
113 case "on_hold":
114 return __("On hold", "yatra");
115 case "paid":
116 return __("Paid", "yatra");
117 case "partial":
118 return __("Partial", "yatra");
119 case "awaiting_response":
120 return __("Awaiting response", "yatra");
121 case "resolved":
122 return __("Resolved", "yatra");
123 case "closed":
124 return __("Closed", "yatra");
125 case "open":
126 return __("Open", "yatra");
127 case "":
128 return __("Pending", "yatra");
129 default:
130 // Unknown status — title-case it so admins can see what's coming through.
131 return raw.charAt(0).toUpperCase() + raw.slice(1).replace(/_/g, " ");
132 }
133 };
134
135 export const getBadge = (status: string | undefined | null) => {
136 const base = "px-2.5 py-0.5 rounded-full text-xs font-medium";
137
138 // Handle undefined/null/empty status
139 if (!status || typeof status !== "string") {
140 return `${base} bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300`;
141 }
142
143 switch (status.toLowerCase()) {
144 case "paid":
145 case "confirmed":
146 case "resolved":
147 return `${base} bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300`;
148 case "pending":
149 case "partial":
150 case "awaiting_response":
151 return `${base} bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300`;
152 case "failed":
153 case "cancelled":
154 return `${base} bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300`;
155 default:
156 return `${base} bg-yatra-chip-bg text-yatra-primary-darker dark:bg-yatra-surface-dark dark:text-yatra-primary-light`;
157 }
158 };
159
160 type PriceConfigWindow = Window & {
161 yatraAdmin?: Record<string, unknown>;
162 yatraAccountPage?: Record<string, unknown>;
163 };
164
165 function readPriceConfig() {
166 if (typeof window === "undefined") {
167 return {
168 globalCurrency: "USD",
169 currencyPosition: "before",
170 decimalPlaces: 2,
171 thousandSeparator: ",",
172 decimalSeparator: ".",
173 };
174 }
175 const w = window as PriceConfigWindow;
176 const a = (w.yatraAdmin || {}) as Record<string, unknown>;
177 const p = (w.yatraAccountPage || {}) as Record<string, unknown>;
178 return {
179 globalCurrency: (a.currency || p.currency || "USD") as string,
180 currencyPosition: (a.currencyPosition ||
181 a.currency_position ||
182 p.currencyPosition ||
183 p.currency_position ||
184 "before") as string,
185 decimalPlaces: Number(a.decimalPlaces ?? p.decimalPlaces ?? 2) || 2,
186 thousandSeparator: (a.thousandSeparator ||
187 p.thousandSeparator ||
188 ",") as string,
189 decimalSeparator: (a.decimalSeparator ||
190 p.decimalSeparator ||
191 ".") as string,
192 };
193 }
194
195 export const formatPrice = (price: number) => {
196 const {
197 globalCurrency,
198 currencyPosition,
199 decimalPlaces,
200 thousandSeparator,
201 decimalSeparator,
202 } = readPriceConfig();
203
204 if (!price || price === 0) {
205 return __("Contact for pricing", "yatra");
206 }
207
208 const formattedAmount = new Intl.NumberFormat(getAccountLocale(), {
209 minimumFractionDigits: decimalPlaces,
210 maximumFractionDigits: decimalPlaces,
211 })
212 .format(price)
213 .replace(/,/g, "TEMP_THOUSAND")
214 .replace(/\./g, decimalSeparator)
215 .replace(/TEMP_THOUSAND/g, thousandSeparator);
216
217 const currencySymbol = new Intl.NumberFormat(getAccountLocale(), {
218 style: "currency",
219 currency: globalCurrency,
220 })
221 .format(0)
222 .replace(/[\d\s.,]/g, "")
223 .trim();
224
225 return applyCurrencyPosition(
226 formattedAmount,
227 currencySymbol,
228 currencyPosition,
229 );
230 };
231
232 export const formatPriceForBooking = (price: number, currency?: string) => {
233 const cfg = readPriceConfig();
234 const globalCurrency = currency || cfg.globalCurrency;
235 const {
236 currencyPosition,
237 decimalPlaces,
238 thousandSeparator,
239 decimalSeparator,
240 } = cfg;
241
242 const currencyToUse = globalCurrency;
243
244 // Always format the price, even if 0 - don't show "Contact for pricing" for bookings
245 const numPrice = Number(price) || 0;
246
247 const formattedAmount = new Intl.NumberFormat(getAccountLocale(), {
248 minimumFractionDigits: decimalPlaces,
249 maximumFractionDigits: decimalPlaces,
250 })
251 .format(numPrice)
252 .replace(/,/g, "TEMP_THOUSAND")
253 .replace(/\./g, decimalSeparator)
254 .replace(/TEMP_THOUSAND/g, thousandSeparator);
255
256 const currencySymbol = new Intl.NumberFormat(getAccountLocale(), {
257 style: "currency",
258 currency: currencyToUse,
259 })
260 .format(0)
261 .replace(/[\d\s.,]/g, "")
262 .trim();
263
264 return applyCurrencyPosition(
265 formattedAmount,
266 currencySymbol,
267 currencyPosition,
268 );
269 };
270
271 export const currency = (value: number, currencyCode?: string) => {
272 const cfg = readPriceConfig();
273 const code = (currencyCode || cfg.globalCurrency || "USD") as string;
274 return formatYatraMoney(Number(value) || 0, code, { zeroAsUnknown: false });
275 };
276
277 /** Values from `wp_localize_script(…, 'yatraAccountPage', …)` on the account page. */
278 export function getYatraAccountPageGlobals(): {
279 logoutUrl: string;
280 companyPhone: string;
281 companyName: string;
282 companyEmail: string;
283 locale: string;
284 } {
285 if (typeof window === "undefined") {
286 return {
287 logoutUrl: "",
288 companyPhone: "",
289 companyName: "",
290 companyEmail: "",
291 locale: "",
292 };
293 }
294 const p = (window as unknown as { yatraAccountPage?: Record<string, string> })
295 .yatraAccountPage;
296 const raw = p || {};
297 return {
298 logoutUrl: String(raw.logoutUrl || "").trim(),
299 companyPhone: String(raw.companyPhone || "").trim(),
300 companyName: String(raw.companyName || "").trim(),
301 companyEmail: String(raw.companyEmail || "").trim(),
302 locale: String((raw as any).locale || "").trim(),
303 };
304 }
305
306 export function phoneToTelHref(phone: string): string {
307 const t = String(phone).trim();
308 if (!t) {
309 return "";
310 }
311 const compact = t.replace(/[^\d+]/g, "");
312 return compact ? `tel:${compact}` : `tel:${encodeURIComponent(t)}`;
313 }
314