EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
apiClient.ts
Go to the documentation of this file.
1// Client-side API helper for secure server communication
2// Replaces direct Fieldpine API calls with server-side proxy calls
3
4interface ApiResponse<T = any> {
5 success: boolean;
6 data?: T;
7 error?: string;
8}
9
10class ApiClient {
11 private baseUrl: string;
12
13 constructor() {
14 this.baseUrl = '/api';
15 }
16
17 // Generic request method for API calls
18 async request<T = any>(
19 endpoint: string,
20 options: RequestInit = {}
21 ): Promise<ApiResponse<T>> {
22 return this.requestInternal(endpoint, options);
23 }
24
25 // Generic GET method - convenience wrapper
26 async get<T = any>(endpoint: string): Promise<T> {
27 const response = await this.requestInternal<T>(endpoint);
28 // For backward compatibility, return just the data
29 // Check if response has the structure we expect
30 if (response && typeof response === 'object' && 'success' in response) {
31 return response as T;
32 }
33 return response as T;
34 }
35
36 private async requestInternal<T = any>(
37 endpoint: string,
38 options: RequestInit = {}
39 ): Promise<ApiResponse<T>> {
40 const url = `${this.baseUrl}${endpoint}`;
41
42 const defaultHeaders = {
43 'Content-Type': 'application/json',
44 };
45
46 try {
47 const response = await fetch(url, {
48 ...options,
49 headers: {
50 ...defaultHeaders,
51 ...options.headers,
52 },
53 credentials: 'same-origin', // Include cookies
54 });
55
56 // Handle 401 Unauthorized - redirect to login
57 if (response.status === 401) {
58 console.log('401 Unauthorized - redirecting to login');
59 // Use window.location for immediate redirect
60 if (typeof window !== 'undefined') {
61 window.location.href = '/pages/login';
62 }
63 return {
64 success: false,
65 error: 'Authentication required',
66 };
67 }
68
69 const result = await response.json();
70
71 if (!response.ok) {
72 throw new Error(result.error || `HTTP ${response.status}`);
73 }
74
75 return result;
76 } catch (error) {
77 console.error(`API request failed: ${endpoint}`, error);
78 return {
79 success: false,
80 error: error instanceof Error ? error.message : 'Unknown error',
81 };
82 }
83 }
84
85 // Authentication
86 async login(username: string, password: string, storeId?: string): Promise<ApiResponse<{ user: any; store?: any; redirect: string }>> {
87 return this.requestInternal('/auth/login', {
88 method: 'POST',
89 body: JSON.stringify({ username, password, storeId }),
90 });
91 }
92
93 async logout(): Promise<ApiResponse> {
94 return this.requestInternal('/auth/logout', { method: 'POST' });
95 }
96
97 // Products
98 async getProducts(params: {
99 search?: string;
100 plu?: string;
101 barcode?: string;
102 limit?: number;
103 skip?: number;
104 filter?: string;
105 source?: 'openapi' | 'elink'; // Optional source preference
106 } = {}): Promise<ApiResponse<any[]>> {
107 const query = new URLSearchParams(
108 Object.entries(params).reduce((acc, [key, value]) => {
109 if (value !== undefined && key !== 'source') {
110 acc[key] = value.toString();
111 }
112 return acc;
113 }, {} as Record<string, string>)
114 ).toString();
115
116 // Determine which API version to use
117 const apiVersion = params.source || 'openapi'; // Default to OpenAPI
118 const endpoint = query ? `/v1/${apiVersion}/products?${query}` : `/v1/${apiVersion}/products`;
119
120 // Try primary choice first, then fallback
121 try {
122 return await this.requestInternal(endpoint);
123 } catch (error) {
124 // Fallback to alternative API if primary fails
125 if (apiVersion === 'openapi') {
126 const fallbackEndpoint = query ? `/v1/elink/products?${query}` : '/v1/elink/products';
127 return this.requestInternal(fallbackEndpoint);
128 } else {
129 const fallbackEndpoint = query ? `/v1/openapi/products?${query}` : '/v1/openapi/products';
130 return this.requestInternal(fallbackEndpoint);
131 }
132 }
133 }
134
135 async getProductById(id: string): Promise<ApiResponse<any>> {
136 return this.requestInternal(`/products/${id}`);
137 }
138
139 async getProduct(id: string | number): Promise<ApiResponse<any>> {
140 return this.requestInternal(`/v1/openapi/products/${id}`);
141 }
142
143 // Sales
144 async createSale(saleData: any): Promise<ApiResponse<any>> {
145 return this.requestInternal('/sales', {
146 method: 'POST',
147 body: JSON.stringify(saleData),
148 });
149 }
150
151 // Dashboard
152 async getDashboard(): Promise<ApiResponse<any>> {
153 return this.requestInternal('/dashboard');
154 }
155
156 // Customers
157 async getCustomers(params: {
158 search?: string;
159 limit?: number;
160 type?: 'list' | 'single' | 'search';
161 id?: number;
162 source?: 'openapi' | 'elink'; // Optional source preference
163 } = {}): Promise<ApiResponse<any[]>> {
164 const query = new URLSearchParams(
165 Object.entries(params).reduce((acc, [key, value]) => {
166 if (value !== undefined && key !== 'source') {
167 acc[key] = value.toString();
168 }
169 return acc;
170 }, {} as Record<string, string>)
171 ).toString();
172
173 // Determine which API version to use
174 const apiVersion = params.source || 'openapi'; // Default to OpenAPI
175 const endpoint = query ? `/v1/${apiVersion}/customers?${query}` : `/v1/${apiVersion}/customers`;
176
177 // Try primary choice first, then fallback
178 try {
179 return await this.requestInternal(endpoint);
180 } catch (error) {
181 // Fallback to alternative API if primary fails
182 if (apiVersion === 'openapi') {
183 const fallbackEndpoint = query ? `/v1/elink/customers?${query}` : '/v1/elink/customers';
184 return this.requestInternal(fallbackEndpoint);
185 } else {
186 const fallbackEndpoint = query ? `/v1/openapi/customers?${query}` : '/v1/openapi/customers';
187 return this.requestInternal(fallbackEndpoint);
188 }
189 }
190 }
191
192 // Suppliers
193 async getSuppliers(params: {
194 search?: string;
195 limit?: number;
196 source?: 'openapi' | 'elink'; // Optional source preference
197 } = {}): Promise<ApiResponse<any[]>> {
198 const query = new URLSearchParams(
199 Object.entries(params).reduce((acc, [key, value]) => {
200 if (value !== undefined && key !== 'source') {
201 acc[key] = value.toString();
202 }
203 return acc;
204 }, {} as Record<string, string>)
205 ).toString();
206
207 // Determine which API version to use
208 const apiVersion = params.source || 'openapi'; // Default to OpenAPI
209 const endpoint = query ? `/v1/${apiVersion}/suppliers?${query}` : `/v1/${apiVersion}/suppliers`;
210
211 return this.requestInternal(endpoint);
212 }
213
214 // OpenAPI Methods - Comprehensive implementation
215
216 // Products Search with Natural Language
217 async searchProducts(params: {
218 querytext?: string;
219 want?: string;
220 like?: string;
221 limit?: number;
222 option?: string;
223 ref?: string;
224 inputmethod?: string;
225 } = {}): Promise<ApiResponse<any[]>> {
226 const query = new URLSearchParams(
227 Object.entries(params).reduce((acc, [key, value]) => {
228 if (value !== undefined) acc[key] = value.toString();
229 return acc;
230 }, {} as Record<string, string>)
231 ).toString();
232 return this.requestInternal(query ? `/v1/openapi/products-search?${query}` : '/v1/openapi/products-search');
233 }
234
235 // Barcodes
236 async decodeBarcode(scan: string, geo?: string): Promise<ApiResponse<any>> {
237 const params = new URLSearchParams({ scan });
238 if (geo) params.append('geo', geo);
239 return this.requestInternal(`/v1/openapi/barcodes?${params.toString()}`);
240 }
241
242 // WebGroups
243 async getWebGroups(): Promise<ApiResponse<any[]>> {
244 return this.requestInternal('/v1/openapi/webgroups');
245 }
246
247 // Reorder Levels
248 async getReorderLevels(params: {
249 pid?: number;
250 locid?: number;
251 want?: string;
252 } = {}): Promise<ApiResponse<any[]>> {
253 const query = new URLSearchParams(
254 Object.entries(params).reduce((acc, [key, value]) => {
255 if (value !== undefined) acc[key] = value.toString();
256 return acc;
257 }, {} as Record<string, string>)
258 ).toString();
259 return this.requestInternal(query ? `/v1/openapi/reorder-levels?${query}` : '/v1/openapi/reorder-levels');
260 }
261
262 async setReorderLevel(locid: number, pid: number, levels: {
263 ReorderLevel?: number;
264 ReorderMax?: number;
265 }): Promise<ApiResponse<any>> {
266 return this.requestInternal(`/v1/openapi/reorder-levels/${locid}/${pid}`, {
267 method: 'POST',
268 body: JSON.stringify(levels)
269 });
270 }
271
272 async deleteReorderLevel(locid: number, pid: number): Promise<ApiResponse<any>> {
273 return this.requestInternal(`/v1/openapi/reorder-levels/${locid}/${pid}`, {
274 method: 'DELETE'
275 });
276 }
277
278 // Supplier Part Codes
279 async getSupplierPartCodes(params: {
280 spid?: number;
281 pid?: number;
282 enabled?: boolean;
283 } = {}): Promise<ApiResponse<any[]>> {
284 const query = new URLSearchParams(
285 Object.entries(params).reduce((acc, [key, value]) => {
286 if (value !== undefined) acc[key] = value.toString();
287 return acc;
288 }, {} as Record<string, string>)
289 ).toString();
290 return this.requestInternal(query ? `/v1/openapi/supplier-part-codes?${query}` : '/v1/openapi/supplier-part-codes');
291 }
292
293 // Purchase Orders
294 async getPurchaseOrders(params: {
295 location?: number;
296 spid?: number;
297 state?: string;
298 } = {}): Promise<ApiResponse<any[]>> {
299 const query = new URLSearchParams(
300 Object.entries(params).reduce((acc, [key, value]) => {
301 if (value !== undefined) acc[key] = value.toString();
302 return acc;
303 }, {} as Record<string, string>)
304 ).toString();
305 return this.requestInternal(query ? `/v1/openapi/purchase-orders?${query}` : '/v1/openapi/purchase-orders');
306 }
307
308 async createPurchaseOrder(order: any): Promise<ApiResponse<any>> {
309 return this.requestInternal('/v1/openapi/purchase-orders', {
310 method: 'POST',
311 body: JSON.stringify(order)
312 });
313 }
314
315 async parsePurchaseOrder(text: string): Promise<ApiResponse<any>> {
316 return this.requestInternal(`/v1/openapi/purchase-order/parse?text=${encodeURIComponent(text)}`);
317 }
318
319 // Stock2 - Stock Movements
320 async getStockMovements(params: {
321 locid?: number;
322 pid?: number;
323 from?: string;
324 to?: string;
325 detail?: string;
326 } = {}): Promise<ApiResponse<any[]>> {
327 const query = new URLSearchParams(
328 Object.entries(params).reduce((acc, [key, value]) => {
329 if (value !== undefined) acc[key] = value.toString();
330 return acc;
331 }, {} as Record<string, string>)
332 ).toString();
333 return this.requestInternal(query ? `/v1/openapi/stock2/movements?${query}` : '/v1/openapi/stock2/movements');
334 }
335
336 // Stock2 - Stock Counts
337 async getStockCounts(params: {
338 locid?: number;
339 pid?: number;
340 depid?: number;
341 want?: string;
342 } = {}): Promise<ApiResponse<any[]>> {
343 const query = new URLSearchParams(
344 Object.entries(params).reduce((acc, [key, value]) => {
345 if (value !== undefined) acc[key] = value.toString();
346 return acc;
347 }, {} as Record<string, string>)
348 ).toString();
349 return this.requestInternal(query ? `/v1/openapi/stock2/count?${query}` : '/v1/openapi/stock2/count');
350 }
351
352 async recordStockCount(count: any): Promise<ApiResponse<any>> {
353 return this.requestInternal('/v1/openapi/stock2/count', {
354 method: 'POST',
355 body: JSON.stringify(count)
356 });
357 }
358
359 async deleteStockCount(key: string): Promise<ApiResponse<any>> {
360 return this.requestInternal(`/v1/openapi/stock2/count/${key}`, {
361 method: 'DELETE'
362 });
363 }
364
365 async getStockCountSummary(params: {
366 locid?: number;
367 pid?: number;
368 depid?: number;
369 } = {}): Promise<ApiResponse<any[]>> {
370 const query = new URLSearchParams(
371 Object.entries(params).reduce((acc, [key, value]) => {
372 if (value !== undefined) acc[key] = value.toString();
373 return acc;
374 }, {} as Record<string, string>)
375 ).toString();
376 return this.requestInternal(query ? `/v1/openapi/stock2/count-summary?${query}` : '/v1/openapi/stock2/count-summary');
377 }
378
379 // Shelf Visits
380 async getShelfVisits(params: {
381 locid?: number;
382 pid?: number;
383 from?: string;
384 to?: string;
385 } = {}): Promise<ApiResponse<any[]>> {
386 const query = new URLSearchParams(
387 Object.entries(params).reduce((acc, [key, value]) => {
388 if (value !== undefined) acc[key] = value.toString();
389 return acc;
390 }, {} as Record<string, string>)
391 ).toString();
392 return this.requestInternal(query ? `/v1/openapi/shelf-visits?${query}` : '/v1/openapi/shelf-visits');
393 }
394
395 async recordShelfVisit(visit: any, params?: { CardNo?: string; Pass?: string }): Promise<ApiResponse<any>> {
396 const query = params ? new URLSearchParams(
397 Object.entries(params).reduce((acc, [key, value]) => {
398 if (value !== undefined) acc[key] = value;
399 return acc;
400 }, {} as Record<string, string>)
401 ).toString() : '';
402 return this.requestInternal(query ? `/v1/openapi/shelf-visits?${query}` : '/v1/openapi/shelf-visits', {
403 method: 'POST',
404 body: JSON.stringify(visit)
405 });
406 }
407
408 // Stocktakes
409 async getStocktakes(params: {
410 location?: number;
411 state?: string;
412 } = {}): Promise<ApiResponse<any[]>> {
413 const query = new URLSearchParams(
414 Object.entries(params).reduce((acc, [key, value]) => {
415 if (value !== undefined) acc[key] = value.toString();
416 return acc;
417 }, {} as Record<string, string>)
418 ).toString();
419 return this.requestInternal(query ? `/v1/openapi/stocktakes?${query}` : '/v1/openapi/stocktakes');
420 }
421
422 async getBuckStocktakes(params: {
423 filter?: 'current' | 'complete' | 'all';
424 } = {}): Promise<ApiResponse<any>> {
425 const query = new URLSearchParams(
426 Object.entries(params).reduce((acc, [key, value]) => {
427 if (value !== undefined) acc[key] = value.toString();
428 return acc;
429 }, {} as Record<string, string>)
430 ).toString();
431 return this.requestInternal(query ? `/v1/buck/stocktakes?${query}` : '/v1/buck/stocktakes');
432 }
433
434 async getBuckStocktakeStatus(id: number): Promise<ApiResponse<any>> {
435 return this.requestInternal(`/v1/buck/stocktake/status?id=${id}`);
436 }
437
438 // Stock Transfers
439 async getStockTransfer(id: string, srcuid?: number): Promise<ApiResponse<any>> {
440 const query = srcuid ? `?srcuid=${srcuid}` : '';
441 return this.requestInternal(`/v1/openapi/stock-transfers/${id}${query}`);
442 }
443
444 async receiveStockTransfer(id: string, location?: number): Promise<ApiResponse<any>> {
445 const query = location ? `?location=${location}` : '';
446 return this.requestInternal(`/v1/openapi/stock-transfers/${id}/receive${query}`, {
447 method: 'POST'
448 });
449 }
450
451 // Product Writeoff
452 async writeoffProduct(id: string, params: {
453 location?: number;
454 qty?: number;
455 reason?: number;
456 }): Promise<ApiResponse<any>> {
457 const query = new URLSearchParams(
458 Object.entries(params).reduce((acc, [key, value]) => {
459 if (value !== undefined) acc[key] = value.toString();
460 return acc;
461 }, {} as Record<string, string>)
462 ).toString();
463 return this.requestInternal(`/v1/openapi/products/${id}/writeoff?${query}`, {
464 method: 'POST'
465 });
466 }
467
468 // Pricebook Specials
469 async getPricebookSpecials(params: {
470 spid?: number;
471 from?: string;
472 to?: string;
473 } = {}): Promise<ApiResponse<any[]>> {
474 const query = new URLSearchParams(
475 Object.entries(params).reduce((acc, [key, value]) => {
476 if (value !== undefined) acc[key] = value.toString();
477 return acc;
478 }, {} as Record<string, string>)
479 ).toString();
480 return this.requestInternal(query ? `/v1/openapi/pricebook-specials?${query}` : '/v1/openapi/pricebook-specials');
481 }
482
483 // Supplier Marketing Messages
484 async getSupplierMarketingMessages(params: {
485 spid?: number;
486 pid?: number;
487 from?: string;
488 to?: string;
489 } = {}): Promise<ApiResponse<any[]>> {
490 const query = new URLSearchParams(
491 Object.entries(params).reduce((acc, [key, value]) => {
492 if (value !== undefined) acc[key] = value.toString();
493 return acc;
494 }, {} as Record<string, string>)
495 ).toString();
496 return this.requestInternal(query ? `/v1/openapi/supplier-marketing-messages?${query}` : '/v1/openapi/supplier-marketing-messages');
497 }
498
499 // Customer Accounts (uses elink/BUCK API)
500 async getAccounts(params: { type?: 'list' | 'summary' | 'search'; search?: string } = {}): Promise<ApiResponse<any[]>> {
501 const query = new URLSearchParams(
502 Object.entries(params).reduce((acc, [key, value]) => {
503 if (value !== undefined) acc[key] = value.toString();
504 return acc;
505 }, {} as Record<string, string>)
506 ).toString();
507 return this.requestInternal(query ? `/v1/accounts?${query}` : '/v1/accounts');
508 }
509
510 async createOrUpdateAccount(data: any): Promise<ApiResponse<any>> {
511 return this.requestInternal('/v1/accounts', {
512 method: 'POST',
513 body: JSON.stringify({
514 action: data.id ? 'update' : 'create',
515 data
516 })
517 });
518 }
519
520 async getRecentAccountSales(): Promise<ApiResponse<any[]>> {
521 return this.requestInternal('/v1/accounts/sales');
522 }
523
524 async getAccountCustomers(accountId: number): Promise<ApiResponse<any[]>> {
525 return this.requestInternal(`/v1/accounts/customers?accountId=${accountId}`);
526 }
527
528 async linkCustomerToAccount(customerId: number, accountId: number): Promise<ApiResponse<any>> {
529 return this.requestInternal('/v1/accounts/customers', {
530 method: 'POST',
531 body: JSON.stringify({
532 action: 'link',
533 customerId,
534 accountId
535 })
536 });
537 }
538
539 async unlinkCustomerFromAccount(customerId: number): Promise<ApiResponse<any>> {
540 return this.requestInternal('/v1/accounts/customers', {
541 method: 'POST',
542 body: JSON.stringify({
543 action: 'unlink',
544 customerId,
545 accountId: 0
546 })
547 });
548 }
549
550 // Sales Reports
551 async searchSales(params: {
552 query: string;
553 limit?: number;
554 ref?: number;
555 inputMethod?: string;
556 }): Promise<ApiResponse<any>> {
557 const query = new URLSearchParams(
558 Object.entries(params).reduce((acc, [key, value]) => {
559 if (value !== undefined) acc[key] = value.toString();
560 return acc;
561 }, {} as Record<string, string>)
562 ).toString();
563 return this.requestInternal(`/v1/sales/search?${query}`);
564 }
565
566 // Locations
567 async getLocations(params: { source?: 'openapi' | 'elink'; want?: string } = {}): Promise<ApiResponse<any[]>> {
568 const query = new URLSearchParams(
569 Object.entries(params).reduce((acc, [key, value]) => {
570 if (value !== undefined) acc[key] = value.toString();
571 return acc;
572 }, {} as Record<string, string>)
573 ).toString();
574 return this.requestInternal(query ? `/v1/locations?${query}` : '/v1/locations');
575 }
576
577 async saveLocation(data: {
578 id?: number;
579 name: string;
580 phone?: string;
581 email?: string;
582 latitude?: number;
583 longitude?: number;
584 storeType?: string;
585 }): Promise<ApiResponse<any>> {
586 return this.requestInternal('/v1/locations', {
587 method: 'POST',
588 body: JSON.stringify(data),
589 });
590 }
591
592 // Departments
593 // async getDepartments(params: { want?: string } = {}): Promise<ApiResponse<any[]>> {
594 // const query = new URLSearchParams(
595 // Object.entries(params).reduce((acc, [key, value]) => {
596 // if (value !== undefined) acc[key] = value.toString();
597 // return acc;
598 // }, {} as Record<string, string>)
599 // ).toString();
600 // return this.requestInternal(query ? `/v1/departments?${query}` : '/v1/departments');
601 // }
602
603 // Payment Types
604 async getPaymentTypes(params: { want?: string } = {}): Promise<ApiResponse<any[]>> {
605 const query = new URLSearchParams(
606 Object.entries(params).reduce((acc, [key, value]) => {
607 if (value !== undefined) acc[key] = value.toString();
608 return acc;
609 }, {} as Record<string, string>)
610 ).toString();
611 return this.requestInternal(query ? `/v1/openapi/payment-types?${query}` : '/v1/openapi/payment-types');
612 }
613
614 // Discounts
615 async getDiscounts(): Promise<ApiResponse<any[]>> {
616 return this.requestInternal('/v1/discounts');
617 }
618
619 // Stocktake
620 async lookupProductByBarcode(barcode: string): Promise<ApiResponse<any>> {
621 return this.requestInternal(`/v1/stocktake?barcode=${encodeURIComponent(barcode)}`);
622 }
623
624 async recordStocktakeCount(params: {
625 productId: number;
626 quantity: number;
627 barcode: string;
628 storeId?: string;
629 reference?: string;
630 }): Promise<ApiResponse<any>> {
631 return this.requestInternal('/v1/stocktake', {
632 method: 'POST',
633 body: JSON.stringify(params)
634 });
635 }
636
637 // Account Financial Data (Aged Debtors, etc.)
638 async getAccountFinancialData(params: {
639 limit?: number;
640 search?: string;
641 showZeroBalance?: boolean;
642 showRetired?: boolean;
643 } = {}): Promise<ApiResponse<any[]>> {
644 const query = new URLSearchParams(
645 Object.entries(params).reduce((acc, [key, value]) => {
646 if (value !== undefined) acc[key] = value.toString();
647 return acc;
648 }, {} as Record<string, string>)
649 ).toString();
650 return this.requestInternal(query ? `/v1/accounts/financial?${query}` : '/v1/accounts/financial');
651 }
652
653 // Payment Types
654 async getPaymentTypesList(): Promise<ApiResponse<any[]>> {
655 return this.requestInternal('/v1/payment-types');
656 }
657
658 // Price Changes
659 async getPriceChanges(params: {
660 limit?: number;
661 search?: string;
662 pluList?: string;
663 showType?: string;
664 } = {}): Promise<ApiResponse<any[]>> {
665 const query = new URLSearchParams(
666 Object.entries(params).reduce((acc, [key, value]) => {
667 if (value !== undefined) acc[key] = value.toString();
668 return acc;
669 }, {} as Record<string, string>)
670 ).toString();
671 return this.requestInternal(query ? `/v1/price-changes?${query}` : '/v1/price-changes');
672 }
673
674 // Sales List
675 async getSalesList(params: {
676 limit?: number;
677 fields?: string;
678 filters?: string[];
679 } = {}): Promise<ApiResponse<any>> {
680 const query = new URLSearchParams();
681 if (params.limit) query.append('limit', params.limit.toString());
682 if (params.fields) query.append('fields', params.fields);
683 if (params.filters) {
684 params.filters.forEach(filter => query.append('filter', filter));
685 }
686 return this.requestInternal(`/v1/sales/list?${query.toString()}`);
687 }
688
689 // Sales by Customer
690 async getSales(params: {
691 customerId?: number;
692 accountId?: number;
693 limit?: number;
694 from?: string;
695 to?: string;
696 } = {}): Promise<ApiResponse<any[]>> {
697 const query = new URLSearchParams(
698 Object.entries(params).reduce((acc, [key, value]) => {
699 if (value !== undefined) acc[key] = value.toString();
700 return acc;
701 }, {} as Record<string, string>)
702 ).toString();
703 return this.requestInternal(query ? `/v1/sales?${query}` : '/v1/sales');
704 }
705
706 // Contact Logs
707 async getContactLogs(params: {
708 customerId?: number;
709 accountId?: number;
710 limit?: number;
711 } = {}): Promise<ApiResponse<any[]>> {
712 const query = new URLSearchParams(
713 Object.entries(params).reduce((acc, [key, value]) => {
714 if (value !== undefined) acc[key] = value.toString();
715 return acc;
716 }, {} as Record<string, string>)
717 ).toString();
718 return this.requestInternal(query ? `/v1/contact-logs?${query}` : '/v1/contact-logs');
719 }
720
721 async saveContactLog(params: {
722 customerId?: number;
723 accountId?: number;
724 message: string;
725 reminder?: string;
726 }): Promise<ApiResponse<any>> {
727 return this.requestInternal('/v1/contact-logs', {
728 method: 'POST',
729 body: JSON.stringify(params)
730 });
731 }
732
733 // Customer Statistics and Analytics
734 async getCustomerStats(params: {
735 customerId: number;
736 type?: 'product-performance' | 'brands';
737 limit?: number;
738 }): Promise<ApiResponse<any>> {
739 const query = new URLSearchParams(
740 Object.entries(params).reduce((acc, [key, value]) => {
741 if (value !== undefined) acc[key] = value.toString();
742 return acc;
743 }, {} as Record<string, string>)
744 ).toString();
745 return this.requestInternal(query ? `/v1/stats?${query}` : '/v1/stats');
746 }
747
748 async getDepartments(): Promise<ApiResponse<any[]>> {
749 return this.requestInternal('/v1/elink/departments');
750 }
751
752 async getDepartmentHierarchy(): Promise<ApiResponse<Record<number, number[]>>> {
753 return this.requestInternal('/v1/departments/hierarchy');
754 }
755
756 async saveDepartmentHierarchy(hierarchy: Record<number, number[]>): Promise<ApiResponse<any>> {
757 return this.requestInternal('/v1/departments/hierarchy', {
758 method: 'POST',
759 body: JSON.stringify({ hierarchy }),
760 });
761 }
762
763 // Update Customer
764 async updateCustomer(
765 customerId: number,
766 data: {
767 name?: string;
768 company?: string;
769 phone?: string;
770 phone2?: string;
771 mobile?: string;
772 email?: string;
773 division?: string;
774 }
775 ): Promise<ApiResponse<any>> {
776 return this.requestInternal(`/v1/elink/customers/${customerId}`, {
777 method: 'PUT',
778 body: JSON.stringify(data),
779 });
780 }
781
782 // Staff Management
783 async getStaff(params: {
784 id?: number;
785 includeRights?: boolean;
786 want?: string;
787 } = {}): Promise<ApiResponse<any>> {
788 const query = new URLSearchParams(
789 Object.entries(params).reduce((acc, [key, value]) => {
790 if (value !== undefined) acc[key] = value.toString();
791 return acc;
792 }, {} as Record<string, string>)
793 ).toString();
794 return this.requestInternal(query ? `/v1/elink/staff?${query}` : '/v1/elink/staff');
795 }
796
797 async getStaffUsage(params?: { staffId?: string; fromDate?: string; toDate?: string }): Promise<ApiResponse<any>> {
798 const queryParams = new URLSearchParams();
799 if (params?.staffId) queryParams.append('staffId', params.staffId);
800 if (params?.fromDate) queryParams.append('fromDate', params.fromDate);
801 if (params?.toDate) queryParams.append('toDate', params.toDate);
802
803 const queryString = queryParams.toString();
804 const endpoint = queryString ? `/v1/elink/staff/used?${queryString}` : '/v1/elink/staff/used';
805
806 return this.requestInternal(endpoint);
807 }
808
809 async getStaffUsageDetail(params: { staffId: string; fromDate?: string; toDate?: string }): Promise<ApiResponse<any>> {
810 const queryParams = new URLSearchParams();
811 queryParams.append('staffId', params.staffId);
812 if (params?.fromDate) queryParams.append('fromDate', params.fromDate);
813 if (params?.toDate) queryParams.append('toDate', params.toDate);
814
815 const queryString = queryParams.toString();
816 return this.requestInternal(`/v1/elink/staff/used/detail?${queryString}`);
817 }
818
819 async getStaffRights(): Promise<ApiResponse<any>> {
820 return this.requestInternal('/v1/elink/staff/rights');
821 }
822
823 async saveStaff(data: {
824 id?: number;
825 name: string;
826 active?: boolean;
827 password?: string;
828 barcode?: string;
829 formalName?: string;
830 mainLocation?: string;
831 printedName?: string;
832 firstName?: string;
833 lastName?: string;
834 nickName?: string;
835 startDate?: string;
836 leaveDate?: string;
837 privateEmail?: string;
838 workEmail?: string;
839 onlineLogin?: string;
840 onlinePassword?: string;
841 isCustomerRep?: boolean;
842 isAccountRep?: boolean;
843 remoteProfile?: number;
844 autoLoginFrom?: string;
845 loginWhere?: number;
846 roles?: number[];
847 }): Promise<ApiResponse<any>> {
848 return this.requestInternal('/v1/elink/staff', {
849 method: 'POST',
850 body: JSON.stringify(data),
851 });
852 }
853
854 // Employee Management
855 async getEmployees(params: {
856 id?: number;
857 } = {}): Promise<ApiResponse<any>> {
858 const query = new URLSearchParams(
859 Object.entries(params).reduce((acc, [key, value]) => {
860 if (value !== undefined) acc[key] = value.toString();
861 return acc;
862 }, {} as Record<string, string>)
863 ).toString();
864 return this.requestInternal(query ? `/v1/elink/employees?${query}` : '/v1/elink/employees');
865 }
866
867 async saveEmployee(data: {
868 id?: number;
869 shortName: string;
870 fullName: string;
871 posLoginId?: string;
872 startDate?: string;
873 endDate?: string;
874 mainLocation?: string;
875 jobTitle?: string;
876 jobCode?: string;
877 departmentId?: string;
878 externalId?: string;
879 nztaUid?: string;
880 }): Promise<ApiResponse<any>> {
881 return this.requestInternal('/v1/elink/employees', {
882 method: 'POST',
883 body: JSON.stringify(data),
884 });
885 }
886
887 async getTopology(): Promise<ApiResponse<any>> {
888 return this.requestInternal('/v1/elink/topology');
889 }
890
891 // Reports - Sales Detail (line-by-line)
892 async getSalesDetail(params: {
893 startDate?: string;
894 endDate?: string;
895 locationId?: string;
896 departmentId?: string;
897 limit?: number;
898 } = {}): Promise<ApiResponse<any[]>> {
899 const query = new URLSearchParams(
900 Object.entries(params).reduce((acc, [key, value]) => {
901 if (value !== undefined) acc[key] = value.toString();
902 return acc;
903 }, {} as Record<string, string>)
904 ).toString();
905 return this.requestInternal(query ? `/v1/reports/sales-detail?${query}` : '/v1/reports/sales-detail');
906 }
907
908 // Reports - Grouped Sales
909 async getSalesGrouped(params: {
910 startDate?: string;
911 endDate?: string;
912 groupBy?: 'product' | 'supplier' | 'department' | 'location' | 'teller';
913 limit?: number;
914 } = {}): Promise<ApiResponse<any[]>> {
915 const query = new URLSearchParams(
916 Object.entries(params).reduce((acc, [key, value]) => {
917 if (value !== undefined) acc[key] = value.toString();
918 return acc;
919 }, {} as Record<string, string>)
920 ).toString();
921 return this.requestInternal(query ? `/v1/reports/sales-grouped?${query}` : '/v1/reports/sales-grouped');
922 }
923
924 // Analytics - Pareto Analysis (80/20 rule)
925 async getParetoAnalysis(params: {
926 startDate?: string;
927 endDate?: string;
928 type?: 'basketrate' | 'grossmargin' | 'returns' | 'volume' | 'revenue';
929 limit?: number;
930 } = {}): Promise<ApiResponse<any[]>> {
931 const query = new URLSearchParams(
932 Object.entries(params).reduce((acc, [key, value]) => {
933 if (value !== undefined) acc[key] = value.toString();
934 return acc;
935 }, {} as Record<string, string>)
936 ).toString();
937 return this.requestInternal(query ? `/v1/analytics/pareto?${query}` : '/v1/analytics/pareto');
938 }
939
940 // Reports - Purchase Orders Summary
941 async getPurchaseOrdersSummary(params: {
942 startDate?: string;
943 endDate?: string;
944 status?: 'pending' | 'received' | 'cancelled';
945 supplierId?: string;
946 limit?: number;
947 } = {}): Promise<ApiResponse<any[]>> {
948 const query = new URLSearchParams(
949 Object.entries(params).reduce((acc, [key, value]) => {
950 if (value !== undefined) acc[key] = value.toString();
951 return acc;
952 }, {} as Record<string, string>)
953 ).toString();
954 return this.requestInternal(query ? `/v1/reports/purchase-orders?${query}` : '/v1/reports/purchase-orders');
955 }
956
957 // Inventory - Goods Movements
958 async getGoodsMovements(params: {
959 startDate?: string;
960 endDate?: string;
961 type?: 'transfer' | 'adjustment' | 'writeoff' | 'stocktake' | 'return';
962 locationId?: string;
963 productId?: string;
964 limit?: number;
965 } = {}): Promise<ApiResponse<any[]>> {
966 const query = new URLSearchParams(
967 Object.entries(params).reduce((acc, [key, value]) => {
968 if (value !== undefined) acc[key] = value.toString();
969 return acc;
970 }, {} as Record<string, string>)
971 ).toString();
972 return this.requestInternal(query ? `/v1/inventory/movements?${query}` : '/v1/inventory/movements');
973 }
974
975 // Analytics - Product Performance (Most critical - 432 uses!)
976 async getProductPerformance(params: {
977 customerId?: string;
978 locationId?: string;
979 groupBy?: 'pid' | 'product' | 'spid' | 'supplier' | 'dept' | 'department';
980 sortBy?: '1' | '2' | '3'; // 1=volume, 2=revenue, 3=profit
981 limit?: number;
982 } = {}): Promise<ApiResponse<any[]>> {
983 const query = new URLSearchParams(
984 Object.entries(params).reduce((acc, [key, value]) => {
985 if (value !== undefined) acc[key] = value.toString();
986 return acc;
987 }, {} as Record<string, string>)
988 ).toString();
989 return this.requestInternal(query ? `/v1/analytics/product-performance?${query}` : '/v1/analytics/product-performance');
990 }
991
992 // Configuration - Base Settings
993 async getBaseSettings(params: {
994 key?: string;
995 section?: string;
996 } = {}): Promise<ApiResponse<any[]>> {
997 const query = new URLSearchParams(
998 Object.entries(params).reduce((acc, [key, value]) => {
999 if (value !== undefined) acc[key] = value.toString();
1000 return acc;
1001 }, {} as Record<string, string>)
1002 ).toString();
1003 return this.requestInternal(query ? `/v1/config/base-settings?${query}` : '/v1/config/base-settings');
1004 }
1005
1006 async updateBaseSetting(key: string, value: any): Promise<ApiResponse<any>> {
1007 return this.requestInternal('/v1/config/base-settings', {
1008 method: 'POST',
1009 body: JSON.stringify({ key, value })
1010 });
1011 }
1012
1013 // Reports - Sales Totals
1014 async getSalesTotals(params: {
1015 startDate?: string;
1016 endDate?: string;
1017 groupBy?: 'day' | 'week' | 'month' | 'location' | 'teller' | 'department';
1018 locationId?: string;
1019 tellerId?: string;
1020 sortBy?: 'date' | 'revenue' | 'units';
1021 } = {}): Promise<ApiResponse<any[]>> {
1022 const query = new URLSearchParams(
1023 Object.entries(params).reduce((acc, [key, value]) => {
1024 if (value !== undefined) acc[key] = value.toString();
1025 return acc;
1026 }, {} as Record<string, string>)
1027 ).toString();
1028 return this.requestInternal(query ? `/v1/reports/sales-totals?${query}` : '/v1/reports/sales-totals');
1029 }
1030
1031 // Stats - Today's Statistics (for dashboard)
1032 async getStatsToday(params: {
1033 locationId?: string;
1034 } = {}): Promise<ApiResponse<any>> {
1035 const query = new URLSearchParams(
1036 Object.entries(params).reduce((acc, [key, value]) => {
1037 if (value !== undefined) acc[key] = value.toString();
1038 return acc;
1039 }, {} as Record<string, string>)
1040 ).toString();
1041 return this.requestInternal(query ? `/v1/stats/today?${query}` : '/v1/stats/today');
1042 }
1043 // BUCK Data - Loyalty Campaigns and Printer-Cartridge relationships
1044 async getBuckData(params: {
1045 table: string;
1046 want?: string;
1047 filter?: string;
1048 limit?: number;
1049 }): Promise<ApiResponse<any>> {
1050 // Support loyalty campaigns
1051 if (params.table === 'retailmax.elink.loyalty.campaign') {
1052 return this.requestInternal('/v1/buck/loyalty/campaigns');
1053 }
1054 // Support printer-cartridge relationships
1055 if (params.table === 'retailmax.elink.printercartridge') {
1056 const query = new URLSearchParams();
1057 if (params.want) query.append('want', params.want);
1058 if (params.filter) query.append('filter', params.filter);
1059 if (params.limit) query.append('limit', params.limit.toString());
1060 const queryString = query.toString();
1061 return this.requestInternal(queryString ? `/v1/elink/printer-cartridge?${queryString}` : '/v1/elink/printer-cartridge');
1062 }
1063 throw new Error(`BUCK table ${params.table} not supported yet`);
1064 }}
1065
1066export const apiClient = new ApiClient();
1067export type { ApiResponse };