PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.11 All 82 releases
yatra / resources / js / lib / api-client.ts

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

796 lines 25.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
576 // Notices
577 getNotices: () => apiClient.get(API_ENDPOINTS.NOTICES),
578 dismissNotice: (id: string) =>
579 apiClient.post(API_ENDPOINTS.NOTICE_DISMISS(id), {}),
580
581 // Payments
582 getPayment: (id: string | number) => fetchPaymentNormalized(id),
583 deletePayment: (id: string | number) =>
584 apiClient.delete(API_ENDPOINTS.PAYMENT_DELETE(id)),
585 getPayments: (params?: Record<string, any>) =>
586 apiClient.get(API_ENDPOINTS.PAYMENTS, { params }),
587 getPaymentsStats: () => apiClient.get(API_ENDPOINTS.PAYMENTS_STATS),
588 createPayment: (data: any) => apiClient.post(API_ENDPOINTS.PAYMENTS, data),
589 updatePayment: (id: string | number, data: any) =>
590 apiClient.put(API_ENDPOINTS.PAYMENT_GET(id), data),
591 updatePaymentStatus: (id: string | number, status: string) =>
592 apiClient.put(API_ENDPOINTS.PAYMENT_GET(id), { status }),
593 bulkPaymentsAction: (action: string, ids: (string | number)[]) =>
594 Promise.all(
595 ids.map((id) => {
596 if (action === "delete") {
597 return apiClient.delete(API_ENDPOINTS.PAYMENT_DELETE(id));
598 } else {
599 return apiClient.put(API_ENDPOINTS.PAYMENT_GET(id), {
600 status: action,
601 });
602 }
603 }),
604 ),
605
606 // Modules
607 getModules: () => apiClient.get(API_ENDPOINTS.MODULES),
608
609 // Facebook Pixel
610 getFacebookPixelSettings: () =>
611 apiClient.get(API_ENDPOINTS.FACEBOOK_PIXEL_SETTINGS),
612 testFacebookPixel: (pixelId: string) =>
613 apiClient.post(API_ENDPOINTS.FACEBOOK_PIXEL_TEST, { pixel_id: pixelId }),
614 testFacebookPixelToken: (accessToken: string) =>
615 apiClient.post(API_ENDPOINTS.FACEBOOK_PIXEL_TEST_TOKEN, {
616 access_token: accessToken,
617 }),
618 getFacebookPixelEvents: () =>
619 apiClient.get(API_ENDPOINTS.FACEBOOK_PIXEL_EVENTS),
620 getFacebookPixelEventLogs: () =>
621 apiClient.get(API_ENDPOINTS.FACEBOOK_PIXEL_EVENT_LOGS),
622 clearFacebookPixelEventLogs: () =>
623 apiClient.delete(API_ENDPOINTS.FACEBOOK_PIXEL_EVENT_LOGS),
624
625 // Google Analytics 4
626 getGoogleAnalyticsSettings: () =>
627 apiClient.get(API_ENDPOINTS.GOOGLE_ANALYTICS_SETTINGS),
628 testGoogleAnalytics: (measurementId: string) =>
629 apiClient.post(API_ENDPOINTS.GOOGLE_ANALYTICS_TEST, {
630 measurement_id: measurementId,
631 }),
632 validateGoogleAnalyticsMeasurementId: (measurementId: string) =>
633 apiClient.post(API_ENDPOINTS.GOOGLE_ANALYTICS_VALIDATE_MEASUREMENT_ID, {
634 measurement_id: measurementId,
635 }),
636 validateGoogleAnalyticsApiSecret: (
637 measurementId: string,
638 apiSecret: string,
639 ) =>
640 apiClient.post(API_ENDPOINTS.GOOGLE_ANALYTICS_VALIDATE_API_SECRET, {
641 measurement_id: measurementId,
642 api_secret: apiSecret,
643 }),
644 getGoogleAnalyticsEvents: () =>
645 apiClient.get(API_ENDPOINTS.GOOGLE_ANALYTICS_EVENTS),
646 getGoogleAnalyticsEventLogs: () =>
647 apiClient.get(API_ENDPOINTS.GOOGLE_ANALYTICS_EVENT_LOGS),
648 clearGoogleAnalyticsEventLogs: () =>
649 apiClient.delete(API_ENDPOINTS.GOOGLE_ANALYTICS_EVENT_LOGS),
650
651 // Payment Gateways
652 getPaymentGateways: () => apiClient.get(API_ENDPOINTS.PAYMENT_GATEWAYS),
653
654 // Abandoned Bookings
655 getAbandonedBookings: (params?: Record<string, any>) =>
656 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS, { params }),
657 getAbandonedBooking: (id: string | number) =>
658 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKING_GET(id)),
659 deleteAbandonedBooking: (id: string | number) =>
660 apiClient.delete(API_ENDPOINTS.ABANDONED_BOOKING_DELETE(id)),
661 getAbandonedBookingsSettings: () =>
662 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_SETTINGS),
663 saveAbandonedBookingsSettings: (data: any) =>
664 apiClient.post(API_ENDPOINTS.ABANDONED_BOOKINGS_SETTINGS, data),
665 getAbandonedBookingsStatistics: () =>
666 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_STATISTICS),
667 sendAbandonedBookingEmail: (id: string | number) =>
668 apiClient.post(API_ENDPOINTS.ABANDONED_BOOKING_SEND_EMAIL(id)),
669 getAbandonedBookingCampaigns: () =>
670 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGNS),
671 getAbandonedBookingCampaign: (id: string | number) =>
672 apiClient.get(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGN_GET(id)),
673 createAbandonedBookingCampaign: (data: any) =>
674 apiClient.post(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGNS, data),
675 updateAbandonedBookingCampaign: (id: string | number, data: any) =>
676 apiClient.put(API_ENDPOINTS.ABANDONED_BOOKINGS_CAMPAIGN_GET(id), data),
677
678 // Enquiries
679 getEnquiries: (params?: Record<string, any>) =>
680 apiClient.get(API_ENDPOINTS.ENQUIRIES, { params }),
681 getEnquiry: (id: string | number) =>
682 apiClient.get(API_ENDPOINTS.ENQUIRY_GET(id)),
683 deleteEnquiry: (id: string | number) =>
684 apiClient.delete(API_ENDPOINTS.ENQUIRY_DELETE(id)),
685 createEnquiry: (data: any) => apiClient.post(API_ENDPOINTS.ENQUIRIES, data),
686 updateEnquiry: (id: string | number, data: any) =>
687 apiClient.put(API_ENDPOINTS.ENQUIRY_GET(id), data),
688 getEnquiriesStats: () => apiClient.get(API_ENDPOINTS.ENQUIRY_STATS),
689 bulkEnquiriesAction: (action: string, ids: (string | number)[]) =>
690 apiClient.put(API_ENDPOINTS.ENQUIRIES_BULK, { action, ids }),
691 respondToEnquiry: (id: string | number, data: any) =>
692 apiClient.post(API_ENDPOINTS.ENQUIRY_RESPOND(id), data),
693
694 // Google Calendar
695 getGoogleCalendarSettings: () =>
696 apiClient.get(API_ENDPOINTS.GOOGLE_CALENDAR_SETTINGS),
697 connectGoogleCalendar: () =>
698 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_CONNECT),
699 disconnectGoogleCalendar: () =>
700 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_DISCONNECT),
701 syncAllGoogleCalendar: () =>
702 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_SYNC_ALL),
703 updateGoogleCalendarSettings: (data: any) =>
704 apiClient.post(API_ENDPOINTS.GOOGLE_CALENDAR_SETTINGS, data),
705
706 // Signed Consents
707 getSignedConsents: (params?: Record<string, any>) =>
708 apiClient.get(API_ENDPOINTS.SIGNED_CONSENTS, { params }),
709 getSignedConsent: (id: string | number) =>
710 apiClient.get(API_ENDPOINTS.SIGNED_CONSENT_GET(id)),
711 downloadSignedConsentPdf: (id: string | number) =>
712 apiClient.get(API_ENDPOINTS.SIGNED_CONSENT_PDF(id)),
713 previewSignedConsent: () =>
714 apiClient.get(API_ENDPOINTS.SIGNED_CONSENTS_PREVIEW),
715
716 // Tools
717 getSystemStatus: () => apiClient.get(API_ENDPOINTS.TOOLS_SYSTEM_STATUS),
718 getActiveJobs: () => apiClient.get(API_ENDPOINTS.TOOLS_ACTIVE_JOBS),
719 getLogs: (type: string, page: number) =>
720 apiClient.get(API_ENDPOINTS.TOOLS_LOGS(type, page)),
721 clearLogs: (type: string) =>
722 apiClient.delete(API_ENDPOINTS.TOOLS_LOGS_CLEAR(type)),
723 createExportJob: (data: any) =>
724 apiClient.post(API_ENDPOINTS.TOOLS_EXPORT_JOB, data),
725 performJobAction: (endpoint: string, jobId: string) =>
726 apiClient.get(API_ENDPOINTS.TOOLS_JOB_ACTION(endpoint, jobId)),
727 downloadExportJob: (jobId: string) =>
728 apiClient.get(API_ENDPOINTS.TOOLS_EXPORT_DOWNLOAD(jobId)),
729 downloadExportJobBlob: (jobId: string) =>
730 apiClient.getBlob(API_ENDPOINTS.TOOLS_EXPORT_DOWNLOAD(jobId)),
731 deleteExportJob: (jobId: string) =>
732 apiClient.delete(API_ENDPOINTS.TOOLS_EXPORT_DELETE(jobId)),
733 getExportJobStatus: (jobId: string) =>
734 apiClient.get(API_ENDPOINTS.TOOLS_EXPORT_STATUS(jobId)),
735 createImportJob: (data: any) =>
736 apiClient.post(API_ENDPOINTS.TOOLS_IMPORT_JOB, data),
737 getImportJob: (jobId: string) =>
738 apiClient.get(API_ENDPOINTS.TOOLS_IMPORT_JOB_GET(jobId)),
739 deleteImportJob: (jobId: string) =>
740 apiClient.delete(API_ENDPOINTS.TOOLS_IMPORT_JOB_GET(jobId)),
741 getAllJobs: () => apiClient.get(API_ENDPOINTS.TOOLS_ALL_JOBS),
742 getCronJobs: () => apiClient.get(API_ENDPOINTS.TOOLS_CRON_JOBS),
743 runCronJob: (hook: string) =>
744 apiClient.post(API_ENDPOINTS.TOOLS_CRON_RUN(hook)),
745 clearCache: () => apiClient.delete(API_ENDPOINTS.TOOLS_CLEAR_CACHE),
746 getCacheView: () => apiClient.get(API_ENDPOINTS.TOOLS_CACHE_VIEW),
747 clearCacheItem: (key: string, type: string) =>
748 apiClient.delete(
749 `${API_ENDPOINTS.TOOLS_CACHE_CLEAR_ITEM}?key=${encodeURIComponent(key)}&type=${encodeURIComponent(type)}`,
750 ),
751
752 // Migration
753 getMigrationStatus: () => apiClient.get(API_ENDPOINTS.MIGRATION_STATUS),
754 clearMigration: () => apiClient.post(API_ENDPOINTS.MIGRATION_CLEAR),
755 getMigrationProgress: () => apiClient.get(API_ENDPOINTS.MIGRATION_PROGRESS),
756 runMigrationAll: (data?: any) =>
757 apiClient.post(API_ENDPOINTS.MIGRATION_MIGRATE_ALL, data),
758 cancelMigration: () => apiClient.post(API_ENDPOINTS.MIGRATION_CANCEL),
759
760 // Sample Data
761 importSampleData: (data: any) => apiClient.post("/sample-data/import", data),
762 getSampleDataStatus: () => apiClient.get("/sample-data/status"),
763 cleanupSampleData: () => apiClient.delete("/sample-data/cleanup"),
764
765 // Common bulk operations
766 bulkDelete: (endpoint: string, ids: (string | number)[]) =>
767 Promise.all(ids.map((id) => apiClient.delete(`${endpoint}/${id}`))),
768
769 bulkUpdateStatus: (
770 endpoint: string,
771 ids: (string | number)[],
772 status: string,
773 ) =>
774 Promise.all(
775 ids.map((id) => apiClient.put(`${endpoint}/${id}/status`, { status })),
776 ),
777 };
778
779 // Format time with AM/PM for display
780 export const formatTimeForDisplay = (timeString: string): string => {
781 if (!timeString) return "";
782
783 // Try to parse the time string
784 const time = new Date(`1970-01-01T${timeString}`);
785 if (isNaN(time.getTime())) {
786 return timeString; // Return original if invalid
787 }
788
789 // Format using JavaScript's Intl.DateTimeFormat for localized time display
790 return time.toLocaleTimeString([], {
791 hour: "numeric",
792 minute: "2-digit",
793 hour12: true,
794 });
795 };
796