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

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