@controleonline/ui-default 1.0.270 → 1.0.273

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 (100) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/.github/workflows/publish-npm.yml +91 -0
  4. package/AGENTS.md +46 -50
  5. package/docs/technical/Contrato-de-colunas.md +67 -0
  6. package/docs/technical/Default-Components.md +76 -0
  7. package/docs/technical/DefaultForm.md +80 -0
  8. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  9. package/docs/technical/DefaultTable.md +272 -0
  10. package/docs/wiki.md +1 -0
  11. package/package.json +4 -3
  12. package/scripts/validate-wiki-pointer.cjs +13 -0
  13. package/src/react/components/address/DefaultAddress.js +446 -0
  14. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  15. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  16. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  17. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  18. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  19. package/src/react/components/address/defaultAddressStyles.js +148 -0
  20. package/src/react/components/address/defaultAddressUtils.js +129 -0
  21. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  22. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  23. package/src/react/components/files/DefaultFile.js +231 -46
  24. package/src/react/components/files/DefaultFileColumn.js +123 -0
  25. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  26. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  27. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  28. package/src/react/components/filters/DefaultColumnFilter.js +60 -48
  29. package/src/react/components/filters/DefaultExternalFilters.js +324 -265
  30. package/src/react/components/filters/filterValue.js +65 -0
  31. package/src/react/components/form/DefaultForm.js +99 -79
  32. package/src/react/components/help/DefaultTooltip.js +48 -12
  33. package/src/react/components/help/DefaultTooltip.styles.js +20 -0
  34. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  35. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  36. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  37. package/src/react/components/inputs/DefaultInput.js +296 -141
  38. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  39. package/src/react/components/inputs/DefaultSelect.js +134 -123
  40. package/src/react/components/inputs/defaultInputUtils.js +262 -213
  41. package/src/react/components/map/DefaultMap.native.js +73 -73
  42. package/src/react/components/map/DefaultMap.shared.js +911 -762
  43. package/src/react/components/map/DefaultMap.web.js +124 -68
  44. package/src/react/components/table/DefaultColumnMenu.js +22 -3
  45. package/src/react/components/table/DefaultDebug.js +44 -37
  46. package/src/react/components/table/DefaultSearchModal.js +9 -1
  47. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  48. package/src/react/components/table/DefaultTable.js +217 -111
  49. package/src/react/components/table/DefaultTable.styles.js +369 -310
  50. package/src/react/components/table/DefaultTable.utils.js +38 -4
  51. package/src/react/components/table/DefaultTableBody.js +13 -8
  52. package/src/react/components/table/DefaultTableCards.js +20 -1
  53. package/src/react/components/table/DefaultTableControls.js +180 -205
  54. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  55. package/src/react/components/table/DefaultTableInput.js +1 -0
  56. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  57. package/src/react/components/table/DefaultTableRows.js +357 -325
  58. package/src/react/components/table/DefaultTableSearch.js +28 -5
  59. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  60. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  61. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  62. package/src/react/components/upload/DefaultUpload.js +406 -0
  63. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  64. package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
  65. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  66. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  67. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  68. package/src/react/components/upload/fileUpload.js +93 -0
  69. package/src/react/hooks/usePostalCodeLookup.js +159 -0
  70. package/src/react/index.js +6 -0
  71. package/src/react/services/addressFormUtils.js +262 -0
  72. package/src/react/services/addressGeo.js +230 -0
  73. package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
  74. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  75. package/src/tests/react/components/files/DefaultFile.test.js +22 -6
  76. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  77. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  78. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  79. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  80. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  81. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  82. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  83. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  84. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  85. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  86. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  87. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  88. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  89. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
  90. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  91. package/src/tests/react/filters/filterValue.test.js +46 -0
  92. package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
  93. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  94. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  95. package/src/tests/react/services/addressFormUtils.test.js +179 -0
  96. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  97. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  98. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  99. package/src/tests/react/services/smoke-637/index.html +132 -0
  100. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
