@controleonline/ui-common 1.2.70

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 (203) 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/.github/workflows/npmjs.yml +35 -0
  6. package/.github/workflows/pull-request-checks.yml +19 -0
  7. package/.scrutinizer.yml +24 -0
  8. package/FUNDING.yml +1 -0
  9. package/package.json +28 -0
  10. package/src/api/fetch.js +74 -0
  11. package/src/api/index.js +155 -0
  12. package/src/api/localDB.js +131 -0
  13. package/src/api/queue.js +66 -0
  14. package/src/react/assets/tap.mp3 +0 -0
  15. package/src/react/components/AddImportModal.js +157 -0
  16. package/src/react/components/AddImportModal.styles.js +81 -0
  17. package/src/react/components/BackgroundRuntimeBridge.js +219 -0
  18. package/src/react/components/BottomNavigationBar.config.js +238 -0
  19. package/src/react/components/BottomNavigationBar.js +120 -0
  20. package/src/react/components/BottomNavigationBar.styles.js +110 -0
  21. package/src/react/components/CategoryForm.js +572 -0
  22. package/src/react/components/CategoryForm.styles.js +334 -0
  23. package/src/react/components/ConfirmModal.js +24 -0
  24. package/src/react/components/ConfirmModal.styles.js +43 -0
  25. package/src/react/components/ContextHelpButton.js +69 -0
  26. package/src/react/components/ContextHelpButton.styles.js +62 -0
  27. package/src/react/components/DefaultProvider.native.js +848 -0
  28. package/src/react/components/DefaultProvider.styles.js +21 -0
  29. package/src/react/components/DefaultProvider.web.js +909 -0
  30. package/src/react/components/DeliveryPushBridge.native.js +314 -0
  31. package/src/react/components/DeliveryPushBridge.web.js +5 -0
  32. package/src/react/components/DeviceAlertSoundService.js +499 -0
  33. package/src/react/components/EntityLogContent.js +909 -0
  34. package/src/react/components/EntityLogContent.styles.js +400 -0
  35. package/src/react/components/EventBus.js +12 -0
  36. package/src/react/components/KioskModeBridge.js +65 -0
  37. package/src/react/components/LauncherModeBridge.js +47 -0
  38. package/src/react/components/LinkedOrderProductsTab.js +388 -0
  39. package/src/react/components/LinkedOrderProductsTab.styles.js +180 -0
  40. package/src/react/components/ManagerPushBridge.native.js +345 -0
  41. package/src/react/components/MessageService.js +314 -0
  42. package/src/react/components/Paywall/Google.js +89 -0
  43. package/src/react/components/PrintService.js +778 -0
  44. package/src/react/components/ProductCatalogCacheService.js +45 -0
  45. package/src/react/components/RemoteCheckoutService.js +292 -0
  46. package/src/react/components/RuntimeBottomNavigationBar.js +151 -0
  47. package/src/react/components/RuntimeInfoFooter.js +214 -0
  48. package/src/react/components/RuntimeInfoFooter.styles.js +56 -0
  49. package/src/react/components/SystemErrorToast.js +29 -0
  50. package/src/react/components/SystemErrorToast.styles.js +38 -0
  51. package/src/react/components/TouchFeedbackProvider.js +65 -0
  52. package/src/react/components/UnifiedPaymentBar.js +155 -0
  53. package/src/react/components/UnifiedPaymentBar.styles.js +174 -0
  54. package/src/react/components/WebsocketListener.native.js +487 -0
  55. package/src/react/components/WebsocketListener.web.js +102 -0
  56. package/src/react/components/inputs/IdInput.js +49 -0
  57. package/src/react/components/inputs/IdInput.styles.js +26 -0
  58. package/src/react/components/inputs/InvoiceDueDateFilterInput.js +74 -0
  59. package/src/react/components/inputs/InvoiceDueDateFilterInput.styles.js +34 -0
  60. package/src/react/components/inputs/InvoiceDueDateRangeFilterInput.js +115 -0
  61. package/src/react/components/inputs/InvoiceDueDateRangeFilterInput.styles.js +39 -0
  62. package/src/react/components/lists/CategoriesList.js +56 -0
  63. package/src/react/components/lists/PaymentTypeList.js +55 -0
  64. package/src/react/components/lists/ReceiverList.js +56 -0
  65. package/src/react/components/lists/SelectList.styles.js +18 -0
  66. package/src/react/components/lists/StatusList.js +55 -0
  67. package/src/react/components/lists/WalletList.js +54 -0
  68. package/src/react/components/radio/Spotify.js +144 -0
  69. package/src/react/components/radio/Spotify.styles.js +32 -0
  70. package/src/react/components/radio/YouTube.js +82 -0
  71. package/src/react/components/radio/YouTube.styles.js +16 -0
  72. package/src/react/components/toastConfig.js +16 -0
  73. package/src/react/config/deviceConfigBootstrap.js +571 -0
  74. package/src/react/index.js +4 -0
  75. package/src/react/localStorage.js +30 -0
  76. package/src/react/pages/EntityLogPage.js +124 -0
  77. package/src/react/pages/EntityLogPage.styles.js +17 -0
  78. package/src/react/pages/GenericLogPage.js +760 -0
  79. package/src/react/pages/GenericLogPage.styles.js +401 -0
  80. package/src/react/pages/Imports.js +361 -0
  81. package/src/react/pages/Imports.styles.js +223 -0
  82. package/src/react/pages/SettingsPage/PaymentTypesByWalletTab.js +462 -0
  83. package/src/react/pages/SettingsPage/index.js +1171 -0
  84. package/src/react/pages/SettingsPage/index.styles.js +122 -0
  85. package/src/react/print/jobs.js +147 -0
  86. package/src/react/print/providers/local.js +53 -0
  87. package/src/react/print/providers/remote.js +81 -0
  88. package/src/react/print/selection.js +296 -0
  89. package/src/react/print/usePrintButtonController.js +474 -0
  90. package/src/react/router/routes.js +42 -0
  91. package/src/react/services/NetworkPrinterService.native.js +391 -0
  92. package/src/react/services/NetworkPrinterService.web.js +15 -0
  93. package/src/react/stores/index.js +128 -0
  94. package/src/react/stores/storeErrorBridge.js +36 -0
  95. package/src/react/utils/backgroundRuntimeRegistration.js +69 -0
  96. package/src/react/utils/cashPayment.js +87 -0
  97. package/src/react/utils/commercialDocumentOrders.js +269 -0
  98. package/src/react/utils/dateRangeFilter.js +264 -0
  99. package/src/react/utils/deviceRuntime.js +193 -0
  100. package/src/react/utils/entityDisplay.js +231 -0
  101. package/src/react/utils/entityLog.js +649 -0
  102. package/src/react/utils/fileUrl.js +102 -0
  103. package/src/react/utils/frontendDebugLog.js +45 -0
  104. package/src/react/utils/googleMapsConfig.js +49 -0
  105. package/src/react/utils/importStatus.js +100 -0
  106. package/src/react/utils/invoiceDueDateMessages.js +14 -0
  107. package/src/react/utils/invoicePresentation.js +46 -0
  108. package/src/react/utils/logSettings.js +143 -0
  109. package/src/react/utils/maintenanceSettings.js +53 -0
  110. package/src/react/utils/managerOrderNotifications.js +523 -0
  111. package/src/react/utils/mapNavigation.js +76 -0
  112. package/src/react/utils/menuCatalogConfig.js +8 -0
  113. package/src/react/utils/menuCatalogDownload.js +157 -0
  114. package/src/react/utils/networkCameraDevices.js +175 -0
  115. package/src/react/utils/networkDeviceProfiles.js +135 -0
  116. package/src/react/utils/normalizedCatalogDownload.js +157 -0
  117. package/src/react/utils/notificationNavigation.js +94 -0
  118. package/src/react/utils/notificationSound.js +42 -0
  119. package/src/react/utils/paymentDevices.js +397 -0
  120. package/src/react/utils/paymentGatewayExecution.js +91 -0
  121. package/src/react/utils/paymentOptions.js +88 -0
  122. package/src/react/utils/peopleDisplay.js +39 -0
  123. package/src/react/utils/printerDevices.js +465 -0
  124. package/src/react/utils/remotePayment.js +61 -0
  125. package/src/react/utils/runtimeFooter.js +475 -0
  126. package/src/react/utils/runtimeLanguage.js +50 -0
  127. package/src/react/utils/runtimeMenu.js +224 -0
  128. package/src/react/utils/screenMetrics.js +30 -0
  129. package/src/react/utils/shopConfig.js +294 -0
  130. package/src/react/utils/shopFranchises.js +114 -0
  131. package/src/react/utils/socketRuntimePipeline.js +454 -0
  132. package/src/react/utils/spoolAckTracker.js +77 -0
  133. package/src/react/utils/storeColumns.js +427 -0
  134. package/src/react/utils/systemErrorChannel.js +109 -0
  135. package/src/react/utils/systemErrorMessage.js +67 -0
  136. package/src/react/utils/toastPresentation.js +53 -0
  137. package/src/react/utils/websocketSharedRuntime.js +826 -0
  138. package/src/store/acl/getters.js +4 -0
  139. package/src/store/acl/index.js +19 -0
  140. package/src/store/acl/mutation_types.js +4 -0
  141. package/src/store/acl/mutations.js +17 -0
  142. package/src/store/address/city.js +22 -0
  143. package/src/store/categories/category_file/index.js +25 -0
  144. package/src/store/categories/index.js +93 -0
  145. package/src/store/configs/customActions.js +131 -0
  146. package/src/store/configs/index.js +25 -0
  147. package/src/store/connections/customActions.js +26 -0
  148. package/src/store/connections/index.js +124 -0
  149. package/src/store/device/index.js +22 -0
  150. package/src/store/device_config/customActions.js +190 -0
  151. package/src/store/device_config/index.js +28 -0
  152. package/src/store/entity_log/customActions.js +63 -0
  153. package/src/store/entity_log/index.js +25 -0
  154. package/src/store/extra/data.js +85 -0
  155. package/src/store/extra/fields.js +88 -0
  156. package/src/store/file/actions.js +27 -0
  157. package/src/store/file/index.js +24 -0
  158. package/src/store/imports/index.js +88 -0
  159. package/src/store/print/customActions.js +251 -0
  160. package/src/store/print/index.js +28 -0
  161. package/src/store/printer/customActions.js +54 -0
  162. package/src/store/printer/index.js +26 -0
  163. package/src/store/runtime_debug/customActions.js +71 -0
  164. package/src/store/runtime_debug/index.js +30 -0
  165. package/src/store/status/index.js +65 -0
  166. package/src/store/timezones/index.js +43 -0
  167. package/src/store/websocket/customActions.js +24 -0
  168. package/src/store/websocket/index.js +28 -0
  169. package/src/tests/react/components/LauncherModeBridge.test.js +93 -0
  170. package/src/tests/react/config/deviceConfigBootstrap.test.js +337 -0
  171. package/src/tests/react/print/localPrintProvider.test.js +45 -0
  172. package/src/tests/react/utils/backgroundRuntimeRegistration.test.js +123 -0
  173. package/src/tests/react/utils/cashPayment.test.js +41 -0
  174. package/src/tests/react/utils/dateRangeFilter.test.mjs +29 -0
  175. package/src/tests/react/utils/entityDisplay.test.js +18 -0
  176. package/src/tests/react/utils/googleMapsConfig.test.js +32 -0
  177. package/src/tests/react/utils/importStatus.test.js +85 -0
  178. package/src/tests/react/utils/invoicePresentation.test.mjs +40 -0
  179. package/src/tests/react/utils/managerOrderNotifications.test.js +127 -0
  180. package/src/tests/react/utils/mapNavigation.test.js +58 -0
  181. package/src/tests/react/utils/notificationNavigation.test.js +39 -0
  182. package/src/tests/react/utils/notificationSound.test.js +27 -0
  183. package/src/tests/react/utils/paymentDevices.test.js +88 -0
  184. package/src/tests/react/utils/runtimeFooter.test.js +429 -0
  185. package/src/tests/react/utils/runtimeLanguage.test.js +80 -0
  186. package/src/tests/react/utils/runtimeMenu.test.js +174 -0
  187. package/src/tests/react/utils/shopFranchises.test.js +82 -0
  188. package/src/tests/react/utils/socketRuntimePipeline.test.js +124 -0
  189. package/src/tests/react/utils/spoolAckTracker.test.js +40 -0
  190. package/src/tests/react/utils/storeErrorBridge.test.js +66 -0
  191. package/src/tests/react/utils/systemErrorChannel.test.js +58 -0
  192. package/src/tests/react/utils/systemErrorMessage.test.js +59 -0
  193. package/src/tests/react/utils/toastPresentation.test.js +75 -0
  194. package/src/tests/react/utils/translate.test.mjs +396 -0
  195. package/src/tests/react/utils/websocketSharedRuntime.test.js +245 -0
  196. package/src/utils/apiEntryPoint.js +31 -0
  197. package/src/utils/appDomain.js +50 -0
  198. package/src/utils/config.js +14 -0
  199. package/src/utils/formatter.js +419 -0
  200. package/src/utils/integrationConfigs.js +79 -0
  201. package/src/utils/methods.js +23 -0
  202. package/src/utils/oauth.js +42 -0
  203. package/src/utils/translate.js +620 -0
