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 / account / utils.ts

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

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