@jescon-technologies/react-native-ui-kit 0.0.14 → 0.0.15

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.
@@ -0,0 +1,1064 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ Platform,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ TextProps,
8
+ View,
9
+ ViewProps,
10
+ FlatList,
11
+ ListRenderItemInfo,
12
+ ActivityIndicator,
13
+ StyleProp,
14
+ ViewStyle,
15
+ TextStyle,
16
+ DimensionValue,
17
+ } from 'react-native';
18
+ import {
19
+ BarChart,
20
+ BubbleChart,
21
+ CandleStickChart,
22
+ LineChart,
23
+ LineChartBicolor,
24
+ PieChart,
25
+ PieChartPro,
26
+ PopulationPyramid,
27
+ RadarChart,
28
+ } from 'react-native-gifted-charts';
29
+ import NetworkServiceCall from '../Common/NetworkServiceCall';
30
+ import { notifyMessage } from '../CommonStyle/ComponentFunctions';
31
+ import HShadowButton from './HShadowButton';
32
+ import HIcons from './HIcons';
33
+ import { normalize } from '../CommonStyle/componentStyle';
34
+ import { Global } from '../Common/Global';
35
+ import SDateTimePicker from '../StaticComponents/SDateTimePicker';
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Types
39
+ // ---------------------------------------------------------------------------
40
+
41
+ interface Theme {
42
+ colors: Record<string, string>;
43
+ }
44
+ type ThemeColorKey = string;
45
+
46
+ interface DateRange {
47
+ from?: string;
48
+ to?: string;
49
+ }
50
+
51
+ interface ChartConfig {
52
+ component: keyof typeof GIFTED_CHARTS | string;
53
+ props?: Record<string, unknown> & {
54
+ data?: PieDatum[];
55
+ centerLabelComponent?: () => React.ReactElement | null;
56
+ };
57
+ }
58
+
59
+ interface WidgetLayout {
60
+ visible?: boolean;
61
+ order?: number;
62
+ }
63
+
64
+ interface Widget {
65
+ id?: string | number;
66
+ type: 'chart' | 'heatmap' | 'ranking' | string;
67
+ title?: string;
68
+ subtitle?: string;
69
+ iconName?: string;
70
+ unit?: string;
71
+ chart?: ChartConfig;
72
+ data?: WidgetDatum[];
73
+ options?: { lowColor?: string; highColor?: string };
74
+ layout?: WidgetLayout;
75
+ }
76
+
77
+ interface WidgetDatum {
78
+ id?: string | number;
79
+ label?: string;
80
+ name?: string;
81
+ value?: number | string;
82
+ ratio?: number;
83
+ percentage?: number;
84
+ color?: string;
85
+ }
86
+
87
+ interface PieDatum {
88
+ value?: number | string;
89
+ text?: string;
90
+ label?: string;
91
+ color?: string;
92
+ }
93
+
94
+ interface Dashboard {
95
+ dashboardId?: string | number;
96
+ dashboardTitle?: string;
97
+ department?: string;
98
+ iconName?: string;
99
+ dateRange?: DateRange;
100
+ widgets?: Widget[];
101
+ theme?: Theme;
102
+ content?: ContentConfig;
103
+ chartStyle?: ChartStyleConfig;
104
+ }
105
+
106
+ interface ContentConfig {
107
+ iconGroup?: number;
108
+ widgetIconSize?: number;
109
+ departmentIconSize?: number;
110
+ detailsLabel?: string;
111
+ unsupportedWidgetMessage?: string;
112
+ loadingLabel?: string;
113
+ emptyStateMessage?: string;
114
+ horizontalPadding?: number | string;
115
+ }
116
+
117
+ interface ChartStyleConfig {
118
+ bar?: Record<string, unknown>;
119
+ line?: Record<string, unknown>;
120
+ pie?: { centerCaption?: string; [key: string]: unknown };
121
+ axisTextStyle?: TextStyle;
122
+ [key: string]: unknown;
123
+ }
124
+
125
+ interface DashboardPayload {
126
+ departments: Dashboard[];
127
+ theme?: Theme;
128
+ content?: ContentConfig;
129
+ chartStyle?: ChartStyleConfig;
130
+ }
131
+
132
+ interface HDashboardProps {
133
+ /** Pre-supplied dashboard data; when present, the API is not called. */
134
+ dashboardData?: DashboardPayload | Dashboard[] | Dashboard | null;
135
+ /** API URL. When present (and `dashboardData` is not), the component fetches. */
136
+ buttonapi?: string;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Constants
141
+ // ---------------------------------------------------------------------------
142
+
143
+ const GIFTED_CHARTS = {
144
+ BarChart,
145
+ BubbleChart,
146
+ CandleStickChart,
147
+ LineChart,
148
+ LineChartBicolor,
149
+ PieChart,
150
+ PieChartPro,
151
+ PopulationPyramid,
152
+ RadarChart,
153
+ } as const;
154
+
155
+ /** Chart components have wildly divergent prop shapes; we don't try to union them. */
156
+ type ChartComponent = React.ComponentType<Record<string, unknown>>;
157
+
158
+ const PIE_CHARTS = new Set<string>(['PieChart', 'PieChartPro']);
159
+ const AXIS_CHARTS = new Set<string>(['LineChart', 'LineChartBicolor', 'BarChart']);
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Core helpers
163
+ // ---------------------------------------------------------------------------
164
+
165
+ const themeColor = (theme: Theme | null | undefined, key: ThemeColorKey): string | undefined =>
166
+ theme?.colors?.[key];
167
+
168
+ const toNumber = (value: unknown): number => Number(value || 0);
169
+ const formatNumber = (value: unknown): string => toNumber(value).toLocaleString();
170
+
171
+ const normalizeDashboardPayload = (
172
+ raw: DashboardPayload | Dashboard[] | Dashboard | null | undefined
173
+ ): DashboardPayload => {
174
+ if (!raw) return { departments: [] };
175
+ if (Array.isArray(raw)) return { departments: raw };
176
+ if (Array.isArray((raw as DashboardPayload).departments)) return raw as DashboardPayload;
177
+ return { ...(raw as object), departments: [raw as Dashboard] };
178
+ };
179
+
180
+ const pickInitialDashboard = (departments: Dashboard[]): Dashboard | null =>
181
+ departments && departments.length ? departments[0] : null;
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Reusable themed primitives
185
+ // ---------------------------------------------------------------------------
186
+
187
+ interface ThemedTextProps extends TextProps {
188
+ theme: Theme | null;
189
+ colorKey?: ThemeColorKey;
190
+ style?: StyleProp<TextStyle>;
191
+ children?: React.ReactNode;
192
+ }
193
+
194
+ const ThemedText: React.FC<ThemedTextProps> = ({
195
+ theme,
196
+ colorKey = 'text',
197
+ style,
198
+ children,
199
+ ...props
200
+ }) => (
201
+ <Text style={[style, { color: themeColor(theme, colorKey) }]} {...props}>
202
+ {children}
203
+ </Text>
204
+ );
205
+
206
+ interface ThemedViewProps extends ViewProps {
207
+ theme: Theme | null;
208
+ backgroundColorKey?: ThemeColorKey;
209
+ borderColorKey?: ThemeColorKey;
210
+ style?: StyleProp<ViewStyle>;
211
+ children?: React.ReactNode;
212
+ }
213
+
214
+ const ThemedView: React.FC<ThemedViewProps> = ({
215
+ theme,
216
+ backgroundColorKey,
217
+ borderColorKey,
218
+ style,
219
+ children,
220
+ ...props
221
+ }) => {
222
+ const dynamic: ViewStyle = {};
223
+ if (backgroundColorKey) dynamic.backgroundColor = themeColor(theme, backgroundColorKey);
224
+ if (borderColorKey) dynamic.borderColor = themeColor(theme, borderColorKey);
225
+ return (
226
+ <View style={[style, dynamic]} {...props}>
227
+ {children}
228
+ </View>
229
+ );
230
+ };
231
+
232
+ interface IconRendererProps {
233
+ name?: string;
234
+ icongroup?: number;
235
+ size?: number;
236
+ color?: string;
237
+ style?: StyleProp<TextStyle>;
238
+ }
239
+
240
+ const IconRenderer: React.FC<IconRendererProps> = ({ name, icongroup, size, color, style }) =>
241
+ name ? (
242
+ <HIcons.Icons
243
+ name={name}
244
+ icongroup={icongroup}
245
+ size={size}
246
+ color={color}
247
+ style={style}
248
+ />
249
+ ) : null;
250
+
251
+ interface EmptyStateProps {
252
+ message: string;
253
+ theme: Theme | null;
254
+ }
255
+
256
+ const EmptyState: React.FC<EmptyStateProps> = ({ message, theme }) => (
257
+ <ThemedText theme={theme} colorKey="muted" style={styles.emptyText}>
258
+ {message}
259
+ </ThemedText>
260
+ );
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Generic list renderer
264
+ // ---------------------------------------------------------------------------
265
+
266
+ const renderList = <T,>(
267
+ data: T[],
268
+ keyFn: (item: T, index: number) => string | number,
269
+ renderItem: (item: T, index: number, key: string | number) => React.ReactNode
270
+ ): React.ReactNode[] => data.map((item, index) => renderItem(item, index, keyFn(item, index)));
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Shared legend
274
+ // ---------------------------------------------------------------------------
275
+
276
+ interface ChartLegendProps {
277
+ data?: PieDatum[];
278
+ theme: Theme | null;
279
+ }
280
+
281
+ const ChartLegend: React.FC<ChartLegendProps> = ({ data, theme }) => {
282
+ if (!Array.isArray(data)) return null;
283
+ return (
284
+ <View
285
+ style={[
286
+ styles.chartLegend,
287
+ { borderTopColor: themeColor(theme, 'border') ?? 'transparent' },
288
+ ]}
289
+ >
290
+ {renderList(
291
+ data,
292
+ (item, i) => `${item.text || item.label || 'segment'}-${i}`,
293
+ (item, _index, key) => (
294
+ <View key={key} style={styles.chartLegendItem}>
295
+ <View
296
+ style={[
297
+ styles.chartLegendDot,
298
+ { backgroundColor: item.color || themeColor(theme, 'primary') },
299
+ ]}
300
+ />
301
+ <ThemedText theme={theme} colorKey="muted" style={styles.chartLegendLabel}>
302
+ {item.text || item.label}
303
+ </ThemedText>
304
+ <ThemedText theme={theme} colorKey="text" style={styles.chartLegendValue}>
305
+ {formatNumber(item.value)}
306
+ </ThemedText>
307
+ </View>
308
+ )
309
+ )}
310
+ </View>
311
+ );
312
+ };
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // Chart sub-components
316
+ // ---------------------------------------------------------------------------
317
+
318
+ interface PieChartCenterLabelProps {
319
+ data: PieDatum[];
320
+ theme: Theme | null;
321
+ caption?: string;
322
+ }
323
+
324
+ const PieChartCenterLabel: React.FC<PieChartCenterLabelProps> = ({ data, theme, caption }) => {
325
+ const total = data.reduce((sum, item) => sum + toNumber(item.value), 0);
326
+ return (
327
+ <View style={styles.pieCenterLabel}>
328
+ <ThemedText theme={theme} colorKey="text" style={styles.pieCenterValue}>
329
+ {formatNumber(total)}
330
+ </ThemedText>
331
+ <ThemedText theme={theme} colorKey="muted" style={styles.pieCenterCaption}>
332
+ {caption}
333
+ </ThemedText>
334
+ </View>
335
+ );
336
+ };
337
+
338
+ const buildChartDefaults = (
339
+ componentName: string,
340
+ styleConfig: ChartStyleConfig,
341
+ configuredProps: NonNullable<ChartConfig['props']>,
342
+ theme: Theme | null
343
+ ): Record<string, unknown> => {
344
+ const defaults: Record<string, unknown> = {};
345
+
346
+ if (AXIS_CHARTS.has(componentName)) {
347
+ const base =
348
+ (componentName === 'BarChart' ? styleConfig.bar : styleConfig.line) || {};
349
+ const axisStyle = styleConfig.axisTextStyle || {};
350
+ Object.assign(defaults, base, {
351
+ yAxisTextStyle: axisStyle,
352
+ xAxisLabelTextStyle: axisStyle,
353
+ });
354
+ }
355
+
356
+ if (
357
+ PIE_CHARTS.has(componentName) &&
358
+ !configuredProps.centerLabelComponent &&
359
+ Array.isArray(configuredProps.data)
360
+ ) {
361
+ defaults.centerLabelComponent = () => (
362
+ <PieChartCenterLabel
363
+ data={configuredProps.data as PieDatum[]}
364
+ theme={theme}
365
+ caption={styleConfig.pie?.centerCaption}
366
+ />
367
+ );
368
+ }
369
+
370
+ return defaults;
371
+ };
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Widget renderers
375
+ // ---------------------------------------------------------------------------
376
+
377
+ interface WidgetRendererProps {
378
+ widget: Widget;
379
+ theme: Theme | null;
380
+ content?: ContentConfig | null;
381
+ chartStyle?: ChartStyleConfig;
382
+ }
383
+
384
+ const ChartWidget: React.FC<WidgetRendererProps> = ({ widget, theme, chartStyle }) => {
385
+ const componentName = widget.chart?.component ?? '';
386
+ const ChartComponent = (GIFTED_CHARTS as Record<string, ChartComponent | undefined>)[
387
+ componentName
388
+ ];
389
+
390
+ if (!ChartComponent) {
391
+ return <EmptyState message={`Unsupported chart: ${componentName}`} theme={theme} />;
392
+ }
393
+
394
+ const configuredProps = widget.chart?.props || {};
395
+ const defaults = buildChartDefaults(componentName, chartStyle || {}, configuredProps, theme);
396
+
397
+ return (
398
+ <View style={styles.chartContainer}>
399
+ {React.createElement(ChartComponent, Object.assign({}, defaults, configuredProps))}
400
+ {PIE_CHARTS.has(componentName) && (
401
+ <ChartLegend data={configuredProps.data as PieDatum[]} theme={theme} />
402
+ )}
403
+ </View>
404
+ );
405
+ };
406
+
407
+ const HeatmapWidget: React.FC<WidgetRendererProps> = ({ widget, theme }) => {
408
+ const data = widget.data || [];
409
+ const lowColor = widget.options?.lowColor || themeColor(theme, 'grid');
410
+ const highColor = widget.options?.highColor || themeColor(theme, 'danger');
411
+
412
+ return (
413
+ <View style={styles.heatmap}>
414
+ {renderList(
415
+ data,
416
+ (item, i) => item.id || item.label || i,
417
+ (item, _index, key) => (
418
+ <View
419
+ key={key}
420
+ style={[
421
+ styles.heatCell,
422
+ {
423
+ backgroundColor:
424
+ item.color || ((item.ratio ?? 0) > 0.5 ? highColor : lowColor),
425
+ },
426
+ ]}
427
+ >
428
+ <ThemedText theme={theme} colorKey="text" style={styles.heatLabel}>
429
+ {item.label || item.name}
430
+ </ThemedText>
431
+ <ThemedText theme={theme} colorKey="text" style={styles.heatValue}>
432
+ {item.value}
433
+ </ThemedText>
434
+ </View>
435
+ )
436
+ )}
437
+ </View>
438
+ );
439
+ };
440
+
441
+ const RankingWidget: React.FC<WidgetRendererProps> = ({ widget, theme }) => {
442
+ const data = widget.data || [];
443
+ return (
444
+ <View>
445
+ {renderList(
446
+ data,
447
+ (item, i) => item.id || i,
448
+ (item, index, key) => (
449
+ <View key={key} style={styles.rankRow}>
450
+ <ThemedText theme={theme} colorKey="muted" style={styles.rankNumber}>
451
+ {index + 1}
452
+ </ThemedText>
453
+ <View style={styles.rankBody}>
454
+ <View style={styles.rankLabels}>
455
+ <ThemedText theme={theme} colorKey="text" style={styles.itemTitle}>
456
+ {item.label}
457
+ </ThemedText>
458
+ <ThemedText theme={theme} colorKey="muted" style={styles.itemSubtitle}>
459
+ {item.value} {widget.unit}
460
+ </ThemedText>
461
+ </View>
462
+ <View
463
+ style={[
464
+ styles.progressTrack,
465
+ { backgroundColor: themeColor(theme, 'border') },
466
+ ]}
467
+ >
468
+ <View
469
+ style={[
470
+ styles.progressFill,
471
+ {
472
+ width: `${item.percentage ?? 0}%` as DimensionValue,
473
+ backgroundColor: themeColor(theme, 'primary'),
474
+ },
475
+ ]}
476
+ />
477
+ </View>
478
+ </View>
479
+ </View>
480
+ )
481
+ )}
482
+ </View>
483
+ );
484
+ };
485
+
486
+ const WIDGET_RENDERERS: Record<string, React.ComponentType<WidgetRendererProps>> = {
487
+ chart: ChartWidget,
488
+ heatmap: HeatmapWidget,
489
+ ranking: RankingWidget,
490
+ };
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // Widget shell
494
+ // ---------------------------------------------------------------------------
495
+
496
+ interface WidgetBodyProps extends WidgetRendererProps {}
497
+
498
+ const WidgetBody: React.FC<WidgetBodyProps> = ({ widget, theme, content, chartStyle }) => {
499
+ const Renderer = WIDGET_RENDERERS[widget.type];
500
+ return Renderer ? (
501
+ <Renderer widget={widget} theme={theme} content={content} chartStyle={chartStyle} />
502
+ ) : (
503
+ <EmptyState
504
+ message={content?.unsupportedWidgetMessage ?? 'Unsupported widget type.'}
505
+ theme={theme}
506
+ />
507
+ );
508
+ };
509
+
510
+ interface DashboardWidgetProps extends WidgetRendererProps {}
511
+
512
+ const DashboardWidget: React.FC<DashboardWidgetProps> = ({
513
+ widget,
514
+ theme,
515
+ content,
516
+ chartStyle,
517
+ }) => {
518
+ const primary = themeColor(theme, 'primary');
519
+
520
+ return (
521
+ <ThemedView
522
+ theme={theme}
523
+ backgroundColorKey="surface"
524
+ borderColorKey="border"
525
+ style={styles.widgetCard}
526
+ >
527
+ <View
528
+ style={[styles.widgetHeader, { borderBottomColor: themeColor(theme, 'border') }]}
529
+ >
530
+ <IconRenderer
531
+ name={widget.iconName}
532
+ icongroup={content?.iconGroup}
533
+ size={content?.widgetIconSize}
534
+ color={primary}
535
+ style={styles.widgetIcon}
536
+ />
537
+ <View style={styles.widgetHeaderCopy}>
538
+ <ThemedText theme={theme} colorKey="text" style={styles.widgetTitle}>
539
+ {widget.title}
540
+ </ThemedText>
541
+ {widget.subtitle ? (
542
+ <ThemedText theme={theme} colorKey="muted" style={styles.widgetSubtitle}>
543
+ {widget.subtitle}
544
+ </ThemedText>
545
+ ) : null}
546
+ </View>
547
+ <HShadowButton
548
+ usetext
549
+ displaytext={`${content?.detailsLabel ?? 'Details'} ›`}
550
+ buttonstyle={[
551
+ styles.detailsButton,
552
+ {
553
+ borderColor: primary,
554
+ backgroundColor: primary ? `${primary}0D` : undefined,
555
+ },
556
+ ]}
557
+ textstyle={[styles.detailsButtonText, { color: primary }]}
558
+ />
559
+ </View>
560
+ <WidgetBody widget={widget} theme={theme} content={content} chartStyle={chartStyle} />
561
+ </ThemedView>
562
+ );
563
+ };
564
+
565
+ // ---------------------------------------------------------------------------
566
+ // Department selector & header
567
+ // ---------------------------------------------------------------------------
568
+
569
+ interface DepartmentButtonProps {
570
+ item: Dashboard;
571
+ selected: boolean;
572
+ onPress: (department: string) => void;
573
+ theme: Theme | null;
574
+ content?: ContentConfig | null;
575
+ }
576
+
577
+ const DepartmentButton: React.FC<DepartmentButtonProps> = ({
578
+ item,
579
+ selected,
580
+ onPress,
581
+ theme,
582
+ content,
583
+ }) => {
584
+ const primary = themeColor(theme, 'primary');
585
+ const onPrimary = themeColor(theme, 'onPrimary');
586
+
587
+ return (
588
+ <HShadowButton
589
+ usetext
590
+ displaytext={item.department ?? ''}
591
+ buttonCallback={() => onPress(item.department ?? '')}
592
+ buttonstyle={[
593
+ styles.departmentButton,
594
+ {
595
+ borderColor: selected ? primary : themeColor(theme, 'border'),
596
+ backgroundColor: selected ? primary : themeColor(theme, 'surface'),
597
+ },
598
+ ]}
599
+ textstyle={[
600
+ styles.departmentButtonText,
601
+ { color: selected ? onPrimary : themeColor(theme, 'text') },
602
+ ]}
603
+ prefixComponent={
604
+ item.iconName ? (
605
+ <HIcons.Icons
606
+ name={item.iconName}
607
+ icongroup={content?.iconGroup}
608
+ size={content?.departmentIconSize}
609
+ color={selected ? onPrimary : primary}
610
+ />
611
+ ) : null
612
+ }
613
+ />
614
+ );
615
+ };
616
+
617
+ interface DateRangeDisplayProps {
618
+ dateRange?: DateRange;
619
+ }
620
+
621
+ const DateRangeDisplay: React.FC<DateRangeDisplayProps> = ({ dateRange }) => {
622
+ if (!dateRange) return null;
623
+ return (
624
+ <View style={styles.datePickerRow}>
625
+ <View style={styles.datePickerWrapper}>
626
+ <SDateTimePicker
627
+ labeltext="From Date"
628
+ icon={dateIcon}
629
+ positionviewstyle={styles.datePickerLabel}
630
+ positiontextstyle={styles.datePickerLabelText}
631
+ pickerstyle={styles.datePicker}
632
+ textstyle={styles.datePickerText}
633
+ passingDate={dateRange.from ? new Date(dateRange.from) : new Date()}
634
+ date={true}
635
+ disabled={true}
636
+ time={false}
637
+ />
638
+ </View>
639
+
640
+ <View style={styles.datePickerWrapper}>
641
+ <SDateTimePicker
642
+ labeltext="To Date"
643
+ icon={dateIcon}
644
+ positionviewstyle={styles.datePickerLabel}
645
+ positiontextstyle={styles.datePickerLabelText}
646
+ pickerstyle={styles.datePicker}
647
+ textstyle={styles.datePickerText}
648
+ passingDate={dateRange.to ? new Date(dateRange.to) : new Date()}
649
+ date={true}
650
+ time={false}
651
+ disabled={true}
652
+ />
653
+ </View>
654
+ </View>
655
+ );
656
+ };
657
+
658
+ interface DashboardHeaderProps {
659
+ dashboard: Dashboard;
660
+ departments: Dashboard[];
661
+ onSelectDepartment: (department: string) => void;
662
+ theme: Theme | null;
663
+ content?: ContentConfig | null;
664
+ horizontalPadding: number;
665
+ }
666
+
667
+ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
668
+ dashboard,
669
+ departments,
670
+ onSelectDepartment,
671
+ theme,
672
+ content,
673
+ horizontalPadding,
674
+ }) => {
675
+ return (
676
+ <ThemedView
677
+ theme={theme}
678
+ backgroundColorKey="surface"
679
+ style={[
680
+ styles.header,
681
+ {
682
+ paddingHorizontal: horizontalPadding,
683
+ borderBottomColor: themeColor(theme, 'border'),
684
+ },
685
+ ]}
686
+ >
687
+ <View style={styles.headerTop}>
688
+ <ThemedText theme={theme} colorKey="text" style={styles.dashboardTitle}>
689
+ {dashboard.dashboardTitle}
690
+ </ThemedText>
691
+ <DateRangeDisplay dateRange={dashboard.dateRange} />
692
+ </View>
693
+ <ScrollView
694
+ horizontal
695
+ showsHorizontalScrollIndicator={false}
696
+ contentContainerStyle={styles.departmentSelector}
697
+ >
698
+ {renderList(
699
+ departments,
700
+ (item, i) => item.dashboardId || item.department || i,
701
+ (item, _index, key) => (
702
+ <DepartmentButton
703
+ key={key}
704
+ item={item}
705
+ selected={item.department === dashboard.department}
706
+ onPress={onSelectDepartment}
707
+ theme={theme}
708
+ content={content}
709
+ />
710
+ )
711
+ )}
712
+ </ScrollView>
713
+ </ThemedView>
714
+ );
715
+ };
716
+
717
+ // ---------------------------------------------------------------------------
718
+ // Loading / empty states
719
+ // ---------------------------------------------------------------------------
720
+
721
+ interface CenteredStateProps {
722
+ theme: Theme | null;
723
+ children?: React.ReactNode;
724
+ }
725
+
726
+ const CenteredState: React.FC<CenteredStateProps> = ({ theme, children }) => (
727
+ <ThemedView theme={theme} backgroundColorKey="background" style={styles.centerState}>
728
+ {children}
729
+ </ThemedView>
730
+ );
731
+
732
+ // ---------------------------------------------------------------------------
733
+ // Main component
734
+ // ---------------------------------------------------------------------------
735
+
736
+ export default function HDashboard({ dashboardData, buttonapi }: HDashboardProps): React.ReactElement {
737
+ const bootstrap = useMemo(() => normalizeDashboardPayload(dashboardData), [dashboardData]);
738
+
739
+ const [departments, setDepartments] = useState<Dashboard[]>(bootstrap.departments);
740
+ const [dashboard, setDashboard] = useState<Dashboard | null>(
741
+ pickInitialDashboard(bootstrap.departments)
742
+ );
743
+ const [apiDashboardConfig, setApiDashboardConfig] = useState<DashboardPayload | null>(null);
744
+ const [isLoading, setIsLoading] = useState<boolean>(false);
745
+
746
+ const theme =
747
+ dashboardData && !Array.isArray(dashboardData)
748
+ ? (dashboardData as DashboardPayload).theme
749
+ : undefined;
750
+ const resolvedTheme: Theme | null =
751
+ theme || apiDashboardConfig?.theme || dashboard?.theme || null;
752
+
753
+ const content: ContentConfig | null =
754
+ (dashboardData && !Array.isArray(dashboardData)
755
+ ? (dashboardData as DashboardPayload).content
756
+ : undefined) ||
757
+ apiDashboardConfig?.content ||
758
+ dashboard?.content ||
759
+ null;
760
+
761
+ const chartStyle: ChartStyleConfig | undefined =
762
+ (dashboardData && !Array.isArray(dashboardData)
763
+ ? (dashboardData as DashboardPayload).chartStyle
764
+ : undefined) ||
765
+ apiDashboardConfig?.chartStyle ||
766
+ dashboard?.chartStyle;
767
+
768
+ const horizontalPadding = Number(content?.horizontalPadding ?? 16);
769
+
770
+ const visibleWidgets = useMemo<Widget[]>(() => {
771
+ if (!dashboard || !Array.isArray(dashboard.widgets)) return [];
772
+ return dashboard.widgets
773
+ .filter((widget) => widget?.layout?.visible)
774
+ .sort((left, right) => (left?.layout?.order ?? 0) - (right?.layout?.order ?? 0));
775
+ }, [dashboard]);
776
+
777
+ const getData = useCallback(async (url: string) => {
778
+ setIsLoading(true);
779
+ try {
780
+ const response = (await NetworkServiceCall('get', url)) as {
781
+ httpstatus?: number;
782
+ data?: DashboardPayload | Dashboard[] | Dashboard;
783
+ };
784
+ if (response?.httpstatus === 200 && response?.data) {
785
+ const normalized = normalizeDashboardPayload(response.data);
786
+ setApiDashboardConfig(normalized);
787
+ setDepartments(normalized.departments);
788
+ setDashboard(pickInitialDashboard(normalized.departments));
789
+ } else {
790
+ notifyMessage('Data Not Found');
791
+ }
792
+ } catch (error) {
793
+ console.error('Error fetching data:', error);
794
+ notifyMessage('Error loading data');
795
+ } finally {
796
+ setIsLoading(false);
797
+ }
798
+ }, []);
799
+
800
+ useEffect(() => {
801
+ if (buttonapi) getData(buttonapi);
802
+ }, [buttonapi, getData]);
803
+
804
+ const selectDepartment = useCallback(
805
+ (nextDepartment: string) => {
806
+ const next = departments.find((item) => item.department === nextDepartment);
807
+ if (next) setDashboard(next);
808
+ },
809
+ [departments]
810
+ );
811
+
812
+ const renderHeader = useCallback(() => {
813
+ if (!dashboard) return null;
814
+ return (
815
+ <DashboardHeader
816
+ dashboard={dashboard}
817
+ departments={departments}
818
+ onSelectDepartment={selectDepartment}
819
+ theme={resolvedTheme}
820
+ content={content}
821
+ horizontalPadding={horizontalPadding}
822
+ />
823
+ );
824
+ }, [dashboard, departments, selectDepartment, resolvedTheme, content, horizontalPadding]);
825
+
826
+ const renderItem = useCallback(
827
+ ({ item }: ListRenderItemInfo<Widget>) => (
828
+ <DashboardWidget
829
+ widget={item}
830
+ theme={resolvedTheme}
831
+ content={content}
832
+ chartStyle={chartStyle}
833
+ />
834
+ ),
835
+ [resolvedTheme, content, chartStyle]
836
+ );
837
+
838
+ if (isLoading && !dashboard) {
839
+ return (
840
+ <CenteredState theme={resolvedTheme}>
841
+ <ActivityIndicator size="large" color={themeColor(resolvedTheme, 'primary')} />
842
+ <ThemedText theme={resolvedTheme} colorKey="muted" style={styles.centerStateText}>
843
+ {content?.loadingLabel ?? 'Loading…'}
844
+ </ThemedText>
845
+ </CenteredState>
846
+ );
847
+ }
848
+
849
+ if (!dashboard) {
850
+ return (
851
+ <CenteredState theme={resolvedTheme}>
852
+ <ThemedText theme={resolvedTheme} colorKey="muted" style={styles.centerStateText}>
853
+ {content?.emptyStateMessage ?? 'No dashboard data available.'}
854
+ </ThemedText>
855
+ </CenteredState>
856
+ );
857
+ }
858
+
859
+ return (
860
+ <FlatList<Widget>
861
+ data={visibleWidgets}
862
+ keyExtractor={(item, index) =>
863
+ item?.id != null ? String(item.id) : String(index)
864
+ }
865
+ renderItem={renderItem}
866
+ ListHeaderComponent={renderHeader}
867
+ keyboardShouldPersistTaps="handled"
868
+ style={[styles.screen, { backgroundColor: themeColor(resolvedTheme, 'background') }]}
869
+ disableVirtualization={Platform.OS === 'web'}
870
+ numColumns={2}
871
+ />
872
+ );
873
+ }
874
+
875
+ // ---------------------------------------------------------------------------
876
+ // Styles
877
+ // ---------------------------------------------------------------------------
878
+
879
+ const styles = StyleSheet.create({
880
+ screen: { flex: 1 },
881
+ centerState: {
882
+ flex: 1,
883
+ alignItems: 'center',
884
+ justifyContent: 'center',
885
+ padding: 24,
886
+ },
887
+ centerStateText: { marginTop: 12, fontSize: 14, textAlign: 'center' },
888
+ header: {
889
+ paddingTop: 12,
890
+ paddingBottom: 10,
891
+ borderBottomWidth: StyleSheet.hairlineWidth,
892
+ marginBottom: 10,
893
+ },
894
+ headerTop: {
895
+ flexDirection: 'row',
896
+ alignItems: 'flex-start',
897
+ justifyContent: 'space-between',
898
+ },
899
+ headerIdentity: { flexDirection: 'row', alignItems: 'center', flex: 1 },
900
+ headerCopy: { flex: 1 },
901
+ dashboardTitle: { fontSize: 24, fontWeight: '800', letterSpacing: -0.2 },
902
+ dateRangeBadge: {
903
+ flexDirection: 'row',
904
+ alignItems: 'center',
905
+ maxWidth: 220,
906
+ flexShrink: 1,
907
+ marginLeft: 12,
908
+ padding: 7,
909
+ borderWidth: 1,
910
+ borderRadius: 14,
911
+ },
912
+ dateRangeIconWrap: {
913
+ width: 32,
914
+ height: 32,
915
+ alignItems: 'center',
916
+ justifyContent: 'center',
917
+ borderRadius: 10,
918
+ },
919
+ dateRangeCopy: { flexShrink: 1, marginLeft: 9 },
920
+ dateRangeLabel: { fontSize: 9, fontWeight: '800', letterSpacing: 0.7 },
921
+ dateRange: { fontSize: 12, fontWeight: '700', marginTop: 2 },
922
+ departmentSelector: { flexDirection: 'row', paddingVertical: 14 },
923
+ departmentButton: {
924
+ flexDirection: 'row',
925
+ alignItems: 'center',
926
+ gap: 7,
927
+ borderWidth: 1,
928
+ borderRadius: 12,
929
+ paddingHorizontal: 14,
930
+ paddingVertical: 8,
931
+ marginRight: 8,
932
+ },
933
+ departmentButtonText: { fontSize: 12, fontWeight: '700' },
934
+ widgetCard: {
935
+ width: '50%',
936
+ borderWidth: 1,
937
+ borderRadius: 12,
938
+ padding: 16,
939
+ marginBottom: 12,
940
+ minHeight: 88,
941
+ overflow: 'hidden',
942
+ },
943
+ chartContainer: { alignItems: 'center', overflow: 'hidden', width: '100%' },
944
+ pieCenterLabel: { alignItems: 'center', justifyContent: 'center' },
945
+ pieCenterValue: { fontSize: 22, fontWeight: '800', letterSpacing: -0.4 },
946
+ pieCenterCaption: { fontSize: 9, marginTop: 2 },
947
+ widgetHeader: {
948
+ minHeight: 38,
949
+ flexDirection: 'row',
950
+ alignItems: 'flex-start',
951
+ justifyContent: 'space-between',
952
+ borderBottomWidth: StyleSheet.hairlineWidth,
953
+ paddingBottom: 10,
954
+ marginBottom: 12,
955
+ },
956
+ widgetHeaderCopy: { flex: 1, paddingRight: 12 },
957
+ widgetIcon: { marginRight: 8, marginTop: 1 },
958
+ widgetTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.2 },
959
+ widgetSubtitle: { fontSize: 12, marginTop: 3, lineHeight: 17, fontWeight: '500' },
960
+ detailsButton: {
961
+ height: 24,
962
+ flexDirection: 'row',
963
+ alignItems: 'center',
964
+ justifyContent: 'center',
965
+ borderWidth: 1,
966
+ borderRadius: 12,
967
+ paddingHorizontal: 10,
968
+ marginTop: 1,
969
+ },
970
+ detailsButtonText: { fontSize: 10, fontWeight: '700' },
971
+ chartLegend: {
972
+ width: '100%',
973
+ flexDirection: 'row',
974
+ flexWrap: 'wrap',
975
+ justifyContent: 'flex-start',
976
+ marginTop: 14,
977
+ borderTopWidth: StyleSheet.hairlineWidth,
978
+ paddingTop: 10,
979
+ },
980
+ chartLegendItem: {
981
+ width: '50%',
982
+ flexDirection: 'row',
983
+ alignItems: 'center',
984
+ paddingVertical: 5,
985
+ paddingRight: 10,
986
+ },
987
+ chartLegendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 7 },
988
+ chartLegendLabel: { flex: 1, fontSize: 10, fontWeight: '600' },
989
+ chartLegendValue: { fontSize: 10, fontWeight: '800' },
990
+ emptyText: { fontSize: 13, textAlign: 'center', paddingVertical: 26 },
991
+ itemTitle: { fontSize: 13, fontWeight: '700' },
992
+ itemSubtitle: { fontSize: 11, marginTop: 3 },
993
+ progressTrack: { height: 10, borderRadius: 8, overflow: 'hidden', marginVertical: 6 },
994
+ progressFill: { height: '100%', borderRadius: 8 },
995
+ heatmap: { flexDirection: 'row', flexWrap: 'wrap' },
996
+ heatCell: {
997
+ flexGrow: 1,
998
+ flexBasis: '25%',
999
+ minWidth: 64,
1000
+ minHeight: 58,
1001
+ alignItems: 'center',
1002
+ justifyContent: 'center',
1003
+ margin: 3,
1004
+ borderRadius: 8,
1005
+ },
1006
+ heatLabel: { fontSize: 11, fontWeight: '600' },
1007
+ heatValue: { fontSize: 14, fontWeight: '700', marginTop: 3 },
1008
+ rankRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 6 },
1009
+ rankNumber: { width: 24, fontSize: 13, fontWeight: '700' },
1010
+ rankBody: { flex: 1 },
1011
+ rankLabels: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
1012
+ datePickerRow: {
1013
+ justifyContent: 'space-between',
1014
+ flexDirection: 'row',
1015
+ },
1016
+ datePickerWrapper: {
1017
+ marginHorizontal: '2%',
1018
+ },
1019
+ datePickerLabel: {
1020
+ position: 'absolute',
1021
+ top: normalize(-8),
1022
+ left: normalize(10),
1023
+ backgroundColor: 'white',
1024
+ paddingHorizontal: normalize(5),
1025
+ zIndex: 1,
1026
+ },
1027
+ datePickerLabelText: {
1028
+ color: Global.AppTheme.primary_color,
1029
+ fontSize: normalize(12),
1030
+ fontWeight: '600',
1031
+ },
1032
+ datePicker: {
1033
+ backgroundColor: 'white',
1034
+ width: '100%',
1035
+ borderColor: '#d1d1d1',
1036
+ borderWidth: 1,
1037
+ borderRadius: normalize(8),
1038
+ height: normalize(30),
1039
+ justifyContent: 'center',
1040
+ shadowColor: '#000',
1041
+ shadowOffset: { width: 0, height: 1 },
1042
+ shadowOpacity: 0.05,
1043
+ shadowRadius: 2,
1044
+ elevation: 2,
1045
+ },
1046
+ datePickerText: {
1047
+ fontSize: normalize(14),
1048
+ color: '#333',
1049
+ fontWeight: '500',
1050
+ paddingLeft: normalize(10),
1051
+ },
1052
+ });
1053
+
1054
+ /**
1055
+ * `dateIcon` is passed to `SDateTimePicker`'s `icon` prop, which expects an
1056
+ * icon descriptor ({ icongroup, name, color, size }). It's NOT a `ViewStyle`,
1057
+ * so `StyleSheet.create` rejects it. Plain object keeps the same shape.
1058
+ */
1059
+ const dateIcon = {
1060
+ icongroup: 5,
1061
+ name: 'calendar-alt',
1062
+ color: Global.AppTheme.primary_color,
1063
+ size: normalize(15),
1064
+ };