PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 / lib / api-client.ts

api-client.ts in Yatra – Travel Booking & Tour Operator Software 3.0.15, at resources/js/lib/api-client.ts

814 lines 26.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * API Client for WordPress REST API
3 * Centralized API service with proper URL handling and authentication
4 */
5
6 import { API_ENDPOINTS } from "./api-endpoints";
7
8 type RequestInfo = {
9 url: string;
10 method: string;
11 payload?: string;
12 };
13
14 const serializePayload = (
15 body: BodyInit | null | undefined,
16 ): string | undefined => {
17 if (!body) {
18 return undefined;
19 }
20
21 if (typeof body === "string") {
22 return body;
23 }
24
25 if (body instanceof URLSearchParams) {
26 return body.toString();
27 }
28
29 if (typeof FormData !== "undefined" && body instanceof FormData) {
30 const entries: Record<string, unknown> = {};
31 body.forEach((value, key) => {
32 entries[key] = value;
33 });
34 try {
35 return JSON.stringify(entries, null, 2);
36 } catch {
37 return "[FormData]";
38 }
39 }
40
41 if (typeof body === "object") {
42 try {
43 return JSON.stringify(body, null, 2);
44 } catch {
45 return String(body);
46 }
47 }
48
49 return String(body);
50 };
51
52 const formatRequestUrl = (rawUrl: string): string => {
53 try {
54 const parsed = new URL(rawUrl, window.location.origin);
55 const params = new URLSearchParams(parsed.search);
56 const restRoute = params.get("rest_route");
57
58 if (restRoute) {
59 params.delete("rest_route");
60 const decodedRoute = decodeURIComponent(restRoute);
61 const normalizedRoute = decodedRoute.startsWith("/")
62 ? decodedRoute
63 : `/${decodedRoute}`;
64 const remainingParams = params.toString();
65 return `${parsed.origin}${normalizedRoute}${remainingParams ? `?${remainingParams}` : ""}`;
66 }
67
68 return parsed.toString();
69 } catch {
70 return rawUrl;
71 }
72 };
73
74 class ApiError extends Error {
75 response: {
76 status: number;
77 statusText: string;
78 data: any;
79 };
80 requestInfo?: RequestInfo;
81
82 constructor(
83 message: string,
84 response: { status: number; statusText: string; data: any },
85 requestInfo?: RequestInfo,
86 ) {
87 super(message);
88 this.name = "ApiError";
89 this.response = response;
90 this.requestInfo = requestInfo;
91 }
92 }
93
94 type YatraWindowGlobals = Window & {
95 yatraAccountPage?: { apiUrl?: string; nonce?: string };
96 yatraAdmin?: { apiUrl?: string; nonce?: string };
97 };
98
99 class ApiClient {
100 /** Public account page uses `yatraAccountPage`; admin uses `yatraAdmin`. Resolve per request so module load order never leaves an empty nonce. */
101 private resolveBaseUrl(): string {
102 if (typeof window === "undefined") {
103 return "/wp-json/yatra/v1";
104 }
105 const w = window as YatraWindowGlobals;
106 const raw =
107 w.yatraAccountPage?.apiUrl || w.yatraAdmin?.apiUrl || "/wp-json/yatra/v1";
108 return raw.endsWith("/") ? raw.slice(0, -1) : raw;
109 }
110
111 private resolveNonce(): string {
112 if (typeof window === "undefined") {
113 return "";
114 }
115 const w = window as YatraWindowGlobals;
116 return w.yatraAccountPage?.nonce || w.yatraAdmin?.nonce || "";
117 }
118
119 private async request(
120 endpoint: string,
121 options: RequestInit = {},
122 queryParams?: URLSearchParams,
123 ): Promise<any> {
124 // Extract endpoint path (remove any query params that might have been appended)
125 const [endpointPath, endpointQuery] = endpoint.split("?");
126 const cleanEndpoint = endpointPath.startsWith("/")
127 ? endpointPath
128 : `/${endpointPath}`;
129
130 // Build URL properly - handle query string format (rest_route) vs pretty permalinks
131 let url: string;
132 const baseUrl = this.resolveBaseUrl();
133
134 // Check if baseUrl uses query string format (contains ?rest_route=)
135 if (baseUrl.includes("?rest_route=")) {
136 // Query string format: append endpoint to rest_route value, then add other params with &
137 const [base, queryString] = baseUrl.split("?");
138 const params = new URLSearchParams(queryString);
139 const restRoute = params.get("rest_route") || "";
140 params.set("rest_route", restRoute + cleanEndpoint);
141
142 // Add any query params from endpoint or passed separately
143 if (endpointQuery) {
144 const endpointParams = new URLSearchParams(endpointQuery);
145 endpointParams.forEach((value, key) => {
146 params.append(key, value);
147 });
148 }
149 if (queryParams) {
150 queryParams.forEach((value, key) => {
151 params.append(key, value);
152 });
153 }
154
155 url = `${base}?${params.toString()}`;
156 } else {
157 // Pretty permalink format: append endpoint and add query params with ?
158 url = `${baseUrl}${cleanEndpoint}`;
159 if (endpointQuery || queryParams) {
160 const params = new URLSearchParams();
161 if (endpointQuery) {
162 const endpointParams = new URLSearchParams(endpointQuery);
163 endpointParams.forEach((value, key) => {
164 params.append(key, value);
165 });
166 }
167 if (queryParams) {
168 queryParams.forEach((value, key) => {
169 params.append(key, value);
170 });
171 }
172 url += `?${params.toString()}`;
173 }
174 }
175
176 const isFormDataBody =
177 typeof FormData !== "undefined" && options.body instanceof FormData;
178
179 // Build headers
180 const headers: HeadersInit = {
181 "X-WP-Nonce": this.resolveNonce(),
182 ...options.headers,
183 };
184
185 // Only set JSON content-type when we're not sending FormData and caller didn't override.
186 if (!isFormDataBody) {
187 const hasContentTypeHeader =
188 (headers instanceof Headers && headers.has("Content-Type")) ||
189 (!(headers instanceof Headers) &&
190 Object.keys(headers as Record<string, any>).some(
191 (k) => k.toLowerCase() === "content-type",
192 ));
193
194 if (!hasContentTypeHeader) {
195 (headers as any)["Content-Type"] = "application/json";
196 }
197 }
198
199 const method = (options.method || "GET").toUpperCase();
200 const serializedPayload = serializePayload(options.body);
201
202 const response = await fetch(url, {
203 ...options,
204 headers,
205 credentials: "include",
206 });
207
208 if (!response.ok) {
209 const raw = await response.text();
210 let data: any = null;
211 if (raw) {
212 try {
213 data = JSON.parse(raw);
214 } catch {
215 data = raw;
216 }
217 }
218 const message =
219 (typeof data === "object" && data?.message) ||
220 (typeof data === "string" && data) ||
221 response.statusText ||
222 `HTTP error! status: ${response.status}`;
223
224 throw new ApiError(
225 message,
226 {
227 status: response.status,
228 statusText: response.statusText,
229 data,
230 },
231 {
232 url: formatRequestUrl(url),
233 method,
234 payload: serializedPayload,
235 },
236 );
237 }
238
239 if (response.status === 204) {
240 return null;
241 }
242
243 const text = await response.text();
244 if (!text) {
245 return null;
246 }
247
248 try {
249 return JSON.parse(text);
250 } catch {
251 return text;
252 }
253 }
254
255 private async requestBlob(
256 endpoint: string,
257 options: RequestInit = {},
258 queryParams?: URLSearchParams,
259 ): Promise<Blob> {
260 const [endpointPath, endpointQuery] = endpoint.split("?");
261 const cleanEndpoint = endpointPath.startsWith("/")
262 ? endpointPath
263 : `/${endpointPath}`;
264
265 let url: string;
266 const baseUrl = this.resolveBaseUrl();
267 if (baseUrl.includes("?rest_route=")) {
268 const [base, queryString] = baseUrl.split("?");
269 const params = new URLSearchParams(queryString);
270 const restRoute = params.get("rest_route") || "";
271 params.set("rest_route", restRoute + cleanEndpoint);
272
273 if (endpointQuery) {
274 const endpointParams = new URLSearchParams(endpointQuery);
275 endpointParams.forEach((value, key) => {
276 params.append(key, value);
277 });
278 }
279 if (queryParams) {
280 queryParams.forEach((value, key) => {
281 params.append(key, value);
282 });
283 }
284 url = `${base}?${params.toString()}`;
285 } else {
286 url = `${baseUrl}${cleanEndpoint}`;
287 if (endpointQuery || queryParams) {
288 const params = new URLSearchParams();
289 if (endpointQuery) {
290 const endpointParams = new URLSearchParams(endpointQuery);
291 endpointParams.forEach((value, key) => {
292 params.append(key, value);
293 });
294 }
295 if (queryParams) {
296 queryParams.forEach((value, key) => {
297 params.append(key, value);
298 });
299 }
300 url += `?${params.toString()}`;
301 }
302 }
303
304 const isFormDataBody =
305 typeof FormData !== "undefined" && options.body instanceof FormData;
306 const headers: HeadersInit = {
307 "X-WP-Nonce": this.resolveNonce(),
308 ...options.headers,
309 };
310 if (!isFormDataBody) {
311 const hasContentTypeHeader =
312 (headers instanceof Headers && headers.has("Content-Type")) ||
313 (!(headers instanceof Headers) &&
314 Object.keys(headers as Record<string, any>).some(
315 (k) => k.toLowerCase() === "content-type",
316 ));
317 if (!hasContentTypeHeader) {
318 (headers as any)["Content-Type"] = "application/json";
319 }
320 }
321
322 const response = await fetch(url, {
323 ...options,
324 headers,
325 credentials: "include",
326 });
327
328 if (!response.ok) {
329 const error = await response
330 .json()
331 .catch(() => ({ message: "An error occurred" }));
332 throw new Error(
333 error.message || `HTTP error! status: ${response.status}`,
334 );
335 }
336
337 return response.blob();
338 }
339
340 async get(
341 endpoint: string,
342 config?: { params?: Record<string, any> },
343 ): Promise<any> {
344 // Build query parameters
345 let queryParams: URLSearchParams | undefined;
346
347 if (config?.params) {
348 queryParams = new URLSearchParams();
349 Object.entries(config.params).forEach(([key, value]) => {
350 if (value !== undefined && value !== null) {
351 queryParams!.append(key, String(value));
352 }
353 });
354 }
355
356 return this.request(
357 endpoint,
358 {
359 method: "GET",
360 },
361 queryParams,
362 );
363 }
364
365 async getBlob(
366 endpoint: string,
367 config?: { params?: Record<string, any> },
368 ): Promise<Blob> {
369 let queryParams: URLSearchParams | undefined;
370
371 if (config?.params) {
372 queryParams = new URLSearchParams();
373 Object.entries(config.params).forEach(([key, value]) => {
374 if (value !== undefined && value !== null) {
375 queryParams!.append(key, String(value));
376 }
377 });
378 }
379
380 return this.requestBlob(endpoint, { method: "GET" }, queryParams);
381 }
382
383 async post(endpoint: string, data?: any): Promise<any> {
384 return this.request(endpoint, {
385 method: "POST",
386 body: data instanceof FormData ? data : JSON.stringify(data),
387 });
388 }
389
390 async put(endpoint: string, data?: any): Promise<any> {
391 return this.request(endpoint, {
392 method: "PUT",
393 body: data instanceof FormData ? data : JSON.stringify(data),
394 });
395 }
396
397 async patch(endpoint: string, data?: any): Promise<any> {
398 return this.request(endpoint, {
399 method: "PATCH",
400 body: data instanceof FormData ? data : JSON.stringify(data),
401 });
402 }
403
404 async delete(endpoint: string, config?: { data?: any }): Promise<any> {
405 return this.request(endpoint, {
406 method: "DELETE",
407 body: config?.data ? JSON.stringify(config.data) : undefined,
408 });
409 }
410 }
411
412 export const apiClient = new ApiClient();
413
414 /**
415 * GET /payments/{id} returns the payment entity as the JSON body. Some admin screens expect
416 * { success: true, data: payment }; normalize so both shapes work.
417 */
418 async function fetchPaymentNormalized(
419 id: string | number,
420 ): Promise<
421 | { success: true; data: Record<string, unknown> }
422 | { success: false; message: string }
423 | null
424 > {
425 const raw = await apiClient.get(API_ENDPOINTS.PAYMENT_GET(id));
426 if (raw == null) {
427 return null;
428 }
429 if (typeof raw !== "object") {
430 return null;
431 }
432 const r = raw as Record<string, unknown>;
433 if (r.success === true && r.data != null && typeof r.data === "object") {
434 return { success: true, data: r.data as Record<string, unknown> };
435 }
436 if (r.id !== undefined && r.id !== null) {
437 return { success: true, data: r as Record<string, unknown> };
438 }
439 return {
440 success: false,
441 message: String(r.message ?? "Payment not found"),
442 };
443 }
444
445 class WpApiClient {
446 private baseUrl: string;
447 private nonce: string;
448
449 constructor() {
450 const rawUrl = (window as any)?.yatraAdmin?.restUrl || "/wp-json";
451 this.baseUrl = rawUrl.endsWith("/") ? rawUrl.slice(0, -1) : rawUrl;
452 this.nonce = (window as any)?.yatraAdmin?.nonce || "";
453 }
454
455 async get(endpoint: string): Promise<any> {
456 const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
457 const url = `${this.baseUrl}${cleanEndpoint}`;
458
459 const response = await fetch(url, {
460 method: "GET",
461 headers: {
462 "X-WP-Nonce": this.nonce,
463 },
464 credentials: "include",
465 });
466
467 if (!response.ok) {
468 const error = await response
469 .json()
470 .catch(() => ({ message: "An error occurred" }));
471 throw new Error(
472 error.message || `HTTP error! status: ${response.status}`,
473 );
474 }
475
476 return response.json();
477 }
478 }
479
480 const wpClient = new WpApiClient();
481
482 export const wpService = {
483 getMedia: (id: string | number) => wpClient.get(`/wp/v2/media/${id}`),
484 };
485
486 export const ajaxService = {
487 post: async (action: string, data: Record<string, any>) => {
488 const siteUrl = (window as any)?.yatraAdmin?.siteUrl || "";
489 const url = `${siteUrl}/wp-admin/admin-ajax.php`;
490
491 const body = new URLSearchParams({
492 action,
493 ...Object.fromEntries(
494 Object.entries(data).map(([k, v]) => [k, v == null ? "" : String(v)]),
495 ),
496 });
497
498 const response = await fetch(url, {
499 method: "POST",
500 headers: {
501 "Content-Type": "application/x-www-form-urlencoded",
502 },
503 body,
504 credentials: "include",
505 });
506
507 return response.json();
508 },
509 };
510
511 // Convenience methods for common operations
512 export const apiService = {
513 // Bookings
514 getBookings: (params?: Record<string, any>) =>
515 apiClient.get(API_ENDPOINTS.BOOKINGS, { params }),
516 getBooking: (id: string | number) =>
517 apiClient.get(API_ENDPOINTS.BOOKING_GET(id)),
518 getTripAvailableDates: (id: string | number) =>
519 apiClient.get(API_ENDPOINTS.TRIP_AVAILABLE_DATES(id)),
520 createBooking: (data: any) => apiClient.post(API_ENDPOINTS.BOOKINGS, data),
521 updateBooking: (id: string | number, data: any) =>
522 apiClient.put(API_ENDPOINTS.BOOKING_GET(id), data),
523 updateBookingStatus: (id: string | number, status: string) =>
524 apiClient.put(API_ENDPOINTS.BOOKING_STATUS(id), { status }),
525 deleteBooking: (id: string | number) =>
526 apiClient.delete(API_ENDPOINTS.BOOKING_DELETE(id)),
527 getBookingsStats: () => apiClient.get(API_ENDPOINTS.BOOKINGS_STATS),
528
529 // Customers
530 getCustomers: (params?: Record<string, any>) =>
531 apiClient.get(API_ENDPOINTS.CUSTOMERS, { params }),
532 getCustomer: (id: string | number) =>
533 apiClient.get(API_ENDPOINTS.CUSTOMER_GET(id)),
534 deleteCustomer: (id: string | number) =>
535 apiClient.delete(API_ENDPOINTS.CUSTOMER_DELETE(id)),
536 createCustomer: (data: any) => apiClient.post(API_ENDPOINTS.CUSTOMERS, data),
537 updateCustomer: (id: string | number, data: any) =>
538 apiClient.put(API_ENDPOINTS.CUSTOMER_GET(id), data),
539 updateCustomerStatus: (id: string | number, status: string) =>
540 apiClient.put(API_ENDPOINTS.CUSTOMER_GET(id), { status }),
541 getCustomerBookings: (id: string | number) =>
542 apiClient.get(API_ENDPOINTS.CUSTOMER_BOOKINGS(id)),
543 getCustomerStats: () => apiClient.get(API_ENDPOINTS.CUSTOMER_STATS),
544
545 // Travelers
546 getTravelers: (params?: Record<string, any>) =>
547 apiClient.get(API_ENDPOINTS.TRAVELERS, { params }),
548 bulkTravelersAction: (action: string, ids: (string | number)[]) =>
549 apiClient.put(API_ENDPOINTS.TRAVELERS_BULK, { action, ids }),
550
551 // Reviews
552 getReviews: (params?: Record<string, any>) =>
553 apiClient.get(API_ENDPOINTS.REVIEWS, { params }),
554 deleteReview: (id: string | number) =>
555 apiClient.delete(API_ENDPOINTS.REVIEW_DELETE(id)),
556 updateReviewStatus: (id: string | number, status: string) =>
557 apiClient.put(API_ENDPOINTS.REVIEW_STATUS(id), { status }),
558 bulkReviewsAction: (action: string, ids: (string | number)[]) =>
559 apiClient.put(API_ENDPOINTS.REVIEWS_BULK, { action, ids }),
560
561 // Trips
562 getTrips: (params?: Record<string, any>) =>
563 apiClient.get(API_ENDPOINTS.TRIPS, { params }),
564 getTrip: (id: string | number) => apiClient.get(API_ENDPOINTS.TRIP_GET(id)),
565 deleteTrip: (id: string | number) =>
566 apiClient.delete(API_ENDPOINTS.TRIP_DELETE(id)),
567 duplicateTrip: (id: string | number) =>
568 apiClient.post(API_ENDPOINTS.TRIP_DUPLICATE(id)),
569
570 // Settings
571 getSettings: (group?: string) =>
572 apiClient.get(
573 group ? API_ENDPOINTS.SETTINGS_GROUP(group) : API_ENDPOINTS.SETTINGS,
574 ),
575 // Booking form config as a trip's checkout renders it (per-trip conditions
576 // resolved). Without tripId: the global config, conditions included.
577 getBookingFormConfig: (tripId?: number | null) =>
578 apiClient.get(API_ENDPOINTS.SETTINGS_BOOKING_FORM, {
579 params: tripId ? { trip_id: tripId } : undefined,
580 }),
581
582 // Notices
583 getNotices: () => apiClient.get(API_ENDPOINTS.NOTICES),
584 dismissNotice: (id: string) =>
585 apiClient.post(API_ENDPOINTS.NOTICE_DISMISS(id), {}),
586
587 // Payments
588 getPayment: (id: string | number) => fetchPaymentNormalized(id),
589 deletePayment: (id: string | number) =>
590 apiClient.delete(API_ENDPOINTS.PAYMENT_DELETE(id)),
591 getPayments: (params?: Record<string, any>) =>
592 apiClient.get(API_ENDPOINTS.PAYMENTS, { params }),
593 getPaymentsStats: () => apiClient.get(API_ENDPOINTS.PAYMENTS_STATS),
594 createPayment: (data: any) => apiClient.post(API_ENDPOINTS.PAYMENTS, data),
595 updatePayment: (id: string | number, data: any) =>
596 apiClient.put(API_ENDPOINTS.PAYMENT_GET(id), data),
597 updatePaymentStatus: (id: string | number, status: string) =>
598 apiClient.put(API_ENDPOINTS.PAYMENT_GET(id), { status }),
599 bulkPaymentsAction: (action: string, ids: (string | number)[]) =>
600 Promise.all(
601 ids.map((id) => {
602 if (action === "delete") {
603 return apiClient.delete(API_ENDPOINTS.PAYMENT_DELETE(id));
604 } else {
605 return apiClient.put(API_ENDPOINTS.PAYMENT_GET(id), {
606 status: action,
607 });
608 }
609 }),
610 ),
611
612 // Modules
613 getModules: () => apiClient.get(API_ENDPOINTS.MODULES),
614
615 // Facebook Pixel
616 getFacebookPixelSettings: () =>
617 apiClient.get(API_ENDPOINTS.FACEBOOK_PIXEL_SETTINGS),
618 testFacebookPixel: (pixelId: string) =>
619 apiClient.post(API_ENDPOINTS.FACEBOOK_PIXEL_TEST, { pixel_id: pixelId }),
620 testFacebookPixelToken: (accessToken: string) =>
621 apiClient.post(API_ENDPOINTS.FACEBOOK_PIXEL_TEST_TOKEN, {
622 access_token: accessToken,
623 }),
624 getFacebookPixelEvents: () =>
625 apiClient.get(API_ENDPOINTS.FACEBOOK_PIXEL_EVENTS),
626 getFacebookPixelEventLogs: () =>
627 apiClient.get(API_ENDPOINTS.FACEBOOK_PIXEL_EVENT_LOGS),
628 clearFacebookPixelEventLogs: () =>
629 apiClient.delete(API_ENDPOINTS.FACEBOOK_PIXEL_EVENT_LOGS),
630
631 // Google Analytics 4
632 getGoogleAnalyticsSettings: () =>
633 apiClient.get(API_ENDPOINTS.GOOGLE_ANALYTICS_SETTINGS),
634 testGoogleAnalytics: (measurementId: string) =>
635 apiClient.post(API_ENDPOINTS.GOOGLE_ANALYTICS_TEST, {
636 measurement_id: measurementId,
637 }),
638 validateGoogleAnalyticsMeasurementId: (measurementId: string) =>
639 apiClient.post(API_ENDPOINTS.GOOGLE_ANALYTICS_VALIDATE_MEASUREMENT_ID, {
640 measurement_id: measurementId,
641 }),
642 validateGoogleAnalyticsApiSecret: (
643 measurementId: string,
644 apiSecret: string,
645 ) =>
646 apiClient.post(API_ENDPOINTS.GOOGLE_ANALYTICS_VALIDATE_API_SECRET, {
647 measurement_id: measurementId,
648 api_secret: apiSecret,
649 }),
650 getGoogleAnalyticsEvents: () =>
651 apiClient.get(API_ENDPOINTS.GOOGLE_ANALYTICS_EVENTS),
652 getGoogleAnalyticsEventLogs: () =>
653 apiClient.get(API_ENDPOINTS.GOOGLE_ANALYTICS_EVENT_LOGS),
654 clearGoogleAnalyticsEventLogs: () =>
655 apiClient.delete(API_ENDPOINTS.GOOGLE_ANALYTICS_EVENT_LOGS),
656
657 // Payment Gateways
658 getPaymentGateways: () => apiClient.get(API_ENDPOINTS.PAYMENT_GATEWAYS),
659
660 // Abandoned Bookings
661 getAbandonedBookings: (params?: Record<string, any>) =>
662 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS, { params }),
663 getAbandonedBooking: (id: string | number) =>
664 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKING_GET(id)),
665 deleteAbandonedBooking: (id: string | number) =>
666 apiClient.delete(API_ENDPOINTS.ABANDONED_BOOKING_DELETE(id)),
667 getAbandonedBookingsSettings: () =>
668 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_SETTINGS),
669 saveAbandonedBookingsSettings: (data: any) =>
670 apiClient.post(API_ENDPOINTS.ABANDONED_BOOKINGS_SETTINGS, data),
671 getAbandonedBookingsStatistics: () =>
672 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_STATISTICS),
673 sendAbandonedBookingEmail: (id: string | number) =>
674 apiClient.post(API_ENDPOINTS.ABANDONED_BOOKING_SEND_EMAIL(id)),
675 getAbandonedBookingCampaigns: () =>
676 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGNS),
677 getAbandonedBookingCampaign: (id: string | number) =>
678 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGN_GET(id)),
679 createAbandonedBookingCampaign: (data: any) =>
680 apiClient.post(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGNS, data),
681 updateAbandonedBookingCampaign: (id: string | number, data: any) =>
682 apiClient.put(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGN_GET(id), data),
683
684 // Enquiries
685 getEnquiries: (params?: Record<string, any>) =>
686 apiClient.get(API_ENDPOINTS.ENQUIRIES, { params }),
687 getEnquiry: (id: string | number) =>
688 apiClient.get(API_ENDPOINTS.ENQUIRY_GET(id)),
689 deleteEnquiry: (id: string | number) =>
690 apiClient.delete(API_ENDPOINTS.ENQUIRY_DELETE(id)),
691 createEnquiry: (data: any) => apiClient.post(API_ENDPOINTS.ENQUIRIES, data),
692 updateEnquiry: (id: string | number, data: any) =>
693 apiClient.put(API_ENDPOINTS.ENQUIRY_GET(id), data),
694 getEnquiriesStats: () => apiClient.get(API_ENDPOINTS.ENQUIRY_STATS),
695 bulkEnquiriesAction: (action: string, ids: (string | number)[]) =>
696 apiClient.put(API_ENDPOINTS.ENQUIRIES_BULK, { action, ids }),
697 respondToEnquiry: (id: string | number, data: any) =>
698 apiClient.post(API_ENDPOINTS.ENQUIRY_RESPOND(id), data),
699
700 // Scheduled balance payments (Pro module)
701 getScheduledPayments: (params?: Record<string, any>) =>
702 apiClient.get(API_ENDPOINTS.SCHEDULED_PAYMENTS, { params }),
703 getScheduledPaymentsStats: () =>
704 apiClient.get(API_ENDPOINTS.SCHEDULED_PAYMENTS_STATS),
705 cancelScheduledPayment: (id: string | number) =>
706 apiClient.post(API_ENDPOINTS.SCHEDULED_PAYMENT_CANCEL(id), {}),
707 getOutstandingBalances: (params?: Record<string, any>) =>
708 apiClient.get(API_ENDPOINTS.SCHEDULED_PAYMENTS_OUTSTANDING, { params }),
709 sendBalancePaymentLink: (bookingId: string | number) =>
710 apiClient.post(API_ENDPOINTS.SCHEDULED_PAYMENT_SEND_LINK(bookingId), {}),
711
712 // Google Calendar
713 getGoogleCalendarSettings: () =>
714 apiClient.get(API_ENDPOINTS.GOOGLE_CALENDAR_SETTINGS),
715 connectGoogleCalendar: () =>
716 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_CONNECT),
717 disconnectGoogleCalendar: () =>
718 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_DISCONNECT),
719 syncAllGoogleCalendar: () =>
720 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_SYNC_ALL),
721 updateGoogleCalendarSettings: (data: any) =>
722 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_SETTINGS, data),
723
724 // Signed Consents
725 getSignedConsents: (params?: Record<string, any>) =>
726 apiClient.get(API_ENDPOINTS.SIGNED_CONSENTS, { params }),
727 getSignedConsent: (id: string | number) =>
728 apiClient.get(API_ENDPOINTS.SIGNED_CONSENT_GET(id)),
729 downloadSignedConsentPdf: (id: string | number) =>
730 apiClient.get(API_ENDPOINTS.SIGNED_CONSENT_PDF(id)),
731 previewSignedConsent: () =>
732 apiClient.get(API_ENDPOINTS.SIGNED_CONSENTS_PREVIEW),
733
734 // Tools
735 getSystemStatus: () => apiClient.get(API_ENDPOINTS.TOOLS_SYSTEM_STATUS),
736 getActiveJobs: () => apiClient.get(API_ENDPOINTS.TOOLS_ACTIVE_JOBS),
737 getLogs: (type: string, page: number) =>
738 apiClient.get(API_ENDPOINTS.TOOLS_LOGS(type, page)),
739 clearLogs: (type: string) =>
740 apiClient.delete(API_ENDPOINTS.TOOLS_LOGS_CLEAR(type)),
741 createExportJob: (data: any) =>
742 apiClient.post(API_ENDPOINTS.TOOLS_EXPORT_JOB, data),
743 performJobAction: (endpoint: string, jobId: string) =>
744 apiClient.get(API_ENDPOINTS.TOOLS_JOB_ACTION(endpoint, jobId)),
745 downloadExportJob: (jobId: string) =>
746 apiClient.get(API_ENDPOINTS.TOOLS_EXPORT_DOWNLOAD(jobId)),
747 downloadExportJobBlob: (jobId: string) =>
748 apiClient.getBlob(API_ENDPOINTS.TOOLS_EXPORT_DOWNLOAD(jobId)),
749 deleteExportJob: (jobId: string) =>
750 apiClient.delete(API_ENDPOINTS.TOOLS_EXPORT_DELETE(jobId)),
751 getExportJobStatus: (jobId: string) =>
752 apiClient.get(API_ENDPOINTS.TOOLS_EXPORT_STATUS(jobId)),
753 createImportJob: (data: any) =>
754 apiClient.post(API_ENDPOINTS.TOOLS_IMPORT_JOB, data),
755 getImportJob: (jobId: string) =>
756 apiClient.get(API_ENDPOINTS.TOOLS_IMPORT_JOB_GET(jobId)),
757 deleteImportJob: (jobId: string) =>
758 apiClient.delete(API_ENDPOINTS.TOOLS_IMPORT_JOB_GET(jobId)),
759 getAllJobs: () => apiClient.get(API_ENDPOINTS.TOOLS_ALL_JOBS),
760 getCronJobs: () => apiClient.get(API_ENDPOINTS.TOOLS_CRON_JOBS),
761 runCronJob: (hook: string) =>
762 apiClient.post(API_ENDPOINTS.TOOLS_CRON_RUN(hook)),
763 clearCache: () => apiClient.delete(API_ENDPOINTS.TOOLS_CLEAR_CACHE),
764 getCacheView: () => apiClient.get(API_ENDPOINTS.TOOLS_CACHE_VIEW),
765 clearCacheItem: (key: string, type: string) =>
766 apiClient.delete(
767 `${API_ENDPOINTS.TOOLS_CACHE_CLEAR_ITEM}?key=${encodeURIComponent(key)}&type=${encodeURIComponent(type)}`,
768 ),
769
770 // Migration
771 getMigrationStatus: () => apiClient.get(API_ENDPOINTS.MIGRATION_STATUS),
772 clearMigration: () => apiClient.post(API_ENDPOINTS.MIGRATION_CLEAR),
773 getMigrationProgress: () => apiClient.get(API_ENDPOINTS.MIGRATION_PROGRESS),
774 runMigrationAll: (data?: any) =>
775 apiClient.post(API_ENDPOINTS.MIGRATION_MIGRATE_ALL, data),
776 cancelMigration: () => apiClient.post(API_ENDPOINTS.MIGRATION_CANCEL),
777
778 // Sample Data
779 importSampleData: (data: any) => apiClient.post("/sample-data/import", data),
780 getSampleDataStatus: () => apiClient.get("/sample-data/status"),
781 cleanupSampleData: () => apiClient.delete("/sample-data/cleanup"),
782
783 // Common bulk operations
784 bulkDelete: (endpoint: string, ids: (string | number)[]) =>
785 Promise.all(ids.map((id) => apiClient.delete(`${endpoint}/${id}`))),
786
787 bulkUpdateStatus: (
788 endpoint: string,
789 ids: (string | number)[],
790 status: string,
791 ) =>
792 Promise.all(
793 ids.map((id) => apiClient.put(`${endpoint}/${id}/status`, { status })),
794 ),
795 };
796
797 // Format time with AM/PM for display
798 export const formatTimeForDisplay = (timeString: string): string => {
799 if (!timeString) return "";
800
801 // Try to parse the time string
802 const time = new Date(`1970-01-01T${timeString}`);
803 if (isNaN(time.getTime())) {
804 return timeString; // Return original if invalid
805 }
806
807 // Format using JavaScript's Intl.DateTimeFormat for localized time display
808 return time.toLocaleTimeString([], {
809 hour: "numeric",
810 minute: "2-digit",
811 hour12: true,
812 });
813 };
814