@suflon/rnmd-reporting 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suflon/rnmd-reporting",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -19058,10 +19058,10 @@ index 0000000..5b11a80
19058
19058
  +export default ReportingDetail;
19059
19059
  diff --git a/node_modules/@suflon/native-ui/src/modules/Reporting/index.tsx b/node_modules/@suflon/native-ui/src/modules/Reporting/index.tsx
19060
19060
  new file mode 100644
19061
- index 0000000..9c355ad
19061
+ index 0000000..98773be
19062
19062
  --- /dev/null
19063
19063
  +++ b/node_modules/@suflon/native-ui/src/modules/Reporting/index.tsx
19064
- @@ -0,0 +1,264 @@
19064
+ @@ -0,0 +1,249 @@
19065
19065
  +import React, { useEffect, useState, useMemo, useCallback } from 'react';
19066
19066
  +import { ScrollView, TouchableOpacity, View, Text, SafeAreaView, useColorScheme } from 'react-native';
19067
19067
  +import { useNavigation } from '@react-navigation/native';
@@ -19144,7 +19144,7 @@ index 0000000..9c355ad
19144
19144
  + const iconMutedColor = isDark ? '#9CA3AF' : themeColors.slate[400];
19145
19145
  +
19146
19146
  + return (
19147
- + <SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#141518' : '#F8FAFC' }}>
19147
+ + <SafeAreaView className="flex-1 bg-slate-50 dark:bg-background-lightBlack">
19148
19148
  + {/* Header Component */}
19149
19149
  + <Header
19150
19150
  + title="Reports Catalog"
@@ -19152,9 +19152,9 @@ index 0000000..9c355ad
19152
19152
  + showActionIcon={false}
19153
19153
  + />
19154
19154
  +
19155
- + <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 30 }}>
19155
+ + <ScrollView className="flex-1" contentContainerStyle={{ paddingBottom: 30 }}>
19156
19156
  + {/* Search Bar */}
19157
- + <View style={{ paddingHorizontal: 16, paddingTop: 14 }}>
19157
+ + <View className="px-4 pt-3.5">
19158
19158
  + <SearchFilter
19159
19159
  + placeholder="Search reports or creator..."
19160
19160
  + value={searchQuery}
@@ -19164,12 +19164,12 @@ index 0000000..9c355ad
19164
19164
  +
19165
19165
  + {/* Quick Access Featured Carousel */}
19166
19166
  + {featuredPicks.length > 0 && (
19167
- + <View style={{ paddingTop: 16 }}>
19168
- + <View style={{ paddingHorizontal: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
19169
- + <Text style={{ fontSize: 11, fontWeight: '800', color: isDark ? '#9CA3AF' : '#94A3B8', letterSpacing: 1 }}>
19167
+ + <View className="pt-4">
19168
+ + <View className="px-4 flex-row justify-between items-center mb-2.5">
19169
+ + <Text className="text-[11px] font-extrabold text-slate-400 dark:text-gray-400 tracking-wider">
19170
19170
  + FEATURED REPORTS
19171
19171
  + </Text>
19172
- + <Text style={{ fontSize: 11, fontWeight: '700', color: isDark ? '#A78BFA' : themeColors.brand[600] }}>
19172
+ + <Text className="text-[11px] font-bold text-violet-600 dark:text-violet-400">
19173
19173
  + Swipe ➔
19174
19174
  + </Text>
19175
19175
  + </View>
@@ -19180,23 +19180,15 @@ index 0000000..9c355ad
19180
19180
  + key={pick.id || i}
19181
19181
  + onPress={() => handleReportPress(pick)}
19182
19182
  + activeOpacity={0.8}
19183
- + style={{
19184
- + width: 170,
19185
- + backgroundColor: isDark ? '#1C1C1E' : '#FFFFFF',
19186
- + borderColor: isDark ? '#2C2C2E' : '#E2E8F0',
19187
- + borderWidth: 1,
19188
- + borderRadius: 16,
19189
- + padding: 12,
19190
- + gap: 6,
19191
- + }}
19183
+ + className="w-[170px] bg-white dark:bg-background-darkSecondaryBg rounded-2xl p-3 border border-slate-200 dark:border-gray-800 gap-1.5"
19192
19184
  + >
19193
- + <View style={{ width: 28, height: 28, borderRadius: 8, backgroundColor: isDark ? '#2E1065' : '#F5F3FF', alignItems: 'center', justifyContent: 'center' }}>
19185
+ + <View className="w-7 h-7 rounded-lg bg-violet-50 dark:bg-violet-950/40 items-center justify-center">
19194
19186
  + <Icon name="bar-chart-2" type="Feather" size={16} color={iconBrandColor} />
19195
19187
  + </View>
19196
- + <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A', fontSize: 12, fontWeight: '800' }} numberOfLines={1}>
19188
+ + <Text className="text-xs font-extrabold text-slate-900 dark:text-text-dark-primary" numberOfLines={1}>
19197
19189
  + {pick.name}
19198
19190
  + </Text>
19199
- + <Text style={{ color: isDark ? '#9CA3AF' : '#64748B', fontSize: 10, fontWeight: '600' }} numberOfLines={1}>
19191
+ + <Text className="text-[9px] font-semibold text-slate-500 dark:text-gray-400" numberOfLines={1}>
19200
19192
  + {pick.sub_report_class ? SUB_CLASS_LABEL_MAP[pick.sub_report_class] || pick.sub_report_class : 'Report'}
19201
19193
  + </Text>
19202
19194
  + </TouchableOpacity>
@@ -19206,7 +19198,7 @@ index 0000000..9c355ad
19206
19198
  + )}
19207
19199
  +
19208
19200
  + {/* Category Pills Switcher - Dynamic from response */}
19209
- + <View style={{ paddingTop: 16 }}>
19201
+ + <View className="pt-4">
19210
19202
  + <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 16, gap: 8 }}>