@@ -0,0 +1,572 @@
1
+ import React, { useState, useEffect, useImperativeHandle, forwardRef, useMemo } from 'react';
2
+
3
+ import {
4
+ View,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ ScrollView,
9
+ FlatList,
10
+ Modal,
11
+ } from 'react-native';
12
+
13
+ import { MaterialCommunityIcons } from '@expo/vector-icons';
14
+ import { useStore } from '@store';
15
+ import styles from './CategoryForm.styles';
16
+ import { inlineStyle_240_75 } from './CategoryForm.styles';
17
+
18
+ /* ─── Paleta de cores predefinidas ─── */
19
+ const PRESET_COLORS = [
20
+ '#c10015', '#F97316', '#e67e22', '#84CC16', '#10b981',
21
+ '#14B8A6', '#06B6D4', '#3B82F6', '#6366F1', '#8B5CF6',
22
+ '#EC4899', '#F43F5E', '#64748B', '#0F172A', '#FFFFFF',
23
+ '#FCA5A5', '#FED7AA', '#FEF08A', '#BBF7D0', '#A7F3D0',
24
+ '#99F6E4', '#BAE6FD', '#BFDBFE', '#C7D2FE', '#DDD6FE',
25
+ '#FBCFE8', '#FDA4AF', '#CBD5E1', '#475569', '#1E293B',
26
+ ];
27
+
28
+ /* ─── Ícones curados para categorias ─── */
29
+ const CATEGORY_ICONS = [
30
+ { name: 'food', label: 'Comida' },
31
+ { name: 'food-variant', label: 'Prato' },
32
+ { name: 'silverware-fork-knife', label: 'Talheres' },
33
+ { name: 'pizza', label: 'Pizza' },
34
+ { name: 'hamburger', label: 'Hambúrguer' },
35
+ { name: 'noodles', label: 'Massa' },
36
+ { name: 'rice', label: 'Arroz' },
37
+ { name: 'bread-slice', label: 'Pão' },
38
+ { name: 'fish', label: 'Peixe' },
39
+ { name: 'egg-fried', label: 'Ovo' },
40
+ { name: 'cheese', label: 'Queijo' },
41
+ { name: 'sausage', label: 'Linguiça' },
42
+ { name: 'corn', label: 'Milho' },
43
+ { name: 'carrot', label: 'Cenoura' },
44
+ { name: 'chili-mild', label: 'Tempero' },
45
+ { name: 'shaker', label: 'Molho' },
46
+ { name: 'leaf', label: 'Vegetal' },
47
+ { name: 'sprout', label: 'Salada' },
48
+ { name: 'mushroom', label: 'Cogumelo' },
49
+ { name: 'fruit-watermelon', label: 'Fruta' },
50
+ { name: 'fruit-cherries', label: 'Frutas' },
51
+ { name: 'pot-steam', label: 'Panela' },
52
+ { name: 'fire', label: 'Churrasco' },
53
+ { name: 'grill', label: 'Grill' },
54
+ { name: 'coffee', label: 'Café' },
55
+ { name: 'tea', label: 'Chá' },
56
+ { name: 'cup', label: 'Xícara' },
57
+ { name: 'bottle-soda', label: 'Refrigerante' },
58
+ { name: 'beer', label: 'Cerveja' },
59
+ { name: 'glass-wine', label: 'Vinho' },
60
+ { name: 'glass-cocktail', label: 'Drinque' },
61
+ { name: 'water', label: 'Água' },
62
+ { name: 'blender', label: 'Vitamina' },
63
+ { name: 'ice-cream', label: 'Sorvete' },
64
+ { name: 'candy', label: 'Doce' },
65
+ { name: 'cake-variant', label: 'Bolo' },
66
+ { name: 'cookie', label: 'Biscoito' },
67
+ { name: 'cupcake', label: 'Cupcake' },
68
+ { name: 'silverware-spoon', label: 'Sobremesa' },
69
+ { name: 'nutrition', label: 'Nutrição' },
70
+ { name: 'bottle-tonic', label: 'Suplemento' },
71
+ { name: 'weight', label: 'Fitness' },
72
+ { name: 'dumbbell', label: 'Academia' },
73
+ { name: 'storefront-outline', label: 'Loja' },
74
+ { name: 'shopping-outline', label: 'Compras' },
75
+ { name: 'tag-outline', label: 'Promoção' },
76
+ { name: 'star-outline', label: 'Destaque' },
77
+ { name: 'heart-outline', label: 'Favorito' },
78
+ { name: 'package-variant-closed', label: 'Pacote' },
79
+ { name: 'bicycle', label: 'Delivery' },
80
+ { name: 'motorbike', label: 'Moto' },
81
+ { name: 'percent', label: 'Desconto' },
82
+ { name: 'new-box', label: 'Novidade' },
83
+ { name: 'flask-outline', label: 'Especial' },
84
+ { name: 'spa-outline', label: 'Bem-estar' },
85
+ { name: 'baby-bottle-outline', label: 'Bebê' },
86
+ { name: 'bowl-mix', label: 'Tigela' },
87
+ { name: 'french-fries', label: 'Batata' },
88
+ { name: 'taco', label: 'Taco' },
89
+ { name: 'food-apple', label: 'Maçã' },
90
+ { name: 'weather-sunny', label: 'Dia' },
91
+ { name: 'moon-waning-crescent', label: 'Noite' },
92
+ ];
93
+
94
+ /* ─── Canais disponíveis ─── */
95
+ const CHANNELS = [
96
+ { key: 'default', label: 'Balcão', icon: 'storefront-outline' },
97
+ { key: 'totem', label: 'Totem', icon: 'monitor' },
98
+ { key: 'delivery', label: 'Delivery', icon: 'bicycle' },
99
+ { key: 'ifood', label: 'iFood', icon: 'food-fork-drink' },
100
+ { key: '99food', label: '99Food', icon: 'food-variant' },
101
+ { key: 'whatsapp', label: 'WhatsApp', icon: 'whatsapp' },
102
+ { key: 'instagram', label: 'Instagram', icon: 'instagram' },
103
+ { key: 'keeta', label: 'Keeta', icon: 'motorbike' },
104
+ { key: 'messenger', label: 'Messenger', icon: 'facebook-messenger' },
105
+ ];
106
+
107
+ /* ─── Modal genérico bottom-sheet ─── */
108
+ const BottomModal = ({ visible, onClose, title, children }) => (
109
+ <Modal
110
+ visible={visible}
111
+ transparent
112
+ animationType="slide"
113
+ onRequestClose={onClose}
114
+ >
115
+ <TouchableOpacity
116
+ style={styles.modalOverlay}
117
+ activeOpacity={1}
118
+ onPress={onClose}
119
+ />
120
+ <View style={styles.modalSheet}>
121
+ <View style={styles.modalHandle} />
122
+ <View style={styles.modalHeader}>
123
+ <Text style={styles.modalTitle}>{title}</Text>
124
+ <TouchableOpacity onPress={onClose} style={styles.modalClose}>
125
+ <MaterialCommunityIcons name="close" size={20} color="#64748B" />
126
+ </TouchableOpacity>
127
+ </View>
128
+ {children}
129
+ </View>
130
+ </Modal>
131
+ );
132
+
133
+ /* ─── Seletor de cor ─── */
134
+ const ColorPicker = ({ value, onChange }) => {
135
+ const [open, setOpen] = useState(false);
136
+ const [hexInput, setHexInput] = useState(value || '#000000');
137
+
138
+ useEffect(() => { setHexInput(value || '#000000'); }, [value]);
139
+
140
+ const applyHex = () => {
141
+ const v = hexInput.startsWith('#') ? hexInput : `#${hexInput}`;
142
+ if (/^#[0-9A-Fa-f]{6}$/.test(v)) {
143
+ onChange(v);
144
+ setOpen(false);
145
+ }
146
+ };
147
+
148
+ return (
149
+ <>
150
+ <TouchableOpacity
151
+ style={styles.colorButton}
152
+ onPress={() => setOpen(true)}
153
+ activeOpacity={0.7}
154
+ >
155
+ <View style={[styles.colorSwatch, { backgroundColor: value || '#000000' }]} />
156
+ <Text style={styles.colorButtonText}>{value || '#000000'}</Text>
157
+ <MaterialCommunityIcons name="palette-outline" size={18} color="#94A3B8" />
158
+ </TouchableOpacity>
159
+
160
+ <BottomModal visible={open} onClose={() => setOpen(false)} title="Escolher cor">
161
+ <ScrollView contentContainerStyle={styles.colorGrid} showsVerticalScrollIndicator={false}>
162
+ {PRESET_COLORS.map(c => {
163
+ const selected = (value || '').toLowerCase() === c.toLowerCase();
164
+ return (
165
+ <TouchableOpacity
166
+ key={c}
167
+ onPress={() => { onChange(c); setOpen(false); }}
168
+ activeOpacity={0.75}
169
+ style={styles.colorCell}
170
+ >
171
+ <View style={[
172
+ styles.colorCircle,
173
+ { backgroundColor: c },
174
+ c === '#FFFFFF' && styles.colorCircleWhite,
175
+ selected && styles.colorCircleSelected,
176
+ ]}>
177
+ {selected && (
178
+ <MaterialCommunityIcons
179
+ name="check"
180
+ size={16}
181
+ color={c === '#FFFFFF' || c === '#FEF08A' || c === '#BBF7D0' || c === '#FED7AA' ? '#0F172A' : '#fff'}
182
+ />
183
+ )}
184
+ </View>
185
+ </TouchableOpacity>
186
+ );
187
+ })}
188
+ </ScrollView>
189
+
190
+ <View style={styles.hexInputRow}>
191
+ <View style={[styles.colorSwatch, { backgroundColor: hexInput.startsWith('#') && hexInput.length >= 7 ? hexInput : '#ccc' }]} />
192
+ <TextInput
193
+ value={hexInput}
194
+ onChangeText={setHexInput}
195
+ style={styles.hexInput}
196
+ placeholder="#000000"
197
+ placeholderTextColor="#CBD5E1"
198
+ maxLength={7}
199
+ autoCapitalize="characters"
200
+ />
201
+ <TouchableOpacity style={styles.hexApplyBtn} onPress={applyHex} activeOpacity={0.8}>
202
+ <Text style={styles.hexApplyText}>OK</Text>
203
+ </TouchableOpacity>
204
+ </View>
205
+ </BottomModal>
206
+ </>
207
+ );
208
+ };
209
+
210
+ /* ─── Seletor de ícone ─── */
211
+ const IconPicker = ({ value, onChange }) => {
212
+ const [open, setOpen] = useState(false);
213
+ const [search, setSearch] = useState('');
214
+
215
+ const filtered = useMemo(() => {
216
+ const q = search.toLowerCase().trim();
217
+ if (!q) return CATEGORY_ICONS;
218
+ return CATEGORY_ICONS.filter(
219
+ ic => ic.name.includes(q) || ic.label.toLowerCase().includes(q),
220
+ );
221
+ }, [search]);
222
+
223
+ return (
224
+ <>
225
+ <TouchableOpacity
226
+ style={styles.iconButton}
227
+ onPress={() => setOpen(true)}
228
+ activeOpacity={0.7}
229
+ >
230
+ {value ? (
231
+ <MaterialCommunityIcons name={value} size={22} color="#0F172A" />
232
+ ) : (
233
+ <MaterialCommunityIcons name="image-outline" size={22} color="#CBD5E1" />
234
+ )}
235
+ <Text style={[styles.iconButtonText, !value && { color: '#CBD5E1' }]}>
236
+ {value || 'Selecionar ícone'}
237
+ </Text>
238
+ <MaterialCommunityIcons name="chevron-down" size={18} color="#94A3B8" />
239
+ </TouchableOpacity>
240
+ <BottomModal visible={open} onClose={() => { setOpen(false); setSearch(''); }} title="Selecionar ícone">
241
+ <View style={styles.iconSearchWrap}>
242
+ <MaterialCommunityIcons name="magnify" size={18} color="#94A3B8" style={inlineStyle_240_75} />
243
+ <TextInput
244
+ value={search}
245
+ onChangeText={setSearch}
246
+ placeholder="Pesquisar..."
247
+ placeholderTextColor="#CBD5E1"
248
+ style={styles.iconSearchInput}
249
+ />
250
+ {!!search && (
251
+ <TouchableOpacity onPress={() => setSearch('')}>
252
+ <MaterialCommunityIcons name="close-circle" size={16} color="#94A3B8" />
253
+ </TouchableOpacity>
254
+ )}
255
+ </View>
256
+
257
+ {/* Opção "Nenhum" */}
258
+ <TouchableOpacity
259
+ style={styles.iconNoneRow}
260
+ onPress={() => { onChange(''); setOpen(false); setSearch(''); }}
261
+ activeOpacity={0.7}
262
+ >
263
+ <View style={styles.iconNoneCircle}>
264
+ <MaterialCommunityIcons name="close" size={16} color="#94A3B8" />
265
+ </View>
266
+ <Text style={styles.iconNoneText}>Nenhum ícone</Text>
267
+ {!value && <MaterialCommunityIcons name="check-circle" size={18} color="#10b981" />}
268
+ </TouchableOpacity>
269
+
270
+ <FlatList
271
+ data={filtered}
272
+ keyExtractor={item => item.name}
273
+ numColumns={5}
274
+ style={styles.iconList}
275
+ contentContainerStyle={styles.iconGrid}
276
+ showsVerticalScrollIndicator={false}
277
+ keyboardShouldPersistTaps="handled"
278
+ renderItem={({ item }) => {
279
+ const selected = value === item.name;
280
+ return (
281
+ <TouchableOpacity
282
+ style={[styles.iconCell, selected && styles.iconCellSelected]}
283
+ onPress={() => { onChange(item.name); setOpen(false); setSearch(''); }}
284
+ activeOpacity={0.7}
285
+ >
286
+ <MaterialCommunityIcons
287
+ name={item.name}
288
+ size={24}
289
+ color={selected ? '#3B82F6' : '#334155'}
290
+ />
291
+ <Text style={[styles.iconCellLabel, selected && styles.iconCellLabelSelected]} numberOfLines={1}>
292
+ {item.label}
293
+ </Text>
294
+ </TouchableOpacity>
295
+ );
296
+ }}
297
+ />
298
+ </BottomModal>
299
+ </>
300
+ );
301
+ };
302
+
303
+ /* ─── Seletor de categoria pai ─── */
304
+ const ParentCategoryPicker = ({ value, onChange, categories, currentId }) => {
305
+ const [open, setOpen] = useState(false);
306
+ const [search, setSearch] = useState('');
307
+
308
+ const available = useMemo(() => {
309
+ const q = search.toLowerCase().trim();
310
+ return (categories || []).filter(c => {
311
+ if (String(c.id) === String(currentId)) return false;
312
+ if (!q) return true;
313
+ return String(c.name || '').toLowerCase().includes(q);
314
+ });
315
+ }, [categories, currentId, search]);
316
+
317
+ const selected = (categories || []).find(c => String(c.id) === String(value));
318
+
319
+ return (
320
+ <>
321
+ <TouchableOpacity
322
+ style={styles.iconButton}
323
+ onPress={() => setOpen(true)}
324
+ activeOpacity={0.7}
325
+ >
326
+ {selected?.color && (
327
+ <View style={[styles.parentColorDot, { backgroundColor: selected.color }]} />
328
+ )}
329
+ <Text style={[styles.iconButtonText, !selected && styles.emptyPickerText]}>
330
+ {selected ? selected.name : 'Nenhuma'}
331
+ </Text>
332
+ <MaterialCommunityIcons name="chevron-down" size={18} color="#94A3B8" />
333
+ </TouchableOpacity>
334
+
335
+ <BottomModal visible={open} onClose={() => { setOpen(false); setSearch(''); }} title="Categoria pai">
336
+ <View style={styles.iconSearchWrap}>
337
+ <MaterialCommunityIcons name="magnify" size={18} color="#94A3B8" style={styles.searchIcon} />
338
+ <TextInput
339
+ value={search}
340
+ onChangeText={setSearch}
341
+ placeholder="Pesquisar categoria..."
342
+ placeholderTextColor="#CBD5E1"
343
+ style={styles.iconSearchInput}
344
+ />
345
+ {!!search && (
346
+ <TouchableOpacity onPress={() => setSearch('')}>
347
+ <MaterialCommunityIcons name="close-circle" size={16} color="#94A3B8" />
348
+ </TouchableOpacity>
349
+ )}
350
+ </View>
351
+
352
+ <ScrollView style={styles.parentSearchList} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
353
+ {/* Opção nenhuma */}
354
+ <TouchableOpacity
355
+ style={styles.parentRow}
356
+ onPress={() => { onChange(null); setOpen(false); setSearch(''); }}
357
+ activeOpacity={0.7}
358
+ >
359
+ <MaterialCommunityIcons name="close-circle-outline" size={18} color="#94A3B8" style={styles.parentLeadingIcon} />
360
+ <Text style={[styles.parentRowText, styles.parentRowTextMuted]}>Nenhuma</Text>
361
+ {!value && <MaterialCommunityIcons name="check-circle" size={18} color="#10b981" />}
362
+ </TouchableOpacity>
363
+
364
+ {available.map(cat => {
365
+ const isSelected = String(cat.id) === String(value);
366
+ return (
367
+ <TouchableOpacity
368
+ key={cat.id}
369
+ style={styles.parentRow}
370
+ onPress={() => { onChange(cat.id); setOpen(false); setSearch(''); }}
371
+ activeOpacity={0.7}
372
+ >
373
+ <View style={[styles.parentColorDot, styles.parentColorDotSpacing, { backgroundColor: cat.color || '#CBD5E1' }]} />
374
+ {!!cat.icon && (
375
+ <MaterialCommunityIcons name={cat.icon} size={16} color="#475569" style={styles.parentIconSpacing} />
376
+ )}
377
+ <Text style={[styles.parentRowText, isSelected && styles.parentRowTextSelected]} numberOfLines={1}>
378
+ {cat.name}
379
+ </Text>
380
+ {isSelected && <MaterialCommunityIcons name="check-circle" size={18} color="#3B82F6" style={styles.parentSelectedIcon} />}
381
+ </TouchableOpacity>
382
+ );
383
+ })}
384
+
385
+ {available.length === 0 && (
386
+ <View style={styles.parentEmpty}>
387
+ <Text style={styles.parentEmptyText}>Nenhuma categoria encontrada</Text>
388
+ </View>
389
+ )}
390
+ </ScrollView>
391
+ </BottomModal>
392
+ </>
393
+ );
394
+ };
395
+
396
+ /* ─── Seletor de canais (multi-toggle) ─── */
397
+ const ChannelPicker = ({ value, onChange }) => {
398
+ const selected = useMemo(() => {
399
+ if (!value) return ['default'];
400
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
401
+ }, [value]);
402
+
403
+ const toggle = key => {
404
+ let next = selected.includes(key)
405
+ ? selected.filter(k => k !== key)
406
+ : [...selected, key];
407
+ if (next.length === 0) next = ['default'];
408
+ onChange(next.join(','));
409
+ };
410
+
411
+ return (
412
+ <View style={styles.channelWrap}>
413
+ {CHANNELS.map(ch => {
414
+ const active = selected.includes(ch.key);
415
+ return (
416
+ <TouchableOpacity
417
+ key={ch.key}
418
+ style={[styles.channelChip, active && styles.channelChipActive]}
419
+ onPress={() => toggle(ch.key)}
420
+ activeOpacity={0.75}
421
+ >
422
+ <MaterialCommunityIcons
423
+ name={ch.icon}
424
+ size={14}
425
+ color={active ? '#fff' : '#64748B'}
426
+ style={styles.channelIcon}
427
+ />
428
+ <Text style={[styles.channelChipText, active && styles.channelChipTextActive]}>
429
+ {ch.label}
430
+ </Text>
431
+ </TouchableOpacity>
432
+ );
433
+ })}
434
+ </View>
435
+ );
436
+ };
437
+
438
+ const normalizeCategoryContext = context =>
439
+ String(context || 'products').trim().toLowerCase() || 'products';
440
+
441
+ /* ─── Formulário principal ─── */
442
+ const CategoryForm = forwardRef(({ category, context = 'products', onClose, onSaved }, ref) => {
443
+ const categoriesStore = useStore('categories');
444
+ const categoryActions = categoriesStore.actions;
445
+ const categories = categoriesStore.getters.items;
446
+ const peopleStore = useStore('people');
447
+ const { currentCompany } = peopleStore.getters;
448
+ const categoryContext = normalizeCategoryContext(context);
449
+
450
+ const [name, setName] = useState('');
451
+ const [color, setColor] = useState('#CBD5E1');
452
+ const [icon, setIcon] = useState('');
453
+ const [parent, setParent] = useState(null);
454
+ const [sortOrder, setSortOrder] = useState('');
455
+ const [channel, setChannel] = useState('default');
456
+
457
+ const companyIri = currentCompany?.['@id']
458
+ ? String(currentCompany['@id'])
459
+ : currentCompany?.id
460
+ ? `/people/${String(currentCompany.id).replace(/\D/g, '')}`
461
+ : null;
462
+
463
+ useEffect(() => {
464
+ if (category) {
465
+ setName(category.name || '');
466
+ setColor(category.color || '#CBD5E1');
467
+ setIcon(category.icon || '');
468
+ setParent(category.parent?.id || null);
469
+ setSortOrder(String(category?.extraData?.sortOrder || ''));
470
+ setChannel(String(category?.extraData?.channel || 'default'));
471
+ } else {
472
+ setName('');
473
+ setColor('#CBD5E1');
474
+ setIcon('');
475
+ setParent(null);
476
+ setSortOrder('');
477
+ setChannel('default');
478
+ }
479
+ }, [category]);
480
+
481
+ const handleSubmit = async () => {
482
+ const payload = {
483
+ ...(category?.id ? { id: category.id } : {}),
484
+ name,
485
+ color,
486
+ icon: icon || '',
487
+ context: categoryContext,
488
+ company: companyIri,
489
+ parent: parent ? `/categories/${parent}` : null,
490
+ extraData: {
491
+ ...(category?.extraData || {}),
492
+ sortOrder: sortOrder ? parseInt(String(sortOrder).replace(/\D/g, ''), 10) || 0 : 0,
493
+ channel: channel || 'default',
494
+ },
495
+ };
496
+
497
+ const saved = await categoryActions.save(payload);
498
+
499
+ await categoryActions.getItems({
500
+ context: categoryContext,
501
+ 'order[name]': 'ASC',
502
+ company: currentCompany.id,
503
+ });
504
+
505
+ if (onSaved) onSaved(saved);
506
+ onClose();
507
+ };
508
+
509
+ useImperativeHandle(ref, () => ({ submit: handleSubmit }));
510
+
511
+ return (
512
+ <View style={styles.form}>
513
+
514
+ {/* Nome */}
515
+ <View style={styles.field}>
516
+ <Text style={styles.label}>Nome</Text>
517
+ <TextInput
518
+ value={name}
519
+ onChangeText={setName}
520
+ style={styles.input}
521
+ placeholder="Ex: Entradas, Bebidas, Sobremesas..."
522
+ placeholderTextColor="#CBD5E1"
523
+ />
524
+ </View>
525
+
526
+ {/* Cor */}
527
+ <View style={styles.field}>
528
+ <Text style={styles.label}>Cor</Text>
529
+ <ColorPicker value={color} onChange={setColor} />
530
+ </View>
531
+
532
+ {/* Ícone */}
533
+ <View style={styles.field}>
534
+ <Text style={styles.label}>Ícone</Text>
535
+ <IconPicker value={icon} onChange={setIcon} />
536
+ </View>
537
+
538
+ {/* Categoria pai */}
539
+ <View style={styles.field}>
540
+ <Text style={styles.label}>Categoria pai</Text>
541
+ <ParentCategoryPicker
542
+ value={parent}
543
+ onChange={setParent}
544
+ categories={categories}
545
+ currentId={category?.id}
546
+ />
547
+ </View>
548
+
549
+ {/* Ordem */}
550
+ <View style={styles.field}>
551
+ <Text style={styles.label}>Ordem no cardápio</Text>
552
+ <TextInput
553
+ value={sortOrder}
554
+ onChangeText={setSortOrder}
555
+ keyboardType="numeric"
556
+ style={styles.input}
557
+ placeholder="0"
558
+ placeholderTextColor="#CBD5E1"
559
+ />
560
+ </View>
561
+
562
+ {/* Canais */}
563
+ <View style={styles.field}>
564
+ <Text style={styles.label}>Canais de venda</Text>
565
+ <ChannelPicker value={channel} onChange={setChannel} />
566
+ </View>
567
+
568
+ </View>
569
+ );
570
+ });
571
+
572
+ export default CategoryForm;