@controleonline/ui-default 1.0.263

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.
Files changed (56) hide show
  1. package/.github/agents/developer.agent.md +30 -0
  2. package/.github/agents/devops.agent.md +30 -0
  3. package/.github/agents/qa.agent.md +30 -0
  4. package/.github/agents/security.agent.md +30 -0
  5. package/.scrutinizer.yml +20 -0
  6. package/AGENTS.md +47 -0
  7. package/FUNDING.yml +1 -0
  8. package/package.json +21 -0
  9. package/src/react/components/errors/DefaultErrors.js +360 -0
  10. package/src/react/components/files/DefaultFile.js +46 -0
  11. package/src/react/components/filters/CompactFilterSelector.js +262 -0
  12. package/src/react/components/filters/CompactFilterSelector.styles.js +124 -0
  13. package/src/react/components/filters/DateShortcutFilter.js +264 -0
  14. package/src/react/components/filters/DateShortcutFilter.styles.js +82 -0
  15. package/src/react/components/filters/DefaultColumnFilter.js +97 -0
  16. package/src/react/components/filters/DefaultColumnFilter.styles.js +21 -0
  17. package/src/react/components/filters/DefaultExternalFilters.js +441 -0
  18. package/src/react/components/filters/DefaultExternalFilters.styles.js +177 -0
  19. package/src/react/components/filters/DefaultSearch.js +103 -0
  20. package/src/react/components/filters/DefaultSearch.styles.js +70 -0
  21. package/src/react/components/filters/dateFilterSelection.js +29 -0
  22. package/src/react/components/form/DefaultForm.js +198 -0
  23. package/src/react/components/form/DefaultForm.styles.js +70 -0
  24. package/src/react/components/help/DefaultTooltip.js +87 -0
  25. package/src/react/components/help/DefaultTooltip.styles.js +61 -0
  26. package/src/react/components/inputs/DefaultInput.js +160 -0
  27. package/src/react/components/inputs/DefaultInput.styles.js +93 -0
  28. package/src/react/components/inputs/DefaultSelect.js +192 -0
  29. package/src/react/components/inputs/DefaultSelect.styles.js +65 -0
  30. package/src/react/components/inputs/defaultInputUtils.js +230 -0
  31. package/src/react/components/map/DefaultGoogleMap.styles.js +9 -0
  32. package/src/react/components/map/DefaultGoogleMap.web.js +698 -0
  33. package/src/react/components/map/DefaultMap.native.js +71 -0
  34. package/src/react/components/map/DefaultMap.shared.js +762 -0
  35. package/src/react/components/map/DefaultMap.styles.js +16 -0
  36. package/src/react/components/map/DefaultMap.web.js +66 -0
  37. package/src/react/components/map/DefaultNativeMap.native.js +615 -0
  38. package/src/react/components/map/DefaultNativeMap.shared.js +532 -0
  39. package/src/react/components/map/DefaultNativeMap.styles.js +122 -0
  40. package/src/react/components/table/DefaultTable.js +1897 -0
  41. package/src/react/components/table/DefaultTable.styles.js +610 -0
  42. package/src/react/index.js +10 -0
  43. package/src/react/utils/tableVisibleColumnsPreferences.js +264 -0
  44. package/src/store/default/actions.js +444 -0
  45. package/src/store/default/getters.js +28 -0
  46. package/src/store/default/mutation_types.js +26 -0
  47. package/src/store/default/mutations.js +138 -0
  48. package/src/tests/react/components/DateShortcutFilter.test.js +96 -0
  49. package/src/tests/react/components/errors/DefaultErrors.test.js +162 -0
  50. package/src/tests/react/components/files/DefaultFile.test.js +80 -0
  51. package/src/tests/react/components/map/DefaultMap.shared.test.js +162 -0
  52. package/src/tests/react/filters/dateFilterSelection.test.js +46 -0
  53. package/src/tests/react/inputs/defaultInputUtils.test.js +45 -0
  54. package/src/tests/react/store/defaultActions.test.js +112 -0
  55. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +223 -0
  56. package/src/utils/filters.js +56 -0