19211
19203
  + {categoryTabs.map((tab) => {
19212
19204
  + const isActive = activeTabId === tab.id;
@@ -19218,17 +19210,11 @@ index 0000000..9c355ad
19218
19210
  + key={tab.id}
19219
19211
  + onPress={() => setActiveTabId(tab.id)}
19220
19212
  + activeOpacity={0.8}
19221
- + style={{
19222
- + backgroundColor: isActive ? themeColors.brand[600] : (isDark ? '#1C1C1E' : '#FFFFFF'),
19223
- + borderColor: isActive ? themeColors.brand[600] : (isDark ? '#2C2C2E' : '#E2E8F0'),
19224
- + borderWidth: 1,
19225
- + borderRadius: 9999,
19226
- + paddingHorizontal: 14,
19227
- + paddingVertical: 8,
19228
- + flexDirection: 'row',
19229
- + alignItems: 'center',
19230
- + gap: 6,
19231
- + }}
19213
+ + className={`flex-row items-center gap-1.5 px-3.5 py-2 rounded-full border ${
19214
+ + isActive
19215
+ + ? 'bg-violet-600 border-violet-600'
19216
+ + : 'bg-white dark:bg-background-darkSecondaryBg border-slate-200 dark:border-gray-800'
19217
+ + }`}
19232
19218
  + >
19233
19219
  + <Icon
19234
19220
  + name={tab.icon}
@@ -19237,20 +19223,27 @@ index 0000000..9c355ad
19237
19223
  + color={isActive ? '#FFFFFF' : (isDark ? '#FFFFFF' : themeColors.slate[600])}
19238
19224
  + />
19239
19225
  + <Text
19240
- + style={{ color: isActive ? '#FFFFFF' : (isDark ? '#FFFFFF' : '#334155'), fontSize: 11, fontWeight: '700' }}
19226
+ + className={`text-[11px] font-bold ${
19227
+ + isActive
19228
+ + ? 'text-white'
19229
+ + : 'text-slate-700 dark:text-gray-200'
19230
+ + }`}
19241
19231
  + >
19242
19232
  + {tab.label}
19243
19233
  + </Text>
19244
19234
  + <View
19245
- + style={{
19246
- + backgroundColor: isActive ? 'rgba(255, 255, 255, 0.25)' : (isDark ? '#2C2C2E' : '#F1F5F9'),
19247
- + borderRadius: 9999,
19248
- + paddingHorizontal: 6,
19249
- + paddingVertical: 2,
19250
- + }}
19235
+ + className={`rounded-full px-1.5 py-0.5 ${
19236
+ + isActive
19237
+ + ? 'bg-white/25'
19238
+ + : 'bg-slate-100 dark:bg-gray-800'
19239
+ + }`}
19251
19240
  + >
19252
19241
  + <Text
19253
- + style={{ color: isActive ? '#FFFFFF' : (isDark ? '#FFFFFF' : '#475569'), fontSize: 9, fontWeight: '800' }}
19242
+ + className={`text-[9px] font-extrabold ${
19243
+ + isActive
19244
+ + ? 'text-white'
19245
+ + : 'text-slate-600 dark:text-gray-300'
19246
+ + }`}
19254
19247
  + >
19255
19248
  + {count}
19256
19249
  + </Text>
@@ -19262,14 +19255,14 @@ index 0000000..9c355ad
19262
19255
  + </View>
19263
19256
  +
19264
19257
  + {/* Report Catalog List */}
19265
- + <View style={{ paddingHorizontal: 16, paddingTop: 14, gap: 10 }}>
19258
+ + <View className="px-4 pt-3.5 gap-2.5">
19266
19259
  + {loading ? (
19267
- + <View style={{ paddingVertical: 40 }}>
19260
+ + <View className="py-10">
19268
19261
  + <Loader message="Loading reports catalog..." color={iconBrandColor} />
19269
19262
  + </View>
19270
19263
  + ) : filteredReports.length === 0 ? (
19271
- + <View style={{ backgroundColor: isDark ? '#1C1C1E' : '#FFFFFF', padding: 32, borderRadius: 16, alignItems: 'center', marginTop: 10, borderWidth: 1, borderColor: isDark ? '#2C2C2E' : '#E2E8F0' }}>
19272
- + <Text style={{ color: isDark ? '#9CA3AF' : '#64748B', fontSize: 12, fontWeight: '600' }}>No reports match the selected category</Text>
19264
+ + <View className="bg-white dark:bg-background-darkSecondaryBg p-8 rounded-2xl items-center mt-2.5 border border-slate-200 dark:border-gray-800">
19265
+ + <Text className="text-slate-500 dark:text-gray-400 text-xs font-semibold">No reports match the selected category</Text>
19273
19266
  + </View>
19274
19267
  + ) : (
19275
19268
  + filteredReports.map((item) => (
@@ -19277,18 +19270,10 @@ index 0000000..9c355ad
19277
19270
  + key={item.id}
19278
19271
  + onPress={() => handleReportPress(item)}
19279
19272
  + activeOpacity={0.8}
19280
- + style={{
19281
- + backgroundColor: isDark ? '#1C1C1E' : '#FFFFFF',
19282
- + borderColor: isDark ? '#2C2C2E' : '#E2E8F0',
19283
- + borderWidth: 1,
19284
- + borderRadius: 16,
19285
- + padding: 14,
19286
- + flexDirection: 'row',
19287
- + alignItems: 'center',
19288
- + }}
19273
+ + className="bg-white dark:bg-background-darkSecondaryBg rounded-2xl p-3.5 border border-slate-200 dark:border-gray-800 flex-row items-center"
19289
19274
  + >
19290
- + <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1 }}>
19291
- + <View style={{ width: 40, height: 40, borderRadius: 12, backgroundColor: isDark ? '#2E1065' : '#F5F3FF', alignItems: 'center', justifyContent: 'center' }}>
19275
+ + <View className="flex-row items-center gap-3 flex-1">
19276
+ + <View className="w-10 h-10 rounded-xl bg-violet-50 dark:bg-violet-950/40 items-center justify-center">
19292
19277
  + <Icon
19293
19278
  + name={SUB_CLASS_ICON_MAP[item.sub_report_class] || (item.is_dynamic ? "pie-chart" : "file-text")}
19294
19279
  + type="Feather"
@@ -19297,23 +19282,23 @@ index 0000000..9c355ad
19297
19282
  + />
19298
19283
  + </View>
19299
19284
  +
19300
- + <View style={{ flex: 1 }}>
19301
- + <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A', fontSize: 13, fontWeight: '800' }} numberOfLines={1}>
19285
+ + <View className="flex-1">
19286
+ + <Text className="text-[13px] font-extrabold text-slate-900 dark:text-text-dark-primary" numberOfLines={1}>
19302
19287
  + {item.name}
19303
19288
  + </Text>
19304
- + <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 2 }}>
19305
- + <Text style={{ fontSize: 10, fontWeight: '700', color: isDark ? '#A78BFA' : themeColors.brand[600] }}>
19289
+ + <View className="flex-row items-center gap-1.5 mt-0.5">
19290
+ + <Text className="text-[10px] font-bold text-violet-600 dark:text-violet-400">
19306
19291
  + {SUB_CLASS_LABEL_MAP[item.sub_report_class] || item.sub_report_class || 'Report'}
19307
19292
  + </Text>
19308
- + <Text style={{ fontSize: 10, color: isDark ? '#6B7280' : '#94A3B8' }}>•</Text>
19309
- + <Text style={{ fontSize: 10, color: isDark ? '#9CA3AF' : '#64748B', fontWeight: '600' }}>
19293
+ + <Text className="text-[10px] text-slate-400 dark:text-gray-500">•</Text>
19294
+ + <Text className="text-[10px] font-semibold text-slate-500 dark:text-gray-400">
19310
19295
  + {item.report_type || 'LISTING'}
19311
19296
  + </Text>
19312
19297
  + </View>
19313
19298
  + </View>
19314
19299
  + </View>
19315
19300
  +
19316
- + <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
19301
+ + <View className="flex-row items-center gap-1.5">
19317
19302
  + <Icon name="chevron-right" type="Feather" size={18} color={iconMutedColor} />
19318
19303
  + </View>
19319
19304
  + </TouchableOpacity>
@@ -21410,15 +21395,17 @@ index 0000000..5af6cc6
21410
21395
  +
21411
21396
  +export default NewConnectionModal;
21412
21397
  diff --git a/node_modules/@suflon/native-ui/src/services/ApiService.ts b/node_modules/@suflon/native-ui/src/services/ApiService.ts
21413
- index 7613a70..b549dc4 100644
21398
+ index 7613a70..abaa6f6 100644
21414
21399
  --- a/node_modules/@suflon/native-ui/src/services/ApiService.ts
21415
21400
  +++ b/node_modules/@suflon/native-ui/src/services/ApiService.ts
21416
- @@ -1,118 +1,30 @@
21401
+ @@ -1,118 +1,44 @@
21417
21402
  -import axios, { AxiosRequestConfig } from 'axios';
21418
21403
  -import type { AxiosRequestHeaders } from 'axios';
21419
21404
  -import { useAuthStore } from '../store/auth.store';
21420
21405
  -import { getData } from '../utils/AsyncStorageUtils';
21421
- -
21406
+ +import { axiosInstance } from './api';
21407
+ +import { saveData, getData } from '@/utils/AsyncStorageUtils';
21408
+
21422
21409
  -/** Default API origin when the host has not called {@link setBaseUrl} yet. Host apps should call `setBaseUrl` at startup (from env or config). */
21423
21410
  -const DEFAULT_BASE_URL =
21424
21411
  - 'https://api-local.woctor.com';
@@ -21466,8 +21453,12 @@ index 7613a70..b549dc4 100644
21466
21453
  - const base_url = apiBaseUrl;
21467
21454
  -
21468
21455
  - return { token, company_id, region_id, staff_id, user_id, base_url };
21469
- -};
21470
- -
21456
+ +export const setBaseUrl = async (url: string) => {
21457
+ + const cleanUrl = url ? url.trim().replace(/\/+$/, '') : '';
21458
+ + await saveData('api_base_url', cleanUrl);
21459
+ + axiosInstance.defaults.baseURL = cleanUrl;
21460
+ };
21461
+
21471
21462
  -axiosInstance.interceptors.request.use(
21472
21463
  - async config => {
21473
21464
  - const { token, region_id, company_id, user_id, staff_id, base_url } = await resolveAuthContext();
@@ -21506,8 +21497,10 @@ index 7613a70..b549dc4 100644
21506
21497
  - data: body,
21507
21498
  - });
21508
21499
  - return response.data as T;
21509
- -};
21510
- +import { axiosInstance } from './api';
21500
+ +export const getBaseUrl = async (): Promise<string> => {
21501
+ + const stored = await getData<string>('api_base_url');
21502
+ + return stored || axiosInstance.defaults.baseURL || '';
21503
+ };
21511
21504
 
21512
21505
  export const ApiService = {
21513
21506
  - get: <T = any>(url: string, config?: AxiosRequestConfig) =>
@@ -21522,6 +21515,14 @@ index 7613a70..b549dc4 100644
21522
21515
  - handleRequest<T>('delete', url, undefined, config),
21523
21516
  - fetchList: <T = any>(url: string, config?: AxiosRequestConfig) =>
21524
21517
  - handleRequest<T>('get', url, undefined, config),
21518
+ -};
21519
+ -
21520
+ -export const getBaseUrl = () => apiBaseUrl;
21521
+ -export const setBaseUrl = (url: string) => {
21522
+ - const normalized = String(url || '').trim().replace(/\/+$/, '');
21523
+ - if (!normalized) return;
21524
+ - apiBaseUrl = normalized;
21525
+ - axiosInstance.defaults.baseURL = normalized;
21525
21526
  + get: async (url: string, paramsOrConfig?: Record<string, any>) => {
21526
21527
  + let config: { params?: Record<string, any> } = {};
21527
21528
  + if (paramsOrConfig) {
@@ -21549,16 +21550,9 @@ index 7613a70..b549dc4 100644
21549
21550
  + return ApiService.get(url, params);
21550
21551
  + },
21551
21552
  };
21552
- -
21553
- -export const getBaseUrl = () => apiBaseUrl;
21554
- -export const setBaseUrl = (url: string) => {
21555
- - const normalized = String(url || '').trim().replace(/\/+$/, '');
21556
- - if (!normalized) return;
21557
- - apiBaseUrl = normalized;
21558
- - axiosInstance.defaults.baseURL = normalized;
21559
- -};
21560
- -
21553
+
21561
21554
  -export default axiosInstance;
21555
+ +export { axiosInstance };
21562
21556
  diff --git a/node_modules/@suflon/native-ui/src/services/ConnectionService.ts b/node_modules/@suflon/native-ui/src/services/ConnectionService.ts
21563
21557
  new file mode 100644
21564
21558
  index 0000000..b7e3f15
@@ -25467,7 +25461,7 @@ index 221e5d1..352ad70 100644
25467
25461
  ),
25468
25462
  );