@@ -0,0 +1,446 @@
1
+ import React, {useCallback, useEffect, useMemo, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Image,
5
+ ScrollView,
6
+ Text,
7
+ TextInput,
8
+ TouchableOpacity,
9
+ View,
10
+ useWindowDimensions,
11
+ } from 'react-native';
12
+ import {
13
+ buildAddressSavePayload,
14
+ listCountries,
15
+ listStates,
16
+ } from '../../services/addressGeo';
17
+ import {
18
+ buildMapMarkerPayload,
19
+ getCurrentCoordinates,
20
+ GEOCODE_MISS_MESSAGE,
21
+ hasCoordinates,
22
+ hydrateAddressFromRow,
23
+ mergePostalCodeData,
24
+ onlyDigits,
25
+ parseOptionalCoordinate,
26
+ } from '../../services/addressFormUtils';
27
+ import usePostalCodeLookup from '../../hooks/usePostalCodeLookup';
28
+ import DefaultMap from '../map/DefaultMap';
29
+ import LatLonReadonlyFields from './LatLonReadonlyFields';
30
+ import styles from './DefaultAddress.styles';
31
+
32
+ export default function DefaultAddress({
33
+ row = null,
34
+ peopleIri,
35
+ mode = 'create',
36
+ onCancel,
37
+ onSaved,
38
+ saveAction,
39
+ onFormChange = null,
40
+ hideActions = false,
41
+ submitLabel = 'Salvar',
42
+ }) {
43
+ const {width} = useWindowDimensions();
44
+ const isDesktop = width >= 900;
45
+ const [form, setForm] = useState(() => hydrateAddressFromRow(row));
46
+ const [countries, setCountries] = useState([]);
47
+ const [states, setStates] = useState([]);
48
+ const [loadingMeta, setLoadingMeta] = useState(true);
49
+ const [saving, setSaving] = useState(false);
50
+ const [error, setError] = useState(null);
51
+ const [showCountryList, setShowCountryList] = useState(false);
52
+ const [showStateList, setShowStateList] = useState(false);
53
+ // Once the user types lat/lon, keep both fields editable (avoid lock when both become finite).
54
+ const [coordsManualEdit, setCoordsManualEdit] = useState(false);
55
+
56
+ const {
57
+ loadingCep,
58
+ cepError,
59
+ setCepError,
60
+ geocodeMiss,
61
+ setGeocodeMiss,
62
+ onCepBlur,
63
+ runLookup,
64
+ cancelPending,
65
+ } = usePostalCodeLookup({
66
+ formCep: form.cep,
67
+ setForm,
68
+ onFormChange,
69
+ setStates,
70
+ enabled: true,
71
+ });
72
+
73
+ useEffect(() => {
74
+ let cancelled = false;
75
+ (async () => {
76
+ setLoadingMeta(true);
77
+ try {
78
+ const [c, s] = await Promise.all([
79
+ listCountries(''),
80
+ listStates(form.countryCode || 'BR'),
81
+ ]);
82
+ if (!cancelled) {
83
+ setCountries(c);
84
+ setStates(s);
85
+ }
86
+ } catch (e) {
87
+ if (!cancelled) setError(e?.message || 'Falha ao carregar país/estado');
88
+ } finally {
89
+ if (!cancelled) setLoadingMeta(false);
90
+ }
91
+ })();
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, []);
97
+
98
+ useEffect(() => {
99
+ let cancelled = false;
100
+ const initial = hydrateAddressFromRow(row);
101
+ setForm(initial);
102
+ setCoordsManualEdit(false);
103
+ onFormChange?.(initial);
104
+ const digits = onlyDigits(initial.cep);
105
+
106
+ (async () => {
107
+ if (digits.length === 8) {
108
+ try {
109
+ await runLookup(digits, {preserveFilledFields: true});
110
+ } catch {
111
+ // keep editable existing data
112
+ }
113
+ return;
114
+ }
115
+ if (mode !== 'create' || hasCoordinates(initial)) {
116
+ return;
117
+ }
118
+ const coords = await getCurrentCoordinates();
119
+ if (cancelled || !coords) return;
120
+ setForm(prev => {
121
+ const next = {...prev, ...coords};
122
+ onFormChange?.(next);
123
+ return next;
124
+ });
125
+ })();
126
+
127
+ return () => {
128
+ cancelled = true;
129
+ cancelPending();
130
+ };
131
+ }, [mode, row]); // eslint-disable-line react-hooks/exhaustive-deps
132
+
133
+ const onChange = useCallback(
134
+ (key, value) => {
135
+ setForm(prev => {
136
+ let nextValue = value;
137
+ if (key === 'latitude' || key === 'longitude') {
138
+ // Keep raw string while typing so decimals/signs are not truncated mid-edit.
139
+ // Persist number only when the full token parses; otherwise keep the draft string.
140
+ const parsed = parseOptionalCoordinate(value);
141
+ const trimmed = String(value ?? '').trim();
142
+ const looksComplete =
143
+ trimmed !== '' &&
144
+ trimmed !== '-' &&
145
+ trimmed !== '.' &&
146
+ trimmed !== '-.' &&
147
+ !trimmed.endsWith('.') &&
148
+ !trimmed.endsWith(',');
149
+ nextValue =
150
+ parsed != null && looksComplete ? parsed : value;
151
+ }
152
+ const next = {...prev, [key]: nextValue};
153
+ // Troca de CEP: número do imóvel anterior não se aplica (#746)
154
+ if (key === 'cep' && onlyDigits(value) !== onlyDigits(prev.cep)) {
155
+ next.number = '';
156
+ }
157
+ onFormChange?.(next);
158
+ return next;
159
+ });
160
+ if (key === 'latitude' || key === 'longitude') {
161
+ setCoordsManualEdit(true);
162
+ }
163
+ if (key === 'cep') {
164
+ setCepError(null);
165
+ setError(null);
166
+ setGeocodeMiss(false);
167
+ setCoordsManualEdit(false);
168
+ }
169
+ },
170
+ [onFormChange, setCepError, setGeocodeMiss],
171
+ );
172
+
173
+ const onSelectCountry = useCallback(async item => {
174
+ setForm(prev => ({
175
+ ...prev,
176
+ countryCode: item.code,
177
+ countryName: item.name,
178
+ uf: '',
179
+ stateName: '',
180
+ }));
181
+ setShowCountryList(false);
182
+ try {
183
+ const s = await listStates(item.code);
184
+ setStates(s);
185
+ } catch (e) {
186
+ setError(e?.message || 'Falha ao carregar estados');
187
+ }
188
+ }, []);
189
+
190
+ const onSelectState = useCallback(item => {
191
+ setForm(prev => ({
192
+ ...prev,
193
+ uf: item.uf,
194
+ stateName: item.name,
195
+ }));
196
+ setShowStateList(false);
197
+ }, []);
198
+
199
+ const handleSave = useCallback(async () => {
200
+ if (!peopleIri && mode === 'create') {
201
+ setError('Pessoa/empresa obrigatória');
202
+ return;
203
+ }
204
+ setSaving(true);
205
+ setError(null);
206
+ try {
207
+ const payload = buildAddressSavePayload(form, peopleIri);
208
+ if (mode === 'edit' && row) {
209
+ payload.id = row['@id'] || row.id;
210
+ }
211
+ const saved = await saveAction(payload);
212
+ onSaved?.(saved);
213
+ } catch (e) {
214
+ setError(e?.message || 'Falha ao salvar endereço');
215
+ } finally {
216
+ setSaving(false);
217
+ }
218
+ }, [form, peopleIri, mode, row, saveAction, onSaved]);
219
+
220
+ const countryLabel = useMemo(
221
+ () => form.countryName || form.countryCode || 'Selecione o país',
222
+ [form.countryCode, form.countryName],
223
+ );
224
+ const stateLabel = useMemo(
225
+ () =>
226
+ form.stateName
227
+ ? `${form.stateName} (${form.uf})`
228
+ : form.uf || 'Selecione o estado',
229
+ [form.stateName, form.uf],
230
+ );
231
+
232
+ const mapMarkerPayload = useMemo(() => buildMapMarkerPayload(form), [form]);
233
+ const mapUserCoordinates = hasCoordinates(form)
234
+ ? {latitude: Number(form.latitude), longitude: Number(form.longitude)}
235
+ : null;
236
+
237
+ const displayError = error || cepError;
238
+
239
+ const mapPanel = (
240
+ <View style={[styles.mapPane, isDesktop && styles.mapPaneDesktop]}>
241
+ <Text style={styles.mapPaneTitle}>Mapa</Text>
242
+ {hasCoordinates(form) || mapMarkerPayload ? (
243
+ <View style={[styles.liveMap, isDesktop && styles.liveMapDesktop]}>
244
+ <DefaultMap
245
+ markerPayloads={mapMarkerPayload ? [mapMarkerPayload] : []}
246
+ userCoordinates={mapUserCoordinates}
247
+ />
248
+ </View>
249
+ ) : form.mapStaticUrl ? (
250
+ <Image
251
+ source={{uri: form.mapStaticUrl}}
252
+ style={[styles.mapImage, isDesktop && styles.mapImageDesktop]}
253
+ resizeMode="cover"
254
+ />
255
+ ) : (
256
+ <View style={[styles.mapPlaceholder, isDesktop && styles.mapImageDesktop]}>
257
+ <Text style={styles.mapPlaceholderTitle}>Mapa indisponível</Text>
258
+ <Text style={styles.mapPlaceholderText}>
259
+ Consulte um CEP para carregar a localização quando a API retornar a
260
+ imagem, ou informe latitude/longitude manualmente.
261
+ </Text>
262
+ </View>
263
+ )}
264
+ </View>
265
+ );
266
+
267
+ return (
268
+ <ScrollView
269
+ style={styles.scroll}
270
+ contentContainerStyle={[
271
+ styles.container,
272
+ isDesktop && styles.containerDesktop,
273
+ ]}
274
+ keyboardShouldPersistTaps="handled">
275
+ <View style={[styles.formPane, isDesktop && styles.formPaneDesktop]}>
276
+ {displayError ? <Text style={styles.error}>{displayError}</Text> : null}
277
+ {geocodeMiss ? (
278
+ <Text testID="address-geocode-miss" style={styles.hint}>
279
+ {GEOCODE_MISS_MESSAGE}
280
+ </Text>
281
+ ) : null}
282
+ <Text style={styles.hint}>
283
+ Informe o CEP (8 dígitos) para autopreencher via ERP. Complemento/apelido são preservados; o número é limpo ao trocar o CEP.
284
+ </Text>
285
+
286
+ <Field label="Apelido">
287
+ <TextInput
288
+ style={styles.input}
289
+ value={form.nickname}
290
+ onChangeText={v => onChange('nickname', v)}
291
+ placeholder="Ex.: Casa, Trabalho"
292
+ />
293
+ </Field>
294
+
295
+ <Field label="CEP">
296
+ <View style={styles.row}>
297
+ <TextInput
298
+ testID="address-cep-input"
299
+ style={[styles.input, styles.flex]}
300
+ value={form.cep}
301
+ onChangeText={v => onChange('cep', v)}
302
+ onBlur={onCepBlur}
303
+ keyboardType="number-pad"
304
+ maxLength={9}
305
+ placeholder="00000-000"
306
+ />
307
+ {loadingCep ? <ActivityIndicator style={styles.loader} /> : null}
308
+ </View>
309
+ </Field>
310
+
311
+ <Field label="País" style={showCountryList ? styles.fieldRaised : null}>
312
+ <TouchableOpacity
313
+ style={styles.select}
314
+ onPress={() => {
315
+ setShowStateList(false);
316
+ setShowCountryList(open => !open);
317
+ }}
318
+ disabled={loadingMeta}>
319
+ <Text numberOfLines={1}>{countryLabel}</Text>
320
+ </TouchableOpacity>
321
+ {showCountryList ? (
322
+ <ScrollView
323
+ style={styles.dropdown}
324
+ nestedScrollEnabled
325
+ keyboardShouldPersistTaps="handled">
326
+ {countries.map(item => (
327
+ <TouchableOpacity
328
+ key={item.id || item.code}
329
+ style={styles.option}
330
+ onPress={() => onSelectCountry(item)}>
331
+ <Text numberOfLines={1}>
332
+ {item.name} ({item.code})
333
+ </Text>
334
+ </TouchableOpacity>
335
+ ))}
336
+ </ScrollView>
337
+ ) : null}
338
+ </Field>
339
+
340
+ <Field label="Estado" style={showStateList ? styles.fieldRaised : null}>
341
+ <TouchableOpacity
342
+ style={styles.select}
343
+ onPress={() => {
344
+ setShowCountryList(false);
345
+ setShowStateList(open => !open);
346
+ }}
347
+ disabled={loadingMeta}>
348
+ <Text numberOfLines={1}>{stateLabel}</Text>
349
+ </TouchableOpacity>
350
+ {showStateList ? (
351
+ <ScrollView
352
+ style={styles.dropdown}
353
+ nestedScrollEnabled
354
+ keyboardShouldPersistTaps="handled">
355
+ {states.map(item => (
356
+ <TouchableOpacity
357
+ key={item.id || item.uf}
358
+ style={styles.option}
359
+ onPress={() => onSelectState(item)}>
360
+ <Text numberOfLines={1}>
361
+ {item.name} ({item.uf})
362
+ </Text>
363
+ </TouchableOpacity>
364
+ ))}
365
+ </ScrollView>
366
+ ) : null}
367
+ </Field>
368
+
369
+ <Field label="Cidade">
370
+ <TextInput
371
+ testID="address-city-input"
372
+ style={styles.input}
373
+ value={form.city}
374
+ onChangeText={v => onChange('city', v)}
375
+ />
376
+ </Field>
377
+ <Field label="Bairro">
378
+ <TextInput
379
+ testID="address-district-input"
380
+ style={styles.input}
381
+ value={form.district}
382
+ onChangeText={v => onChange('district', v)}
383
+ />
384
+ </Field>
385
+ <Field label="Logradouro">
386
+ <TextInput
387
+ testID="address-street-input"
388
+ style={styles.input}
389
+ value={form.street}
390
+ onChangeText={v => onChange('street', v)}
391
+ />
392
+ </Field>
393
+ <Field label="Número">
394
+ <TextInput
395
+ style={styles.input}
396
+ value={form.number}
397
+ onChangeText={v => onChange('number', v)}
398
+ keyboardType="default"
399
+ />
400
+ </Field>
401
+ <Field label="Complemento">
402
+ <TextInput
403
+ style={styles.input}
404
+ value={form.complement}
405
+ onChangeText={v => onChange('complement', v)}
406
+ />
407
+ </Field>
408
+ <LatLonReadonlyFields
409
+ form={form}
410
+ styles={styles}
411
+ Field={Field}
412
+ editable={!hasCoordinates(form) || geocodeMiss || coordsManualEdit}
413
+ onChange={onChange}
414
+ />
415
+
416
+ {!hideActions ? (
417
+ <View style={styles.actions}>
418
+ <TouchableOpacity style={styles.btnSecondary} onPress={onCancel}>
419
+ <Text>Cancelar</Text>
420
+ </TouchableOpacity>
421
+ <TouchableOpacity
422
+ style={styles.btnPrimary}
423
+ onPress={handleSave}
424
+ disabled={saving}>
425
+ <Text style={styles.btnPrimaryText}>
426
+ {saving ? 'Salvando...' : submitLabel}
427
+ </Text>
428
+ </TouchableOpacity>
429
+ </View>
430
+ ) : null}
431
+ </View>
432
+
433
+ {mapPanel}
434
+ </ScrollView>
435
+ );
436
+ }
437
+
438
+ function Field({label, children, style = null}) {
439
+ return (
440
+ <View style={[styles.field, style]}>
441
+ <Text style={styles.label}>{label}</Text>
442
+ {children}
443
+ </View>
444
+ );
445
+ }
446
+
@@ -0,0 +1,133 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ export default StyleSheet.create({
4
+ scroll: {flex: 1, width: '100%'},
5
+ container: {padding: 16, gap: 16},
6
+ containerDesktop: {
7
+ flexDirection: 'row',
8
+ alignItems: 'flex-start',
9
+ alignSelf: 'center',
10
+ width: '100%',
11
+ maxWidth: 1280,
12
+ paddingHorizontal: 32,
13
+ paddingVertical: 24,
14
+ gap: 24,
15
+ },
16
+ formPane: {width: '100%'},
17
+ formPaneDesktop: {flex: 1, maxWidth: 560},
18
+ field: {marginBottom: 10, position: 'relative', zIndex: 1},
19
+ fieldRaised: {zIndex: 30, elevation: 8},
20
+ label: {fontSize: 13, fontWeight: '600', marginBottom: 4, color: '#334155'},
21
+ input: {
22
+ borderWidth: 1,
23
+ borderColor: '#CBD5E1',
24
+ borderRadius: 8,
25
+ paddingHorizontal: 12,
26
+ paddingVertical: 10,
27
+ backgroundColor: '#fff',
28
+ },
29
+ select: {
30
+ borderWidth: 1,
31
+ borderColor: '#CBD5E1',
32
+ borderRadius: 8,
33
+ paddingHorizontal: 12,
34
+ paddingVertical: 12,
35
+ backgroundColor: '#F8FAFC',
36
+ },
37
+ dropdown: {
38
+ maxHeight: 180,
39
+ borderWidth: 1,
40
+ borderColor: '#E2E8F0',
41
+ borderRadius: 8,
42
+ marginTop: 4,
43
+ backgroundColor: '#fff',
44
+ overflow: 'hidden',
45
+ zIndex: 40,
46
+ elevation: 8,
47
+ },
48
+ option: {paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#F1F5F9'},
49
+ row: {flexDirection: 'row', alignItems: 'center'},
50
+ flex: {flex: 1},
51
+ loader: {marginLeft: 8},
52
+ error: {color: '#B91C1C', marginBottom: 8},
53
+ hint: {color: '#64748B', marginBottom: 8, fontSize: 12},
54
+ mapPane: {
55
+ width: '100%',
56
+ borderWidth: 1,
57
+ borderColor: '#E2E8F0',
58
+ borderRadius: 12,
59
+ backgroundColor: '#F8FAFC',
60
+ padding: 16,
61
+ gap: 12,
62
+ },
63
+ mapPaneDesktop: {flex: 1, minHeight: 560},
64
+ mapPaneTitle: {fontWeight: '700', fontSize: 15, color: '#0F172A'},
65
+ mapImage: {
66
+ width: '100%',
67
+ height: 220,
68
+ borderRadius: 10,
69
+ backgroundColor: '#E2E8F0',
70
+ },
71
+ mapImageDesktop: {height: 360},
72
+ liveMap: {
73
+ width: '100%',
74
+ height: 220,
75
+ borderRadius: 10,
76
+ overflow: 'hidden',
77
+ backgroundColor: '#E2E8F0',
78
+ },
79
+ liveMapDesktop: {height: 360},
80
+ facadeImage: {
81
+ width: '100%',
82
+ height: 180,
83
+ borderRadius: 10,
84
+ backgroundColor: '#E2E8F0',
85
+ },
86
+ mapPlaceholder: {
87
+ height: 220,
88
+ borderRadius: 10,
89
+ backgroundColor: '#E2E8F0',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ padding: 18,
93
+ },
94
+ facadePlaceholder: {
95
+ minHeight: 120,
96
+ borderRadius: 10,
97
+ backgroundColor: '#E2E8F0',
98
+ alignItems: 'center',
99
+ justifyContent: 'center',
100
+ padding: 18,
101
+ },
102
+ mapPlaceholderTitle: {
103
+ fontWeight: '700',
104
+ color: '#334155',
105
+ marginBottom: 6,
106
+ },
107
+ mapPlaceholderText: {
108
+ color: '#64748B',
109
+ textAlign: 'center',
110
+ fontSize: 13,
111
+ lineHeight: 18,
112
+ },
113
+ actions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 16},
114
+ btnSecondary: {paddingHorizontal: 16, paddingVertical: 12},
115
+ btnPrimary: {
116
+ backgroundColor: '#1D4ED8',
117
+ paddingHorizontal: 18,
118
+ paddingVertical: 12,
119
+ borderRadius: 8,
120
+ },
121
+ btnPrimaryText: {color: '#fff', fontWeight: '600'},
122
+ coordRow: {
123
+ flexDirection: 'row',
124
+ gap: 12,
125
+ },
126
+ coordField: {
127
+ flex: 1,
128
+ },
129
+ inputReadonly: {
130
+ backgroundColor: '#F1F5F9',
131
+ color: '#475569',
132
+ },
133
+ });
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Explicit Latitude/Longitude inputs for DefaultAddress (app-community#360).
3
+ */
4
+ import React from 'react';
5
+ import {Text, TextInput, View} from 'react-native';
6
+ import styles from './DefaultAddress.styles';
7
+ import {parseOptionalCoordinate} from '../../services/addressFormUtils';
8
+
9
+ function Field({label, children, style = null}) {
10
+ return (
11
+ <View style={[styles.field, style]}>
12
+ <Text style={styles.label}>{label}</Text>
13
+ {children}
14
+ </View>
15
+ );
16
+ }
17
+
18
+ export default function DefaultAddressCoordFields({form, onChange}) {
19
+ return (
20
+ <View style={styles.coordRow}>
21
+ <View style={styles.coordHalf}>
22
+ <Field label="Latitude">
23
+ <TextInput
24
+ style={styles.input}
25
+ value={
26
+ form.latitude === null || form.latitude === undefined
27
+ ? ''
28
+ : String(form.latitude)
29
+ }
30
+ onChangeText={v => onChange('latitude', parseOptionalCoordinate(v))}
31
+ placeholder="-23.5505"
32
+ keyboardType="decimal-pad"
33
+ accessibilityLabel="Latitude"
34
+ testID="address-latitude-input"
35
+ />
36
+ </Field>
37
+ </View>
38
+ <View style={styles.coordHalf}>
39
+ <Field label="Longitude">
40
+ <TextInput
41
+ style={styles.input}
42
+ value={
43
+ form.longitude === null || form.longitude === undefined
44
+ ? ''
45
+ : String(form.longitude)
46
+ }
47
+ onChangeText={v =>
48
+ onChange('longitude', parseOptionalCoordinate(v))
49
+ }
50
+ placeholder="-46.6333"
51
+ keyboardType="decimal-pad"
52
+ accessibilityLabel="Longitude"
53
+ testID="address-longitude-input"
54
+ />
55
+ </Field>
56
+ </View>
57
+ </View>
58
+ );
59
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import {Image, Text, View} from 'react-native';
3
+ import DefaultMap from '../map/DefaultMap';
4
+ import {hasCoordinates} from './defaultAddressHelpers';
5
+ import {styles} from './defaultAddressStyles';
6
+
7
+ /**
8
+ * Map + facade preview pane for DefaultAddress.
9
+ */
10
+ export default function DefaultAddressMapPane({
11
+ form,
12
+ mapMarkerPayload,
13
+ mapUserCoordinates,
14
+ isDesktop,
15
+ }) {
16
+ return (
17
+ <View style={[styles.mapPane, isDesktop && styles.mapPaneDesktop]}>
18
+ <Text style={styles.mapPaneTitle}>Mapa</Text>
19
+ {hasCoordinates(form) || mapMarkerPayload ? (
20
+ <View style={[styles.liveMap, isDesktop && styles.liveMapDesktop]}>
21
+ <DefaultMap
22
+ markerPayloads={mapMarkerPayload ? [mapMarkerPayload] : []}
23
+ userCoordinates={mapUserCoordinates}
24
+ />
25
+ </View>
26
+ ) : form.mapStaticUrl ? (
27
+ <Image
28
+ source={{uri: form.mapStaticUrl}}
29
+ style={[styles.mapImage, isDesktop && styles.mapImageDesktop]}
30
+ resizeMode="cover"
31
+ />
32
+ ) : (
33
+ <View style={[styles.mapPlaceholder, isDesktop && styles.mapImageDesktop]}>
34
+ <Text style={styles.mapPlaceholderTitle}>Mapa indisponível</Text>
35
+ <Text style={styles.mapPlaceholderText}>
36
+ Consulte um CEP para carregar a localização quando a API retornar a
37
+ imagem, ou informe latitude/longitude manualmente.
38
+ </Text>
39
+ </View>
40
+ )}
41
+
42
+ {/* Fachada only when provider returned street-view URL (not OSM). */}
43
+ {form.facadeUrl ? (
44
+ <>
45
+ <Text style={styles.mapPaneTitle}>Fachada</Text>
46
+ <Image
47
+ source={{uri: form.facadeUrl}}
48
+ style={styles.facadeImage}
49
+ resizeMode="cover"
50
+ />
51
+ </>
52
+ ) : null}
53
+ </View>
54
+ );
55
+ }