@@ -0,0 +1,262 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ ScrollView,
5
+ Text,
6
+ TouchableOpacity,
7
+ TouchableWithoutFeedback,
8
+ useWindowDimensions,
9
+ View,
10
+ } from 'react-native';
11
+ import Icon from 'react-native-vector-icons/Feather';
12
+ import {
13
+ formatStoreColumnLabel,
14
+ resolveStoreColumn,
15
+ resolveStoreConfigByName,
16
+ } from '@controleonline/ui-common/src/react/utils/storeColumns';
17
+ import createStyles from './CompactFilterSelector.styles';
18
+
19
+ const buildTheme = ({ accentColor, themeColors = {} }) => ({
20
+ accentColor: accentColor,
21
+ activeBackgroundColor:
22
+ themeColors.activeBackgroundColor || `${accentColor}14`,
23
+ activeIconBackgroundColor:
24
+ themeColors.activeIconBackgroundColor || `${accentColor}24`,
25
+ activeChevronColor:
26
+ themeColors.activeChevronColor ||
27
+ themeColors.activeIconColor ||
28
+ accentColor,
29
+ activeIconColor: themeColors.activeIconColor || accentColor,
30
+ activeTextColor: themeColors.activeTextColor || accentColor,
31
+ backgroundColor: themeColors.backgroundColor || '#F8FAFC',
32
+ borderColor: themeColors.borderColor || '#E2E8F0',
33
+ captionColor: themeColors.captionColor || '#94A3B8',
34
+ chevronColor: themeColors.chevronColor || '#94A3B8',
35
+ closeIconColor: themeColors.closeIconColor || '#64748B',
36
+ iconBackgroundColor: themeColors.iconBackgroundColor || '#E2E8F0',
37
+ iconColor: themeColors.iconColor || '#64748B',
38
+ modalBackgroundColor: themeColors.modalBackgroundColor || '#FFFFFF',
39
+ modalOverlayColor:
40
+ themeColors.modalOverlayColor || 'rgba(15, 23, 42, 0.35)',
41
+ modalTitleColor: themeColors.modalTitleColor || '#0F172A',
42
+ optionBackgroundColor: themeColors.optionBackgroundColor || '#F8FAFC',
43
+ optionBorderColor: themeColors.optionBorderColor || '#E2E8F0',
44
+ optionSelectedTextColor:
45
+ themeColors.optionSelectedTextColor || accentColor,
46
+ textColor: themeColors.textColor || '#0F172A',
47
+ });
48
+
49
+ const noop = () => {};
50
+
51
+ const normalizeText = value => String(value || '').trim();
52
+
53
+ const resolveStoreName = store => {
54
+ if (typeof store === 'string') return normalizeText(store);
55
+ return normalizeText(store?.storeName || store?.name || '');
56
+ };
57
+
58
+ const resolveStoreFieldLabel = ({ fallbackLabel = '', field = '', store = '' }) => {
59
+ const storeName = resolveStoreName(store);
60
+ const fieldName = normalizeText(field);
61
+ if (!storeName || !fieldName) return fallbackLabel;
62
+
63
+ const { columns } = resolveStoreConfigByName(storeName);
64
+ const column = resolveStoreColumn(columns, fieldName);
65
+ const labelKey = normalizeText(column?.label || fieldName);
66
+ const translatedLabel =
67
+ global.t?.t(storeName, 'label', labelKey) ||
68
+ global.t?.t(storeName, 'input', labelKey);
69
+
70
+ return (
71
+ translatedLabel ||
72
+ formatStoreColumnLabel({
73
+ columns,
74
+ fallbackLabel: fallbackLabel || labelKey,
75
+ fieldName,
76
+ storeName,
77
+ }) ||
78
+ fallbackLabel
79
+ );
80
+ };
81
+
82
+ const CompactFilterSelector = ({
83
+ accentColor = '#2563EB',
84
+ active = false,
85
+ children = null,
86
+ dense = false,
87
+ disabled = false,
88
+ icon = 'sliders',
89
+ label = '',
90
+ labelCaption = '',
91
+ onClose = null,
92
+ onSelect = null,
93
+ options = [],
94
+ selectedKey = '',
95
+ field = '',
96
+ store = '',
97
+ themeColors = {},
98
+ title = '',
99
+ }) => {
100
+ const [visible, setVisible] = useState(false);
101
+ const { width: windowWidth } = useWindowDimensions();
102
+ const resolvedTheme = useMemo(
103
+ () => buildTheme({ accentColor, themeColors }),
104
+ [accentColor, themeColors],
105
+ );
106
+ const styles = useMemo(
107
+ () => createStyles(resolvedTheme, dense, windowWidth),
108
+ [dense, resolvedTheme, windowWidth],
109
+ );
110
+ const storeFieldLabel = resolveStoreFieldLabel({
111
+ fallbackLabel: labelCaption || title,
112
+ field,
113
+ store,
114
+ });
115
+ const resolvedLabelCaption = labelCaption || storeFieldLabel;
116
+ const resolvedTitle = title || storeFieldLabel;
117
+
118
+ const closeModal = useCallback(() => {
119
+ setVisible(false);
120
+ onClose?.();
121
+ }, [onClose]);
122
+
123
+ const openModal = useCallback(() => {
124
+ if (!disabled) {
125
+ setVisible(true);
126
+ }
127
+ }, [disabled]);
128
+
129
+ const handleSelect = useCallback(optionKey => {
130
+ const shouldClose = onSelect?.(optionKey, {
131
+ close: closeModal,
132
+ open: openModal,
133
+ });
134
+
135
+ if (shouldClose !== false) {
136
+ closeModal();
137
+ }
138
+ }, [closeModal, onSelect, openModal]);
139
+
140
+ const resolvedChildren = useMemo(() => {
141
+ if (typeof children === 'function') {
142
+ return children({ close: closeModal, open: openModal });
143
+ }
144
+
145
+ return children;
146
+ }, [children, closeModal, openModal]);
147
+
148
+ return (
149
+ <>
150
+ <TouchableOpacity
151
+ style={[
152
+ styles.trigger,
153
+ active ? styles.triggerActive : null,
154
+ ]}
155
+ activeOpacity={0.9}
156
+ disabled={disabled}
157
+ onPress={openModal}
158
+ >
159
+ <View style={[styles.iconWrap, active ? styles.iconWrapActive : null]}>
160
+ <Icon
161
+ name={icon}
162
+ size={dense ? 14 : 15}
163
+ color={active ? resolvedTheme.activeIconColor : resolvedTheme.iconColor}
164
+ />
165
+ </View>
166
+
167
+ <View style={styles.textWrap}>
168
+ {!!resolvedLabelCaption && (
169
+ <Text
170
+ numberOfLines={1}
171
+ style={[
172
+ styles.triggerCaption,
173
+ active ? styles.triggerCaptionActive : null,
174
+ ]}
175
+ >
176
+ {resolvedLabelCaption}
177
+ </Text>
178
+ )}
179
+
180
+ <Text
181
+ numberOfLines={1}
182
+ style={[styles.triggerText, active ? styles.triggerTextActive : null]}
183
+ >
184
+ {label}
185
+ </Text>
186
+ </View>
187
+
188
+ <Icon
189
+ name="chevron-down"
190
+ size={dense ? 14 : 16}
191
+ color={active ? resolvedTheme.activeChevronColor : resolvedTheme.chevronColor}
192
+ />
193
+ </TouchableOpacity>
194
+
195
+ <Modal
196
+ transparent
197
+ visible={visible}
198
+ animationType="fade"
199
+ onRequestClose={closeModal}
200
+ >
201
+ <TouchableWithoutFeedback onPress={closeModal}>
202
+ <View style={styles.modalOverlay}>
203
+ <TouchableWithoutFeedback onPress={noop}>
204
+ <View style={styles.modalCard}>
205
+ <View style={styles.modalHeader}>
206
+ <Text style={styles.modalTitle}>{resolvedTitle}</Text>
207
+
208
+ <TouchableOpacity onPress={closeModal} activeOpacity={0.8}>
209
+ <Icon name="x" size={20} color={resolvedTheme.closeIconColor} />
210
+ </TouchableOpacity>
211
+ </View>
212
+
213
+ <ScrollView
214
+ style={styles.modalScroll}
215
+ contentContainerStyle={styles.modalContent}
216
+ keyboardShouldPersistTaps="handled"
217
+ >
218
+ {options.map(option => {
219
+ const isSelected = option.key === selectedKey;
220
+
221
+ return (
222
+ <TouchableOpacity
223
+ key={option.key}
224
+ style={[
225
+ styles.modalOption,
226
+ isSelected ? styles.modalOptionActive : null,
227
+ ]}
228
+ activeOpacity={0.9}
229
+ onPress={() => handleSelect(option.key)}
230
+ >
231
+ <Text
232
+ style={[
233
+ styles.modalOptionText,
234
+ isSelected ? styles.modalOptionTextActive : null,
235
+ ]}
236
+ >
237
+ {option.label}
238
+ </Text>
239
+
240
+ {isSelected ? (
241
+ <Icon
242
+ name="check"
243
+ size={16}
244
+ color={resolvedTheme.activeIconColor}
245
+ />
246
+ ) : null}
247
+ </TouchableOpacity>
248
+ );
249
+ })}
250
+
251
+ {resolvedChildren}
252
+ </ScrollView>
253
+ </View>
254
+ </TouchableWithoutFeedback>
255
+ </View>
256
+ </TouchableWithoutFeedback>
257
+ </Modal>
258
+ </>
259
+ );
260
+ };
261
+
262
+ export default CompactFilterSelector;
@@ -0,0 +1,124 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ const createStyles = (theme, dense = false, windowWidth = 0) => {
4
+ const compactModal = Number(windowWidth) >= 768;
5
+
6
+ return StyleSheet.create({
7
+ trigger: {
8
+ minWidth: 0,
9
+ flexGrow: 1,
10
+ flexDirection: 'row',
11
+ alignItems: 'center',
12
+ gap: dense ? 8 : 10,
13
+ borderWidth: 1,
14
+ borderColor: theme.borderColor,
15
+ borderRadius: dense ? 12 : 14,
16
+ paddingHorizontal: dense ? 10 : 12,
17
+ paddingVertical: dense ? 8 : 10,
18
+ backgroundColor: theme.backgroundColor,
19
+ },
20
+ triggerActive: {
21
+ borderColor: theme.accentColor,
22
+ backgroundColor: theme.activeBackgroundColor,
23
+ },
24
+ iconWrap: {
25
+ width: dense ? 24 : 28,
26
+ height: dense ? 24 : 28,
27
+ borderRadius: dense ? 8 : 10,
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ backgroundColor: theme.iconBackgroundColor,
31
+ },
32
+ iconWrapActive: {
33
+ backgroundColor: theme.activeIconBackgroundColor,
34
+ },
35
+ textWrap: {
36
+ flex: 1,
37
+ minWidth: 0,
38
+ },
39
+ triggerCaption: {
40
+ fontSize: dense ? 9 : 10,
41
+ lineHeight: dense ? 10 : 12,
42
+ fontWeight: '800',
43
+ letterSpacing: 0.45,
44
+ textTransform: 'uppercase',
45
+ color: theme.captionColor,
46
+ marginBottom: dense ? 1 : 2,
47
+ },
48
+ triggerCaptionActive: {
49
+ color: theme.activeTextColor,
50
+ },
51
+ triggerText: {
52
+ fontSize: dense ? 12 : 13,
53
+ lineHeight: dense ? 14 : 16,
54
+ fontWeight: dense ? '800' : '700',
55
+ color: theme.textColor,
56
+ },
57
+ triggerTextActive: {
58
+ color: theme.activeTextColor,
59
+ },
60
+ modalOverlay: {
61
+ flex: 1,
62
+ backgroundColor: theme.modalOverlayColor,
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ paddingHorizontal: compactModal ? 24 : 14,
66
+ },
67
+ modalCard: {
68
+ width: '100%',
69
+ maxWidth: compactModal ? 420 : undefined,
70
+ backgroundColor: theme.modalBackgroundColor,
71
+ borderRadius: 24,
72
+ maxHeight: compactModal ? 560 : '82%',
73
+ paddingHorizontal: 16,
74
+ paddingTop: 16,
75
+ paddingBottom: 10,
76
+ },
77
+ modalHeader: {
78
+ flexDirection: 'row',
79
+ alignItems: 'center',
80
+ justifyContent: 'space-between',
81
+ marginBottom: 10,
82
+ },
83
+ modalTitle: {
84
+ fontSize: 18,
85
+ fontWeight: '800',
86
+ color: theme.modalTitleColor,
87
+ },
88
+ modalScroll: {
89
+ flexGrow: 0,
90
+ },
91
+ modalContent: {
92
+ paddingBottom: 10,
93
+ gap: 10,
94
+ },
95
+ modalOption: {
96
+ minHeight: 50,
97
+ borderWidth: 1,
98
+ borderColor: theme.optionBorderColor,
99
+ borderRadius: 16,
100
+ paddingHorizontal: 14,
101
+ paddingVertical: 12,
102
+ backgroundColor: theme.optionBackgroundColor,
103
+ flexDirection: 'row',
104
+ alignItems: 'center',
105
+ justifyContent: 'space-between',
106
+ gap: 8,
107
+ },
108
+ modalOptionActive: {
109
+ borderColor: theme.accentColor,
110
+ backgroundColor: theme.activeBackgroundColor,
111
+ },
112
+ modalOptionText: {
113
+ flex: 1,
114
+ fontSize: 14,
115
+ fontWeight: '700',
116
+ color: theme.textColor,
117
+ },
118
+ modalOptionTextActive: {
119
+ color: theme.optionSelectedTextColor,
120
+ },
121
+ });
122
+ };
123
+
124
+ export default createStyles;
@@ -0,0 +1,264 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { Text, TextInput, TouchableOpacity, View } from 'react-native';
3
+ import CompactFilterSelector from './CompactFilterSelector';
4
+ import createStyles from './DateShortcutFilter.styles';
5
+ import {
6
+ buildDateFilterOptions,
7
+ DEFAULT_DATE_FILTER_OPTION_KEYS,
8
+ resolveDateFilterTitle,
9
+ resolveDateRangeSummary,
10
+ validateCustomDateRange,
11
+ } from '@controleonline/ui-common/src/react/utils/dateRangeFilter';
12
+ import {
13
+ formatStoreColumnLabel,
14
+ resolveStoreColumn,
15
+ resolveStoreConfigByName,
16
+ } from '@controleonline/ui-common/src/react/utils/storeColumns';
17
+
18
+ const createDefaultColors = colors => ({
19
+ accent: colors?.accent,
20
+ appBg: colors?.appBg || 'transparent',
21
+ border: colors?.border,
22
+ borderSoft: colors?.borderSoft,
23
+ cardBg: colors?.cardBg,
24
+ cardBgSoft: colors?.cardBgSoft,
25
+ danger: colors?.danger,
26
+ isLight: typeof colors?.isLight === 'boolean' ? colors.isLight : true,
27
+ panelBg: colors?.panelBg,
28
+ pillTextDark: colors?.pillTextDark,
29
+ textPrimary: colors?.textPrimary,
30
+ textSecondary: colors?.textSecondary,
31
+ });
32
+
33
+ const COMPACT_DATE_LABELS = {
34
+ all: 'Todos',
35
+ today: 'Hoje',
36
+ yesterday: 'Ontem',
37
+ '7d': '7 dias',
38
+ '30d': '30 dias',
39
+ '90d': '90 dias',
40
+ this_month: 'Este mes',
41
+ last_month: 'Mes ant.',
42
+ custom: 'Periodo',
43
+ };
44
+
45
+ const normalizeText = value => String(value || '').trim();
46
+
47
+ const resolveStoreName = store => {
48
+ if (typeof store === 'string') return normalizeText(store);
49
+ return normalizeText(store?.storeName || store?.name || '');
50
+ };
51
+
52
+ const resolveStoreFieldLabel = ({ fallbackLabel = '', field = '', store = '' }) => {
53
+ const storeName = resolveStoreName(store);
54
+ const fieldName = normalizeText(field);
55
+ if (!storeName || !fieldName) return fallbackLabel;
56
+
57
+ const { columns } = resolveStoreConfigByName(storeName);
58
+ const column = resolveStoreColumn(columns, fieldName);
59
+ const labelKey = normalizeText(column?.label || fieldName);
60
+ const translatedLabel =
61
+ global.t?.t(storeName, 'label', labelKey) ||
62
+ global.t?.t(storeName, 'input', labelKey);
63
+
64
+ return (
65
+ translatedLabel ||
66
+ formatStoreColumnLabel({
67
+ columns,
68
+ fallbackLabel: fallbackLabel || labelKey,
69
+ fieldName,
70
+ storeName,
71
+ }) ||
72
+ fallbackLabel
73
+ );
74
+ };
75
+
76
+ const DateShortcutFilter = ({
77
+ value = '',
78
+ onChange = null,
79
+ customRange = null,
80
+ onCustomRangeChange = null,
81
+ colors = null,
82
+ dense = false,
83
+ field = '',
84
+ labelCaption = '',
85
+ optionKeys = DEFAULT_DATE_FILTER_OPTION_KEYS,
86
+ store = '',
87
+ }) => {
88
+ const themeColors = useMemo(() => createDefaultColors(colors), [colors]);
89
+ const styles = useMemo(() => createStyles(themeColors), [themeColors]);
90
+ const [customFromInput, setCustomFromInput] = useState('');
91
+ const [customToInput, setCustomToInput] = useState('');
92
+ const [dateValidationMessage, setDateValidationMessage] = useState('');
93
+ const [isCustomEditorVisible, setIsCustomEditorVisible] = useState(
94
+ value === 'custom',
95
+ );
96
+ const options = buildDateFilterOptions(optionKeys);
97
+ const periodLabel = resolveStoreFieldLabel({
98
+ fallbackLabel: labelCaption || resolveDateFilterTitle(),
99
+ field,
100
+ store,
101
+ });
102
+ const activeRangeSummary = useMemo(
103
+ () => resolveDateRangeSummary(value, customRange),
104
+ [customRange, value],
105
+ );
106
+ const currentOptionLabel = useMemo(
107
+ () => options.find(option => option.key === value)?.label || '',
108
+ [options, value],
109
+ );
110
+ const selectedLabel = useMemo(() => (
111
+ value === 'custom'
112
+ ? activeRangeSummary || currentOptionLabel
113
+ : currentOptionLabel || activeRangeSummary
114
+ ), [activeRangeSummary, currentOptionLabel, value]);
115
+ const compactSelectedLabel = useMemo(() => {
116
+ if (value === 'custom') {
117
+ return activeRangeSummary || COMPACT_DATE_LABELS.custom;
118
+ }
119
+
120
+ return COMPACT_DATE_LABELS[value] || currentOptionLabel || selectedLabel;
121
+ }, [activeRangeSummary, currentOptionLabel, selectedLabel, value]);
122
+
123
+ useEffect(() => {
124
+ setCustomFromInput(String(customRange?.from || ''));
125
+ setCustomToInput(String(customRange?.to || ''));
126
+ }, [customRange?.from, customRange?.to]);
127
+
128
+ useEffect(() => {
129
+ if (value !== 'custom') {
130
+ setDateValidationMessage('');
131
+ setIsCustomEditorVisible(false);
132
+ }
133
+ }, [value]);
134
+
135
+ // Custom ranges are applied explicitly so the parent only refreshes when valid.
136
+ const applyCustomRange = useCallback(() => {
137
+ const validationMessage = validateCustomDateRange(
138
+ customFromInput,
139
+ customToInput,
140
+ );
141
+
142
+ setDateValidationMessage(validationMessage);
143
+
144
+ if (validationMessage) {
145
+ return false;
146
+ }
147
+
148
+ onCustomRangeChange?.({
149
+ from: String(customFromInput || '').trim(),
150
+ to: String(customToInput || '').trim(),
151
+ });
152
+ return true;
153
+ }, [customFromInput, customToInput, onCustomRangeChange]);
154
+
155
+ const clearCustomRange = useCallback(() => {
156
+ setCustomFromInput('');
157
+ setCustomToInput('');
158
+ setDateValidationMessage('');
159
+ onCustomRangeChange?.({
160
+ from: '',
161
+ to: '',
162
+ });
163
+ }, [onCustomRangeChange]);
164
+
165
+ const handleSelect = useCallback(optionKey => {
166
+ if (optionKey === 'custom') {
167
+ setIsCustomEditorVisible(true);
168
+ setDateValidationMessage('');
169
+ return false;
170
+ }
171
+
172
+ setIsCustomEditorVisible(false);
173
+ onChange?.(optionKey);
174
+ return true;
175
+ }, [onChange]);
176
+
177
+ return (
178
+ <CompactFilterSelector
179
+ accentColor={themeColors.accent}
180
+ dense={dense}
181
+ active={value !== 'all'}
182
+ icon="calendar"
183
+ label={dense ? compactSelectedLabel : selectedLabel}
184
+ labelCaption={labelCaption}
185
+ options={options}
186
+ selectedKey={isCustomEditorVisible || value === 'custom' ? 'custom' : value}
187
+ title=""
188
+ store={store}
189
+ field={field}
190
+ onSelect={handleSelect}
191
+ >
192
+ {({ close }) => (isCustomEditorVisible || value === 'custom') ? (
193
+ <View style={styles.customRangeWrap}>
194
+ <Text style={styles.customLabel}>{periodLabel}</Text>
195
+
196
+ <View style={styles.customInputsRow}>
197
+ <TextInput
198
+ value={customFromInput}
199
+ onChangeText={setCustomFromInput}
200
+ placeholder={global.t?.t('orders', 'placeholder', 'date_from')}
201
+ placeholderTextColor={themeColors.textSecondary}
202
+ style={styles.customInput}
203
+ autoCapitalize="none"
204
+ autoCorrect={false}
205
+ />
206
+
207
+ <TextInput
208
+ value={customToInput}
209
+ onChangeText={setCustomToInput}
210
+ placeholder={global.t?.t('orders', 'placeholder', 'date_to')}
211
+ placeholderTextColor={themeColors.textSecondary}
212
+ style={styles.customInput}
213
+ autoCapitalize="none"
214
+ autoCorrect={false}
215
+ />
216
+ </View>
217
+
218
+ {!!activeRangeSummary && (
219
+ <Text style={styles.currentDateValue}>
220
+ {activeRangeSummary}
221
+ </Text>
222
+ )}
223
+
224
+ {!!dateValidationMessage && (
225
+ <Text style={styles.validationText}>
226
+ {dateValidationMessage}
227
+ </Text>
228
+ )}
229
+
230
+ <View style={styles.customActionsRow}>
231
+ <TouchableOpacity
232
+ style={styles.secondaryButton}
233
+ activeOpacity={0.9}
234
+ onPress={clearCustomRange}
235
+ >
236
+ <Text style={styles.secondaryButtonText}>
237
+ {global.t?.t('orders', 'button', 'clear')}
238
+ </Text>
239
+ </TouchableOpacity>
240
+
241
+ <TouchableOpacity
242
+ style={styles.primaryButton}
243
+ activeOpacity={0.9}
244
+ onPress={() => {
245
+ const applied = applyCustomRange();
246
+ if (!applied) {
247
+ return;
248
+ }
249
+
250
+ close();
251
+ }}
252
+ >
253
+ <Text style={styles.primaryButtonText}>
254
+ {global.t?.t('orders', 'button', 'apply_period')}
255
+ </Text>
256
+ </TouchableOpacity>
257
+ </View>
258
+ </View>
259
+ ) : null}
260
+ </CompactFilterSelector>
261
+ );
262
+ };
263
+
264
+ export default DateShortcutFilter;