25469
25463
  diff --git a/node_modules/@suflon/native-ui/src/store/package.store.ts b/node_modules/@suflon/native-ui/src/store/package.store.ts
25470
- index bee9382..21f88e2 100644
25464
+ index bee9382..f1ccdef 100644
25471
25465
  --- a/node_modules/@suflon/native-ui/src/store/package.store.ts
25472
25466
  +++ b/node_modules/@suflon/native-ui/src/store/package.store.ts
25473
25467
  @@ -4,6 +4,8 @@ import { IPackage } from '@utils/index';
@@ -25479,7 +25473,7 @@ index bee9382..21f88e2 100644
25479
25473
  interface IPackageState {
25480
25474
  /* flags & data */
25481
25475
  loading: boolean;
25482
- @@ -12,16 +14,17 @@ interface IPackageState {
25476
+ @@ -12,196 +14,231 @@ interface IPackageState {
25483
25477
  packages: IPackage[];
25484
25478
  package: IPackage | null;
25485
25479
 
@@ -25494,13 +25488,19 @@ index bee9382..21f88e2 100644
25494
25488
  /* writes */
25495
25489
  createPackage: (data: Partial<IPackage>) => Promise<IPackage | null>;
25496
25490
  updatePackage: (id: number, data: Partial<IPackage>) => Promise<IPackage | null>;
25491
+ + updatePackageDetail: (detailId: number, payload: Record<string, any>) => Promise<any>;
25497
25492
  addPackageGroup: (packageId: number, groups: any[]) => Promise<any>;
25498
25493
  - // updatePackageDetail: (detailId: number, payload: Record<string, any>) => Promise<any>;
25499
25494
  - // updatePackageDetail: (detailId: number, payload: Record<string, any>) => Promise<any>;
25500
25495
  }
25501
25496
 
25502
25497
  /* ─────────────────────────────
25503
- @@ -32,71 +35,86 @@ const serviceUrl = `/billing/api/online/catalogue/package`;
25498
+ - Endpoint root
25499
+ + Endpoint root (Content service)
25500
+ ────────────────────────────── */
25501
+ -const serviceUrl = `/billing/api/online/catalogue/package`;
25502
+ +const serviceUrl = `/content/api/online/catalogue/package`;
25503
+
25504
25504
  /* ─────────────────────────────
25505
25505
  Store
25506
25506
  ────────────────────────────── */
@@ -25535,27 +25535,6 @@ index bee9382..21f88e2 100644
25535
25535
  - // Only include company_id for COMPANY source, not for GENERIC
25536
25536
  - if (!isGeneric && company_id) {
25537
25537
  - urlObj.searchParams.append('company_id', String(company_id));
25538
- - }
25539
- -
25540
- - // Add optional parameters
25541
- - if (params?.package_type) {
25542
- - urlObj.searchParams.append('package_type', params.package_type);
25543
- - }
25544
- - if (params?.source) {
25545
- - urlObj.searchParams.append('source', params.source);
25546
- - }
25547
- - if (params?.search_query) {
25548
- - urlObj.searchParams.append('search_query', params.search_query);
25549
- - }
25550
- -
25551
- - const url = urlObj.toString();
25552
- -
25553
- - // ✅ Debug logging for search verification
25554
- - console.log('[PackageStore] Fetching packages with URL:', url);
25555
- -
25556
- - const res = await ApiService.get(url);
25557
- - if (!Array.isArray(res)) {
25558
- - set({ error: 'Unexpected API response', errorStatus: null, packages: [] });
25559
25538
  +export const usePackageStore = create<IPackageState>()((set, get) => {
25560
25539
  + registerStore('package', get, set);
25561
25540
  +
@@ -25587,36 +25566,34 @@ index bee9382..21f88e2 100644
25587
25566
  + // Check if source is GENERIC (generic packages don't use company_id)
25588
25567
  + const isGeneric = params?.source === 'GENERIC';
25589
25568
  +
25590
- + // Build URL and query parameters
25591
- + const urlObj = new URL(serviceUrl + '/');
25569
+ + const queryParams: Record<string, any> = {};
25592
25570
  +
25593
25571
  + // Always include region_id
25594
25572
  + if (region_id) {
25595
- + urlObj.searchParams.append('region_id', String(region_id));
25573
+ + queryParams.region_id = region_id;
25596
25574
  + }
25597
25575
  +
25598
25576
  + // Only include company_id for COMPANY source, not for GENERIC
25599
25577
  + if (!isGeneric && company_id) {
25600
- + urlObj.searchParams.append('company_id', String(company_id));
25578
+ + queryParams.company_id = company_id;
25601
25579
  + }
25602
25580
  +
25603
25581
  + // Add optional parameters
25604
25582
  + if (params?.package_type) {
25605
- + urlObj.searchParams.append('package_type', params.package_type);
25583
+ + queryParams.package_type = params.package_type;
25606
25584
  + }
25607
25585
  + if (params?.source) {
25608
- + urlObj.searchParams.append('source', params.source);
25586
+ + queryParams.source = params.source;
25609
25587
  + }
25610
25588
  + if (params?.search_query) {
25611
- + urlObj.searchParams.append('search_query', params.search_query);
25589
+ + queryParams.search_query = params.search_query;
25612
25590
  + }
25613
25591
  +
25614
- + const url = urlObj.toString();
25592
+ + const url = `${serviceUrl}/`;
25615
25593
  +
25616
- + // Debug logging for search verification
25617
- + console.log('[PackageStore] Fetching packages with URL:', url);
25594
+ + console.log('[PackageStore] Fetching packages with URL:', url, 'params:', queryParams);
25618
25595
  +
25619
- + const res = await ApiService.get(url);
25596
+ + const res = await ApiService.get(url, queryParams);
25620
25597
  + if (!Array.isArray(res)) {
25621
25598
  + throw new Error('Unexpected API response');
25622
25599
  + }
@@ -25631,7 +25608,131 @@ index bee9382..21f88e2 100644
25631
25608
  + } catch (err: any) {
25632
25609
  + const status = err?.response?.status || null;
25633
25610
  + set({ error: err.response?.data?.detail || err.message || 'An error occurred', errorStatus: status });
25634
- return null;
25611
+ + return null;
25612
+ + } finally {
25613
+ + set({ loading: false });
25614
+ }
25615
+ -
25616
+ - // Add optional parameters
25617
+ - if (params?.package_type) {
25618
+ - urlObj.searchParams.append('package_type', params.package_type);
25619
+ + },
25620
+ +
25621
+ + getPackageById: async (id: number, source: string = 'COMPANY') => {
25622
+ + try {
25623
+ + set({ loading: true, error: null, errorStatus: null });
25624
+ +
25625
+ + let url: string;
25626
+ + if (source === 'GENERIC') {
25627
+ + url = `${serviceUrl}/generic/${id}`;
25628
+ + } else {
25629
+ + url = `${serviceUrl}/${id}`;
25630
+ + }
25631
+ +
25632
+ + const res = await ApiService.get(url);
25633
+ + set({ package: res });
25634
+ +
25635
+ + return res;
25636
+ + } catch (err: any) {
25637
+ + const status = err?.response?.status || null;
25638
+ + set({ error: err.message || 'An error occurred', errorStatus: status });
25639
+ + return null;
25640
+ + } finally {
25641
+ + set({ loading: false });
25642
+ }
25643
+ - if (params?.source) {
25644
+ - urlObj.searchParams.append('source', params.source);
25645
+ + },
25646
+ +
25647
+ + /* ---------- WRITES ---------- */
25648
+ + createPackage: async (data) => {
25649
+ + try {
25650
+ + set({ loading: true, error: null, errorStatus: null });
25651
+ + const res: IPackage = await ApiService.post(`${serviceUrl}/create`, data);
25652
+ + if (res) {
25653
+ + const currentPkgs = get().packages || [];
25654
+ + const updatedPkgs = [res, ...currentPkgs.filter((p) => p.id !== res.id)];
25655
+ + set({ package: res, packages: updatedPkgs });
25656
+ +
25657
+ + injectIntoCache({
25658
+ + get,
25659
+ + set,
25660
+ + listPrefix: 'getPackages',
25661
+ + detailPrefix: 'getPackageById:',
25662
+ + item: res,
25663
+ + });
25664
+ + }
25665
+ + return res;
25666
+ + } catch (err: any) {
25667
+ + const status = err?.response?.status || null;
25668
+ + set({ error: err.message || 'An error occurred', errorStatus: status });
25669
+ + return null;
25670
+ + } finally {
25671
+ + set({ loading: false });
25672
+ }
25673
+ - if (params?.search_query) {
25674
+ - urlObj.searchParams.append('search_query', params.search_query);
25675
+ + },
25676
+ +
25677
+ + /** PATCH the *meta* (top-card) of a package */
25678
+ + updatePackage: async (id, data) => {
25679
+ + try {
25680
+ + set({ loading: true, error: null, errorStatus: null });
25681
+ + const endpoint = `${serviceUrl}/update/${id}`;
25682
+ +
25683
+ + const res: IPackage = await ApiService.patch(endpoint, data);
25684
+ + if (res) {
25685
+ + const currentPkgs = get().packages || [];
25686
+ + const updatedPkgs = currentPkgs.map((p) => p.id === id ? { ...p, ...res } : p);
25687
+ + set({ package: res, packages: updatedPkgs });
25688
+ +
25689
+ + updateInCache({
25690
+ + get,
25691
+ + set,
25692
+ + listPrefix: 'getPackages',
25693
+ + detailPrefix: 'getPackageById:',
25694
+ + item: { id, ...data, ...res },
25695
+ + });
25696
+ + }
25697
+ + return res;
25698
+ + } catch (err: any) {
25699
+ + const status = err?.response?.status || null;
25700
+ + const errorMsg = err?.response?.data?.detail || err?.response?.data?.message || err?.message || 'Update failed';
25701
+ + set({ error: errorMsg, errorStatus: status });
25702
+ + throw err; // Re-throw to allow component to handle
25703
+ + } finally {
25704
+ + set({ loading: false });
25705
+ }
25706
+ -
25707
+ - const url = urlObj.toString();
25708
+ -
25709
+ - // ✅ Debug logging for search verification
25710
+ - console.log('[PackageStore] Fetching packages with URL:', url);
25711
+ -
25712
+ - const res = await ApiService.get(url);
25713
+ - if (!Array.isArray(res)) {
25714
+ - set({ error: 'Unexpected API response', errorStatus: null, packages: [] });
25715
+ - return null;
25716
+ + },
25717
+ +
25718
+ + /** PATCH a single detail row (service) */
25719
+ + updatePackageDetail: async (detailId: number, payload: Record<string, any>) => {
25720
+ + try {
25721
+ + set({ loading: true, error: null, errorStatus: null });
25722
+ + const res = await ApiService.patch(
25723
+ + `${serviceUrl}/update_detail/${detailId}`,
25724
+ + payload,
25725
+ + );
25726
+ +
25727
+ + /* If backend returns parent id refresh local state */
25728
+ + const pkgId = res?.package_id;
25729
+ + if (typeof pkgId === 'number') await get().getPackageById(pkgId);
25730
+ +
25731
+ + return res;
25732
+ + } catch (err: any) {
25733
+ + const status = err?.response?.status || null;
25734
+ + set({ error: err.message || 'Detail update failed', errorStatus: status });
25735
+ + throw err;
25635
25736
  + } finally {
25636
25737
  + set({ loading: false });
25637
25738
  }
@@ -25646,60 +25747,134 @@ index bee9382..21f88e2 100644
25646
25747
  - set({ loading: false });
25647
25748
  - }
25648
25749
  - },
25750
+ -
25751
+ - getPackageById: async (id: number, source: string = 'COMPANY') => {
25752
+ - try {
25753
+ - set({ loading: true, error: null, errorStatus: null });
25754
+ -
25755
+ - let url: string;
25756
+ - if (source === 'GENERIC') {
25757
+ - url = `/billing/api/online/catalogue/package/generic/${id}`;
25758
+ - } else {
25759
+ - url = `${serviceUrl}/${id}`;
25649
25760
  + },
25650
-
25651
- getPackageById: async (id: number, source: string = 'COMPANY') => {
25652
- try {
25653
- @@ -128,7 +146,19 @@ export const usePackageStore = create<IPackageState>((set, get) => ({
25654
- try {
25655
- set({ loading: true, error: null, errorStatus: null });
25656
- const res: IPackage = await ApiService.post(`${serviceUrl}/create`, data);
25657
- - set({ package: res });
25658
- + if (res) {
25659
- + const currentPkgs = get().packages || [];
25660
- + const updatedPkgs = [res, ...currentPkgs.filter((p) => p.id !== res.id)];
25661
- + set({ package: res, packages: updatedPkgs });
25662
25761
  +
25663
- + injectIntoCache({
25664
- + get,
25665
- + set,
25666
- + listPrefix: 'getPackages',
25667
- + detailPrefix: 'getPackageById:',
25668
- + item: res,
25669
- + });
25670
- + }
25671
- return res;
25672
- } catch (err: any) {
25673
- const status = err?.response?.status || null;
25674
- @@ -146,8 +176,19 @@ export const usePackageStore = create<IPackageState>((set, get) => ({
25675
- const endpoint = `${serviceUrl}/update/${id}`;
25676
-
25677
- const res: IPackage = await ApiService.patch(endpoint, data);
25762
+ + /** POST new groups to an existing package */
25763
+ + addPackageGroup: async (packageId, groups) => {
25764
+ + try {
25765
+ + set({ loading: true, error: null, errorStatus: null });
25766
+ + const res = await ApiService.post(
25767
+ + `${serviceUrl}/add_group/${packageId}`,
25768
+ + groups,
25769
+ + );
25770
+ + // Refresh package data after adding groups
25771
+ + await get().getPackageById(packageId);
25772
+ + return res;
25773
+ + } catch (err: any) {
25774
+ + const status = err?.response?.status || null;
25775
+ + set({ error: err.message || 'Adding group failed', errorStatus: status });
25776
+ + throw err;
25777
+ + } finally {
25778
+ + set({ loading: false });
25779
+ }
25780
+ -
25781
+ - const res = await ApiService.get(url);
25782
+ - set({ package: res });
25783
+ -
25784
+ -
25785
+ - return res;
25786
+ - } catch (err: any) {
25787
+ - const status = err?.response?.status || null;
25788
+ - set({ error: err.message || 'An error occurred', errorStatus: status });
25789
+ - return null;
25790
+ - } finally {
25791
+ - set({ loading: false });
25792
+ - }
25793
+ - },
25794
+ -
25795
+ - /* ---------- WRITES ---------- */
25796
+ - createPackage: async (data) => {
25797
+ - try {
25798
+ - set({ loading: true, error: null, errorStatus: null });
25799
+ - const res: IPackage = await ApiService.post(`${serviceUrl}/create`, data);
25800
+ - set({ package: res });
25801
+ - return res;
25802
+ - } catch (err: any) {
25803
+ - const status = err?.response?.status || null;
25804
+ - set({ error: err.message || 'An error occurred', errorStatus: status });
25805
+ - return null;
25806
+ - } finally {
25807
+ - set({ loading: false });
25808
+ - }
25809
+ - },
25810
+ -
25811
+ - /** PATCH the *meta* (top-card) of a package */
25812
+ - updatePackage: async (id, data) => {
25813
+ - try {
25814
+ - set({ loading: true, error: null, errorStatus: null });
25815
+ - const endpoint = `${serviceUrl}/update/${id}`;
25816
+ -
25817
+ - const res: IPackage = await ApiService.patch(endpoint, data);
25678
25818
  -
25679
25819
  - set({ package: res }); // keep local copy fresh
25680
- + if (res) {
25681
- + const currentPkgs = get().packages || [];
25682
- + const updatedPkgs = currentPkgs.map((p) => p.id === id ? { ...p, ...res } : p);
25683
- + set({ package: res, packages: updatedPkgs });
25684
- +
25685
- + updateInCache({
25686
- + get,
25687
- + set,
25688
- + listPrefix: 'getPackages',
25689
- + detailPrefix: 'getPackageById:',
25690
- + item: { id, ...data, ...res },
25691
- + });
25692
- + }
25693
- return res;
25694
- } catch (err: any) {
25695
-
25696
- @@ -202,6 +243,5 @@ export const usePackageStore = create<IPackageState>((set, get) => ({
25697
- set({ loading: false });
25698
- }
25699
- },
25820
+ - return res;
25821
+ - } catch (err: any) {
25822
+ -
25823
+ - const status = err?.response?.status || null;
25824
+ - const errorMsg = err?.response?.data?.detail || err?.response?.data?.message || err?.message || 'Update failed';
25825
+ - set({ error: errorMsg, errorStatus: status });
25826
+ - throw err; // Re-throw to allow component to handle
25827
+ - } finally {
25828
+ - set({ loading: false });
25829
+ - }
25830
+ - },
25831
+ -
25832
+ - /** PATCH a single detail row (service) */
25833
+ - updatePackageDetail: async (detailId: number, payload: Record<string, any>) => {
25834
+ - try {
25835
+ - set({ loading: true, error: null, errorStatus: null });
25836
+ - const res = await ApiService.patch(
25837
+ - `${serviceUrl}/update_detail/${detailId}`,
25838
+ - payload,
25839
+ - );
25840
+ -
25841
+ - /* If backend returns parent id refresh local state */
25842
+ - const pkgId = res?.package_id;
25843
+ - if (typeof pkgId === 'number') await get().getPackageById(pkgId);
25844
+ -
25845
+ - return res;
25846
+ - } catch (err: any) {
25847
+ - const status = err?.response?.status || null;
25848
+ - set({ error: err.message || 'Detail update failed', errorStatus: status });
25849
+ - throw err;
25850
+ - } finally {
25851
+ - set({ loading: false });
25852
+ - }
25853
+ - },
25854
+ -
25855
+ - /** POST new groups to an existing package */
25856
+ - addPackageGroup: async (packageId, groups) => {
25857
+ - try {
25858
+ - set({ loading: true, error: null, errorStatus: null });
25859
+ - const res = await ApiService.post(
25860
+ - `${serviceUrl}/add_group/${packageId}`,
25861
+ - groups,
25862
+ - );
25863
+ - // Refresh package data after adding groups
25864
+ - await get().getPackageById(packageId);
25865
+ - return res;
25866
+ - } catch (err: any) {
25867
+ - const status = err?.response?.status || null;
25868
+ - set({ error: err.message || 'Adding group failed', errorStatus: status });
25869
+ - throw err;
25870
+ - } finally {
25871
+ - set({ loading: false });
25872
+ - }
25873
+ - },
25700
25874
  -
25701
25875
  -
25702
25876
  -}));
25877
+ + },
25703
25878
  + };
25704
25879
  +});
25705
25880
  diff --git a/node_modules/@suflon/native-ui/src/store/patient.store.ts b/node_modules/@suflon/native-ui/src/store/patient.store.ts
@@ -88,7 +88,8 @@ const Reporting = ({ onBackPress }: { onBackPress?: () => void } = {}) => {
88
88
  showActionIcon={false}
89
89
  />
90
90
 
91
- <ScrollView className="flex-1" contentContainerStyle={{ paddingBottom: 30 }}>
91
+ {/* Fixed Top Controls */}
92
+ <View>
92
93
  {/* Search Bar */}
93
94
  <View className="px-4 pt-3.5">
94
95
  <SearchFilter
@@ -134,7 +135,7 @@ const Reporting = ({ onBackPress }: { onBackPress?: () => void } = {}) => {
134
135
  )}
135
136
 
136
137
  {/* Category Pills Switcher - Dynamic from response */}
137
- <View className="pt-4">
138
+ <View className="py-4">
138
139
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 16, gap: 8 }}>
139
140
  {categoryTabs.map((tab) => {
140
141
  const isActive = activeTabId === tab.id;
@@ -189,58 +190,58 @@ const Reporting = ({ onBackPress }: { onBackPress?: () => void } = {}) => {
189
190
  })}
190
191
  </ScrollView>
191
192
  </View>
193
+ </View>
192
194
 
193
- {/* Report Catalog List */}
194
- <View className="px-4 pt-3.5 gap-2.5">
195
- {loading ? (
196
- <View className="py-10">
197
- <Loader message="Loading reports catalog..." color={iconBrandColor} />
198
- </View>
199
- ) : filteredReports.length === 0 ? (
200
- <View className="bg-white dark:bg-background-darkSecondaryBg p-8 rounded-2xl items-center mt-2.5 border border-slate-200 dark:border-gray-800">
201
- <Text className="text-slate-500 dark:text-gray-400 text-xs font-semibold">No reports match the selected category</Text>
202
- </View>
203
- ) : (
204
- filteredReports.map((item) => (
205
- <TouchableOpacity
206
- key={item.id}
207
- onPress={() => handleReportPress(item)}
208
- activeOpacity={0.8}
209
- className="bg-white dark:bg-background-darkSecondaryBg rounded-2xl p-3.5 border border-slate-200 dark:border-gray-800 flex-row items-center"
210
- >
211
- <View className="flex-row items-center gap-3 flex-1">
212
- <View className="w-10 h-10 rounded-xl bg-violet-50 dark:bg-violet-950/40 items-center justify-center">
213
- <Icon
214
- name={SUB_CLASS_ICON_MAP[item.sub_report_class] || (item.is_dynamic ? "pie-chart" : "file-text")}
215
- type="Feather"
216
- size={20}
217
- color={iconBrandColor}
218
- />
219
- </View>
195
+ {/* Scrollable Report Catalog List ONLY */}
196
+ <ScrollView className="flex-1" contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 30, gap: 10 }}>
197
+ {loading ? (
198
+ <View className="py-10">
199
+ <Loader message="Loading reports catalog..." color={iconBrandColor} />
200
+ </View>
201
+ ) : filteredReports.length === 0 ? (
202
+ <View className="bg-white dark:bg-background-darkSecondaryBg p-8 rounded-2xl items-center mt-2.5 border border-slate-200 dark:border-gray-800">
203
+ <Text className="text-slate-500 dark:text-gray-400 text-xs font-semibold">No reports match the selected category</Text>
204
+ </View>
205
+ ) : (
206
+ filteredReports.map((item) => (
207
+ <TouchableOpacity
208
+ key={item.id}
209
+ onPress={() => handleReportPress(item)}
210
+ activeOpacity={0.8}
211
+ className="bg-white dark:bg-background-darkSecondaryBg rounded-2xl p-3.5 border border-slate-200 dark:border-gray-800 flex-row items-center"
212
+ >
213
+ <View className="flex-row items-center gap-3 flex-1">
214
+ <View className="w-10 h-10 rounded-xl bg-violet-50 dark:bg-violet-950/40 items-center justify-center">
215
+ <Icon
216
+ name={SUB_CLASS_ICON_MAP[item.sub_report_class] || (item.is_dynamic ? "pie-chart" : "file-text")}
217
+ type="Feather"
218
+ size={20}
219
+ color={iconBrandColor}
220
+ />
221
+ </View>
220
222
 
221
- <View className="flex-1">
222
- <Text className="text-[13px] font-extrabold text-slate-900 dark:text-text-dark-primary" numberOfLines={1}>
223
- {item.name}
223
+ <View className="flex-1">
224
+ <Text className="text-[13px] font-extrabold text-slate-900 dark:text-text-dark-primary" numberOfLines={1}>
225
+ {item.name}
226
+ </Text>
227
+ <View className="flex-row items-center gap-1.5 mt-0.5">
228
+ <Text className="text-[10px] font-bold text-violet-600 dark:text-violet-400">
229
+ {SUB_CLASS_LABEL_MAP[item.sub_report_class] || item.sub_report_class || 'Report'}
230
+ </Text>
231
+ <Text className="text-[10px] text-slate-400 dark:text-gray-500">•</Text>
232
+ <Text className="text-[10px] font-semibold text-slate-500 dark:text-gray-400">
233
+ {item.report_type || 'LISTING'}
224
234
  </Text>
225
- <View className="flex-row items-center gap-1.5 mt-0.5">
226
- <Text className="text-[10px] font-bold text-violet-600 dark:text-violet-400">
227
- {SUB_CLASS_LABEL_MAP[item.sub_report_class] || item.sub_report_class || 'Report'}
228
- </Text>
229
- <Text className="text-[10px] text-slate-400 dark:text-gray-500">•</Text>
230
- <Text className="text-[10px] font-semibold text-slate-500 dark:text-gray-400">
231
- {item.report_type || 'LISTING'}
232
- </Text>
233
- </View>
234
235
  </View>
235
236
  </View>
237
+ </View>
236
238
 
237
- <View className="flex-row items-center gap-1.5">
238
- <Icon name="chevron-right" type="Feather" size={18} color={iconMutedColor} />
239
- </View>
240
- </TouchableOpacity>
241
- ))
242
- )}
243
- </View>
239
+ <View className="flex-row items-center gap-1.5">
240
+ <Icon name="chevron-right" type="Feather" size={18} color={iconMutedColor} />
241
+ </View>
242
+ </TouchableOpacity>
243
+ ))
244
+ )}
244
245
  </ScrollView>
245
246
  </SafeAreaView>
246
247
  );
@@ -85,7 +85,7 @@ const DEV_STRINGS: Record<LanguageCode, Record<string, string>> = {
85
85
 
86
86
  // Default dev values
87
87
  const DEFAULT_VALUES = {
88
- authToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxOTQxNjg3Nzk1LCJ0aWQiOiJ5aDRsa2pkbiIsImNsdCI6IldFQiIsInNjb3BlIjoiQ09NUEFOWSIsImNvbXBhbnlfaWQiOjIsInJlZ2lvbl9pZCI6MSwic3RhZmZfaWQiOjF9.-Z6cH67dOnYJOoG1Gx49nAOeWrA1wKMO5jMFAbaBjeQ',
88
+ authToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxOTQxNzEwNzI5LCJ0aWQiOiJka2t3Ynd1ZyIsImNsdCI6IldFQiIsInNjb3BlIjoiQ09NUEFOWSIsImNvbXBhbnlfaWQiOjIsInJlZ2lvbl9pZCI6MSwic3RhZmZfaWQiOjF9.hPQC0_-uFfHU253gvJi-eJD1HnM8fGWA0lG3IRVs09I',
89
89
  companyId: '2',
90
90
  regionId: '1',
91
91
  staffId: '1',