@suflon/rnmd-reporting 0.0.3 → 0.0.5

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/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export { default as Reporting } from './modules/Reporting';
3
3
  export { default as ReportingDetail } from './modules/Reporting/component/ReportingDetail';
4
4
  export { default as ReportChart } from './modules/Reporting/component/ReportChart';
5
5
  export { default as ReportFilterModal } from './modules/Reporting/component/ReportFilterModal';
6
+ export { default as ManagementReport } from './modules/ManagementReport';
@@ -0,0 +1,530 @@
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ ScrollView,
6
+ TouchableOpacity,
7
+ ActivityIndicator,
8
+ useColorScheme,
9
+ Dimensions,
10
+ } from 'react-native';
11
+ import Svg, { Path, Circle, Defs, LinearGradient, Stop, Line as SvgLine } from 'react-native-svg';
12
+ import {
13
+ IManagementReportDetail,
14
+ Icon,
15
+ useManagementReportStore,
16
+ } from '@suflon/native-ui';
17
+ import { TimeframeDropdown, TimeframeOption, getTimeframeDateFilter } from './TimeframeDropdown';
18
+
19
+ interface AnalyticsChartViewProps {
20
+ details: IManagementReportDetail[];
21
+ executionData: Record<number, any[]>;
22
+ loading?: boolean;
23
+ }
24
+
25
+ interface ChartItemData {
26
+ label: string;
27
+ value: number;
28
+ }
29
+
30
+ function formatLabel(label: string): string {
31
+ if (!label) return '-';
32
+ if (/^\d{4}-\d{2}-\d{2}$/.test(label)) {
33
+ const parts = label.split('-');
34
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
35
+ const m = parseInt(parts[1], 10) - 1;
36
+ return `${parts[2]} ${months[m] || parts[1]}`;
37
+ }
38
+
39
+ return label
40
+ .replace(/_/g, ' ')
41
+ .replace(/([A-Z])/g, ' $1')
42
+ .replace(/\b\w/g, (c) => c.toUpperCase())
43
+ .trim();
44
+ }
45
+
46
+ function formatYAxisValue(val: number): string {
47
+ if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
48
+ if (val >= 1000) return `${(val / 1000).toFixed(1)}k`;
49
+ if (Number.isInteger(val)) return String(val);
50
+ return val.toFixed(1);
51
+ }
52
+
53
+ function parseChartData(rows: any[]): ChartItemData[] {
54
+ if (!rows || rows.length === 0) return [];
55
+
56
+ return rows.map((r, i) => {
57
+ if (typeof r === 'number') {
58
+ return { label: `Item ${i + 1}`, value: r };
59
+ }
60
+
61
+ if (typeof r === 'object' && r !== null) {
62
+ const keys = Object.keys(r);
63
+ const labelKey =
64
+ keys.find((k) =>
65
+ [
66
+ 'date',
67
+ 'month',
68
+ 'day',
69
+ 'label',
70
+ 'name',
71
+ 'status',
72
+ 'type',
73
+ 'department',
74
+ 'category',
75
+ 'group_key',
76
+ 'appointment_type',
77
+ 'severity',
78
+ ].includes(k.toLowerCase())
79
+ ) || keys[0];
80
+
81
+ const valueKey =
82
+ keys.find((k) =>
83
+ [
84
+ 'count',
85
+ 'total',
86
+ 'value',
87
+ 'amount',
88
+ 'total_amount',
89
+ 'net_amount',
90
+ 'sum',
91
+ 'metric_value',
92
+ 'patients',
93
+ ].includes(k.toLowerCase())
94
+ ) || keys[1] || keys[0];
95
+
96
+ const val = Number(r[valueKey]) || 0;
97
+ const rawLbl = String(r[labelKey] || `Item ${i + 1}`);
98
+
99
+ return { label: formatLabel(rawLbl), value: val };
100
+ }
101
+
102
+ return { label: `Item ${i + 1}`, value: 0 };
103
+ });
104
+ }
105
+
106
+ const SingleChartCard: React.FC<{
107
+ detail: IManagementReportDetail;
108
+ rows: any[];
109
+ loading: boolean;
110
+ }> = ({ detail, rows, loading }) => {
111
+ const isDark = useColorScheme() === 'dark';
112
+ const [timeframe, setTimeframe] = useState<TimeframeOption>('Monthly');
113
+ const [chartType, setChartType] = useState<'bar' | 'line' | 'trend'>('bar');
114
+ const [fetching, setFetching] = useState(false);
115
+
116
+ const executeReportItem = useManagementReportStore((s) => s.executeReportItem);
117
+
118
+ const handleSelectTimeframe = useCallback(
119
+ async (selectedTf: TimeframeOption) => {
120
+ setTimeframe(selectedTf);
121
+ if (detail.report_id) {
122
+ setFetching(true);
123
+ const dateFilter = getTimeframeDateFilter(selectedTf);
124
+ const state = useManagementReportStore.getState();
125
+ const origin = (state.selectedReport?.mgmt_report_type || state.activeCategory || 'opd').toLowerCase();
126
+ await executeReportItem(
127
+ detail.report_id,
128
+ detail.report?.is_dynamic || false,
129
+ { filters: dateFilter },
130
+ origin,
131
+ true
132
+ );
133
+ setFetching(false);
134
+ }
135
+ },
136
+ [detail, executeReportItem]
137
+ );
138
+
139
+ const chartData = useMemo(() => parseChartData(rows), [rows]);
140
+ const maxValue = useMemo(
141
+ () => Math.max(...chartData.map((d) => d.value), 1),
142
+ [chartData]
143
+ );
144
+
145
+ // Generate 4 evenly spaced Y-axis ticks
146
+ const yTicks = useMemo(() => {
147
+ return [
148
+ maxValue,
149
+ Math.round(maxValue * 0.66),
150
+ Math.round(maxValue * 0.33),
151
+ 0,
152
+ ];
153
+ }, [maxValue]);
154
+
155
+ const isBusy = loading || fetching;
156
+ const title = detail.report?.name || detail.report_num || 'Performance Metric';
157
+ const isCompact = chartData.length <= 4;
158
+ const chartHeight = 130;
159
+ const cardWidth = Dimensions.get('window').width - 90;
160
+ const svgWidth = Math.max(cardWidth, chartData.length * 64);
161
+
162
+ // SVG Line / Trend calculations
163
+ const svgPoints = useMemo(() => {
164
+ if (chartData.length === 0) return [];
165
+ const step = chartData.length > 1 ? svgWidth / (chartData.length - 1) : svgWidth / 2;
166
+ const paddingBottom = 15;
167
+ const paddingTop = 15;
168
+ const availableHeight = chartHeight - paddingTop - paddingBottom;
169
+
170
+ return chartData.map((d, i) => {
171
+ const x = chartData.length === 1 ? svgWidth / 2 : i * step;
172
+ const y = chartHeight - paddingBottom - (d.value / maxValue) * availableHeight;
173
+ return { x, y, value: d.value, label: d.label };
174
+ });
175
+ }, [chartData, maxValue, svgWidth, chartHeight]);
176
+
177
+ const linePath = useMemo(() => {
178
+ if (svgPoints.length === 0) return '';
179
+ return svgPoints.reduce((acc, p, i) => `${acc} ${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`, '');
180
+ }, [svgPoints]);
181
+
182
+ const areaPath = useMemo(() => {
183
+ if (svgPoints.length === 0) return '';
184
+ const lastX = svgPoints[svgPoints.length - 1].x;
185
+ const firstX = svgPoints[0].x;
186
+ return `${linePath} L ${lastX},${chartHeight - 10} L ${firstX},${chartHeight - 10} Z`;
187
+ }, [linePath, svgPoints, chartHeight]);
188
+
189
+ return (
190
+ <View className="p-3.5 rounded-3xl bg-white dark:bg-background-darkSecondaryBg border border-slate-100 dark:border-slate-800 mb-3.5">
191
+ {/* Header: Title & Timeframe Selector */}
192
+ <View className="flex-row items-center justify-between mb-3 pb-2 border-b border-slate-100 dark:border-slate-800">
193
+ <Text
194
+ style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
195
+ className="text-sm font-extrabold tracking-tight flex-1 mr-2"
196
+ numberOfLines={1}
197
+ >
198
+ {title}
199
+ </Text>
200
+
201
+ <View className="flex-row items-center gap-1.5 shrink-0">
202
+ {/* Chart Type Selector */}
203
+ <View className="p-0.5 rounded-xl bg-slate-100 dark:bg-slate-800 flex-row items-center">
204
+ <TouchableOpacity
205
+ onPress={() => setChartType('bar')}
206
+ activeOpacity={0.8}
207
+ className={`p-1 rounded-lg ${chartType === 'bar' ? 'bg-white dark:bg-slate-700 shadow-sm' : 'bg-transparent'}`}
208
+ >
209
+ <Icon
210
+ name="bar-chart-2"
211
+ type="Feather"
212
+ size={11}
213
+ color={chartType === 'bar' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
214
+ />
215
+ </TouchableOpacity>
216
+ <TouchableOpacity
217
+ onPress={() => setChartType('line')}
218
+ activeOpacity={0.8}
219
+ className={`p-1 rounded-lg ${chartType === 'line' ? 'bg-white dark:bg-slate-700 shadow-sm' : 'bg-transparent'}`}
220
+ >
221
+ <Icon
222
+ name="activity"
223
+ type="Feather"
224
+ size={11}
225
+ color={chartType === 'line' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
226
+ />
227
+ </TouchableOpacity>
228
+ <TouchableOpacity
229
+ onPress={() => setChartType('trend')}
230
+ activeOpacity={0.8}
231
+ className={`p-1 rounded-lg ${chartType === 'trend' ? 'bg-white dark:bg-slate-700 shadow-sm' : 'bg-transparent'}`}
232
+ >
233
+ <Icon
234
+ name="trending-up"
235
+ type="Feather"
236
+ size={11}
237
+ color={chartType === 'trend' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
238
+ />
239
+ </TouchableOpacity>
240
+ </View>
241
+
242
+ {/* Timeframe Dropdown */}
243
+ <TimeframeDropdown
244
+ value={timeframe}
245
+ onChange={handleSelectTimeframe}
246
+ />
247
+ </View>
248
+ </View>
249
+
250
+ {/* Chart Visual Surface */}
251
+ {isBusy ? (
252
+ <View className="h-44 items-center justify-center">
253
+ <ActivityIndicator size="small" color="#7c3aed" />
254
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-semibold mt-2">
255
+ Fetching chart metrics...
256
+ </Text>
257
+ </View>
258
+ ) : chartData.length === 0 ? (
259
+ <View className="h-36 items-center justify-center rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-background-lightBlack">
260
+ <Icon name="bar-chart-2" type="Feather" size={20} color="#94a3b8" />
261
+ <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-xs font-bold mt-1.5">No metrics recorded</Text>
262
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] text-center mt-0.5">
263
+ Try adjusting your timeframe filter.
264
+ </Text>
265
+ </View>
266
+ ) : (
267
+ <View className="pt-1">
268
+ {/* Main Chart Body: Left Y-Axis Scale Column + Chart Canvas */}
269
+ <View className="flex-row">
270
+ {/* 1. Left Y-Axis Labels Column */}
271
+ <View className="w-9 justify-between items-end pr-1.5 pb-6" style={{ height: chartHeight + 24 }}>
272
+ {yTicks.map((tick, i) => (
273
+ <Text
274
+ key={i}
275
+ style={{ color: isDark ? '#64748B' : '#94A3B8' }}
276
+ className="text-[9px] font-mono font-medium text-right"
277
+ numberOfLines={1}
278
+ >
279
+ {formatYAxisValue(tick)}
280
+ </Text>
281
+ ))}
282
+ </View>
283
+
284
+ {/* 2. Chart Grid & Plot Area */}
285
+ <View className="flex-1 relative">
286
+ {/* Background Horizontal Guide Lines */}
287
+ <View className="absolute inset-0 justify-between pb-6 pointer-events-none" style={{ height: chartHeight + 24 }}>
288
+ {yTicks.map((_, i) => (
289
+ <View
290
+ key={i}
291
+ style={{
292
+ height: 1,
293
+ backgroundColor: i === yTicks.length - 1
294
+ ? (isDark ? '#475569' : '#cbd5e1')
295
+ : (isDark ? '#1e293b' : '#f1f5f9'),
296
+ }}
297
+ className="w-full"
298
+ />
299
+ ))}
300
+ </View>
301
+
302
+ {/* Chart Mode Rendering */}
303
+ {chartType === 'bar' ? (
304
+ isCompact ? (
305
+ /* Balanced Full-Width Grid for 2-4 items */
306
+ <View>
307
+ <View className="flex-row justify-around items-end px-1" style={{ height: chartHeight }}>
308
+ {chartData.map((item, idx) => {
309
+ const barHeight = Math.max(14, Math.round((item.value / maxValue) * (chartHeight - 30)));
310
+ const isTopValue = item.value === maxValue;
311
+
312
+ return (
313
+ <View
314
+ key={idx}
315
+ className="items-center justify-end flex-1 max-w-[68px] px-1"
316
+ >
317
+ <Text
318
+ style={{
319
+ color: isTopValue ? (isDark ? '#A78BFA' : '#7C3AED') : (isDark ? '#E2E8F0' : '#475569'),
320
+ }}
321
+ className="text-[9px] font-extrabold mb-1 font-mono"
322
+ numberOfLines={1}
323
+ >
324
+ {formatYAxisValue(item.value)}
325
+ </Text>
326
+
327
+ <View
328
+ style={{ height: barHeight }}
329
+ className={`w-full rounded-t-xl ${
330
+ isTopValue
331
+ ? 'bg-violet-600 dark:bg-violet-500'
332
+ : 'bg-violet-400/80 dark:bg-violet-600/60'
333
+ }`}
334
+ />
335
+ </View>
336
+ );
337
+ })}
338
+ </View>
339
+
340
+ {/* Continuous X-Axis Baseline */}
341
+ <View className="w-full h-[1.5px] bg-slate-300 dark:bg-slate-700" />
342
+
343
+ {/* Category Labels below Baseline */}
344
+ <View className="flex-row justify-around px-1 pt-1.5">
345
+ {chartData.map((item, idx) => (
346
+ <View key={idx} className="flex-1 max-w-[68px] items-center px-0.5">
347
+ <Text
348
+ style={{ color: isDark ? '#E2E8F0' : '#475569' }}
349
+ className="text-[9px] font-semibold text-center leading-tight"
350
+ numberOfLines={2}
351
+ >
352
+ {item.label}
353
+ </Text>
354
+ </View>
355
+ ))}
356
+ </View>
357
+ </View>
358
+ ) : (
359
+ /* Horizontal Scroll for > 4 items */
360
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
361
+ <View>
362
+ <View className="flex-row items-end px-1" style={{ height: chartHeight }}>
363
+ {chartData.map((item, idx) => {
364
+ const barHeight = Math.max(14, Math.round((item.value / maxValue) * (chartHeight - 30)));
365
+ const isTopValue = item.value === maxValue;
366
+
367
+ return (
368
+ <View
369
+ key={idx}
370
+ className="items-center justify-end mr-3"
371
+ style={{ width: 52 }}
372
+ >
373
+ <Text
374
+ style={{
375
+ color: isTopValue ? (isDark ? '#A78BFA' : '#7C3AED') : (isDark ? '#E2E8F0' : '#475569'),
376
+ }}
377
+ className="text-[9px] font-extrabold mb-1 font-mono"
378
+ numberOfLines={1}
379
+ >
380
+ {formatYAxisValue(item.value)}
381
+ </Text>
382
+
383
+ <View
384
+ style={{ height: barHeight }}
385
+ className={`w-full rounded-t-xl ${
386
+ isTopValue
387
+ ? 'bg-violet-600 dark:bg-violet-500'
388
+ : 'bg-violet-400/80 dark:bg-violet-600/60'
389
+ }`}
390
+ />
391
+ </View>
392
+ );
393
+ })}
394
+ </View>
395
+
396
+ {/* Continuous X-Axis Baseline */}
397
+ <View className="w-full h-[1.5px] bg-slate-300 dark:bg-slate-700" />
398
+
399
+ {/* Category Labels below Baseline */}
400
+ <View className="flex-row px-1 pt-1.5">
401
+ {chartData.map((item, idx) => (
402
+ <View key={idx} style={{ width: 52 }} className="mr-3 items-center">
403
+ <Text
404
+ style={{ color: isDark ? '#E2E8F0' : '#475569' }}
405
+ className="text-[9px] font-semibold text-center leading-tight"
406
+ numberOfLines={2}
407
+ >
408
+ {item.label}
409
+ </Text>
410
+ </View>
411
+ ))}
412
+ </View>
413
+ </View>
414
+ </ScrollView>
415
+ )
416
+ ) : (
417
+ /* LINE / TREND SVG GRAPH MODE */
418
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
419
+ <View style={{ width: svgWidth }}>
420
+ <Svg width={svgWidth} height={chartHeight}>
421
+ <Defs>
422
+ <LinearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
423
+ <Stop offset="0%" stopColor="#7c3aed" stopOpacity="0.35" />
424
+ <Stop offset="100%" stopColor="#7c3aed" stopOpacity="0.0" />
425
+ </LinearGradient>
426
+ </Defs>
427
+
428
+ {/* Continuous X-Axis Baseline */}
429
+ <SvgLine
430
+ x1="0"
431
+ y1={chartHeight - 10}
432
+ x2={svgWidth}
433
+ y2={chartHeight - 10}
434
+ stroke={isDark ? '#475569' : '#cbd5e1'}
435
+ strokeWidth="1.5"
436
+ />
437
+
438
+ {/* Trend Area Gradient Underfill */}
439
+ {chartType === 'trend' && (
440
+ <Path d={areaPath} fill="url(#gradientArea)" />
441
+ )}
442
+
443
+ {/* Line Path */}
444
+ <Path
445
+ d={linePath}
446
+ fill="none"
447
+ stroke="#7c3aed"
448
+ strokeWidth="2.5"
449
+ strokeLinecap="round"
450
+ strokeLinejoin="round"
451
+ />
452
+
453
+ {/* Data Points */}
454
+ {svgPoints.map((p, i) => (
455
+ <Circle
456
+ key={i}
457
+ cx={p.x}
458
+ cy={p.y}
459
+ r="4"
460
+ fill={isDark ? '#1C1C1E' : '#FFFFFF'}
461
+ stroke="#7c3aed"
462
+ strokeWidth="2"
463
+ />
464
+ ))}
465
+ </Svg>
466
+
467
+ {/* Data Labels under Points */}
468
+ <View className="flex-row justify-between w-full px-1 pt-1.5">
469
+ {svgPoints.map((p, i) => (
470
+ <View key={i} style={{ width: 56, alignItems: 'center' }}>
471
+ <Text style={{ color: isDark ? '#E2E8F0' : '#475569' }} className="text-[8px] font-semibold text-center" numberOfLines={1}>
472
+ {p.label}
473
+ </Text>
474
+ <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-[9px] font-mono font-bold mt-0.5">
475
+ {formatYAxisValue(p.value)}
476
+ </Text>
477
+ </View>
478
+ ))}
479
+ </View>
480
+ </View>
481
+ </ScrollView>
482
+ )}
483
+ </View>
484
+ </View>
485
+
486
+ {/* Subtitle / Legend Footer */}
487
+ <View className="flex-row items-center justify-between pt-2.5 mt-2 border-t border-slate-100 dark:border-slate-800">
488
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-medium">
489
+ Peak: <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="font-bold">{maxValue.toLocaleString()}</Text>
490
+ </Text>
491
+ <View className="flex-row items-center gap-1">
492
+ <View className="w-2 h-2 rounded-full bg-violet-600" />
493
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-semibold">
494
+ {chartType === 'bar' ? `${timeframe} Comparison` : chartType === 'line' ? 'Line Trajectory' : 'Trend Area'}
495
+ </Text>
496
+ </View>
497
+ </View>
498
+ </View>
499
+ )}
500
+ </View>
501
+ );
502
+ };
503
+
504
+ export const AnalyticsChartView: React.FC<AnalyticsChartViewProps> = ({
505
+ details,
506
+ executionData,
507
+ loading = false,
508
+ }) => {
509
+ const chartDetails = details.filter(
510
+ (d) =>
511
+ d.display_size === 'MD' ||
512
+ d.report?.report_type === 'CHART' ||
513
+ d.report?.report_type === 'SUMMARY'
514
+ );
515
+
516
+ if (chartDetails.length === 0) return null;
517
+
518
+ return (
519
+ <View className="space-y-3.5">
520
+ {chartDetails.map((detail) => (
521
+ <SingleChartCard
522
+ key={detail.id || detail.report_id}
523
+ detail={detail}
524
+ rows={executionData[detail.report_id] || []}
525
+ loading={loading}
526
+ />
527
+ ))}
528
+ </View>
529
+ );
530
+ };
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { ScrollView, TouchableOpacity, View, Text } from 'react-native';
3
+ import { Icon } from '@suflon/native-ui';
4
+
5
+ export interface CategoryItem {
6
+ id: string;
7
+ name: string;
8
+ icon: string;
9
+ type: string;
10
+ }
11
+
12
+ export const CATEGORIES_DATA: CategoryItem[] = [
13
+ { id: 'OPD', name: 'OPD', icon: 'calendar', type: 'Feather' },
14
+ { id: 'PRESCRIPTION', name: 'PRESCRIPTION', icon: 'file-text', type: 'Feather' },
15
+ { id: 'SALES', name: 'SALES', icon: 'pie-chart', type: 'Feather' },
16
+ { id: 'PURCHASE', name: 'PURCHASE', icon: 'package', type: 'Feather' },
17
+ { id: 'TRANSACTION', name: 'TRANSACTION', icon: 'credit-card', type: 'Feather' },
18
+ ];
19
+
20
+ interface CategoryTabsProps {
21
+ activeCategory: string;
22
+ onSelectCategory: (category: string) => void;
23
+ reportCounts?: Record<string, number>;
24
+ }
25
+
26
+ export const CategoryTabs: React.FC<CategoryTabsProps> = ({
27
+ activeCategory,
28
+ onSelectCategory,
29
+ reportCounts = {},
30
+ }) => {
31
+ return (
32
+ <View className="py-0.5">
33
+ <ScrollView
34
+ horizontal
35
+ showsHorizontalScrollIndicator={false}
36
+ contentContainerStyle={{ gap: 8 }}
37
+ >
38
+ {CATEGORIES_DATA.map((cat) => {
39
+ const isActive = activeCategory === cat.id;
40
+ const count = reportCounts[cat.id];
41
+
42
+ return (
43
+ <TouchableOpacity
44
+ key={cat.id}
45
+ onPress={() => onSelectCategory(cat.id)}
46
+ activeOpacity={0.8}
47
+ className={`px-3.5 py-1.5 rounded-xl flex-row items-center gap-1.5 border ${
48
+ isActive
49
+ ? 'bg-violet-600 border-violet-600'
50
+ : 'bg-white dark:bg-slate-800/90 border-slate-200/60 dark:border-slate-700/60'
51
+ }`}
52
+ >
53
+ <Icon
54
+ name={cat.icon}
55
+ type={cat.type as any}
56
+ size={12}
57
+ color={isActive ? '#FFFFFF' : '#94a3b8'}
58
+ />
59
+ <Text
60
+ style={{ color: isActive ? '#FFFFFF' : '#94a3b8' }}
61
+ className={`text-xs ${isActive ? 'font-bold' : 'font-normal'}`}
62
+ >
63
+ {cat.name}
64
+ </Text>
65
+ {count !== undefined && count > 0 && (
66
+ <View
67
+ className={`px-1.5 py-0.5 rounded-full ${
68
+ isActive ? 'bg-white/20' : 'bg-slate-100 dark:bg-slate-700'
69
+ }`}
70
+ >
71
+ <Text
72
+ style={{ color: isActive ? '#FFFFFF' : '#94a3b8' }}
73
+ className={`text-[9px] ${isActive ? 'font-bold' : 'font-normal'}`}
74
+ >
75
+ {count}
76
+ </Text>
77
+ </View>
78
+ )}
79
+ </TouchableOpacity>
80
+ );
81
+ })}
82
+ </ScrollView>
83
+ </View>
84
+ );
85
+ };