@controleonline/ui-default 1.0.272 → 1.0.274

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 (45) hide show
  1. package/.github/workflows/publish-npm.yml +91 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/technical/DefaultTable.md +70 -1
  4. package/package.json +22 -22
  5. package/src/react/components/address/DefaultAddress.js +20 -9
  6. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  7. package/src/react/components/address/LatLonReadonlyFields.js +2 -2
  8. package/src/react/components/address/defaultAddressStyles.js +13 -1
  9. package/src/react/components/files/DefaultFile.js +2 -2
  10. package/src/react/components/files/DefaultFileColumn.js +1 -1
  11. package/src/react/components/filters/DefaultColumnFilter.js +2 -7
  12. package/src/react/components/filters/DefaultExternalFilters.js +4 -11
  13. package/src/react/components/filters/filterValue.js +65 -0
  14. package/src/react/components/help/DefaultTooltip.js +73 -63
  15. package/src/react/components/help/DefaultTooltip.styles.js +23 -23
  16. package/src/react/components/inputs/DefaultInput.js +301 -210
  17. package/src/react/components/inputs/defaultInputUtils.js +193 -170
  18. package/src/react/components/table/DefaultColumnMenu.js +127 -109
  19. package/src/react/components/table/DefaultTable.js +43 -92
  20. package/src/react/components/table/DefaultTable.styles.js +10 -4
  21. package/src/react/components/table/DefaultTable.utils.js +38 -4
  22. package/src/react/components/table/DefaultTableRows.js +324 -153
  23. package/src/react/components/table/DefaultTableToolbar.js +17 -7
  24. package/src/react/components/table/DefaultTableView.js +118 -0
  25. package/src/react/components/upload/DefaultUpload.js +6 -36
  26. package/src/react/components/upload/DefaultUpload.styles.js +5 -5
  27. package/src/react/components/upload/DefaultUploadAttachmentsList.js +11 -3
  28. package/src/react/hooks/usePostalCodeLookup.js +15 -1
  29. package/src/react/services/addressFormUtils.js +104 -38
  30. package/src/react/services/addressGeo.js +6 -3
  31. package/src/react/utils/tableVisibleColumnsPreferences.js +84 -3
  32. package/src/tests/react/components/files/DefaultFile.test.js +16 -0
  33. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  34. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  35. package/src/tests/react/components/table/DefaultTable.test.js +37 -0
  36. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  37. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  38. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  39. package/src/tests/react/components/table/DefaultTableToolbar.waiter.test.js +42 -0
  40. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +31 -9
  41. package/src/tests/react/filters/filterValue.test.js +46 -0
  42. package/src/tests/react/hooks/usePostalCodeLookup.test.js +67 -0
  43. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  44. package/src/tests/react/services/addressFormUtils.test.js +6 -3
  45. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +12 -1
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { Modal, Text, TouchableOpacity, View } from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+ import DefaultTableBody from './DefaultTableBody';
5
+ import DefaultTableFooter from './DefaultTableFooter';
6
+ import DefaultTableToolbar from './DefaultTableToolbar';
7
+ import DefaultForm from '../form/DefaultForm';
8
+ import styles from './DefaultTable.styles';
9
+
10
+ export default function DefaultTableView({
11
+ tablePanelBorderColor,
12
+ themeColors,
13
+ showToolbar,
14
+ storeName,
15
+ showToolbarActions,
16
+ showToolbarControls,
17
+ shouldRenderBottomAddButton,
18
+ resolvedAddLabel,
19
+ floatingAddBackgroundColor,
20
+ resolvedOnAdd,
21
+ floatingAddIconColor,
22
+ shouldRenderFloatingAddButton,
23
+ closeCreateForm,
24
+ isCreateFormOpen,
25
+ resolvedActions,
26
+ columnsForTable,
27
+ getOptionsForColumn,
28
+ handleDefaultCreateSaved,
29
+ requestParamsSeed
30
+ }) {
31
+ return (
32
+ <View
33
+ style={[
34
+ styles.wrap,
35
+ {
36
+ borderWidth: tablePanelBorderColor ? 1 : 0,
37
+ borderColor: tablePanelBorderColor,
38
+ backgroundColor: themeColors.panelBackground,
39
+ },
40
+ ]}
41
+ >
42
+ {showToolbar !== false ? (
43
+ <DefaultTableToolbar
44
+ storeName={storeName}
45
+ showToolbarActions={showToolbarActions}
46
+ showToolbarControls={showToolbarControls}
47
+ />
48
+ ) : null}
49
+ <DefaultTableBody storeName={storeName} />
50
+ <DefaultTableFooter storeName={storeName} />
51
+ {shouldRenderBottomAddButton ? (
52
+ <View style={styles.bottomAddBar}>
53
+ <TouchableOpacity
54
+ accessibilityRole="button"
55
+ accessibilityLabel={resolvedAddLabel}
56
+ activeOpacity={0.84}
57
+ style={[
58
+ styles.bottomAddButton,
59
+ { backgroundColor: floatingAddBackgroundColor },
60
+ ]}
61
+ onPress={resolvedOnAdd}
62
+ >
63
+ <Icon name="plus" size={18} color={floatingAddIconColor} />
64
+ <Text style={[styles.bottomAddText, { color: floatingAddIconColor }]}>
65
+ {resolvedAddLabel}
66
+ </Text>
67
+ </TouchableOpacity>
68
+ </View>
69
+ ) : null}
70
+ {shouldRenderFloatingAddButton ? (
71
+ <TouchableOpacity
72
+ accessibilityRole="button"
73
+ accessibilityLabel={resolvedAddLabel}
74
+ activeOpacity={0.84}
75
+ style={[
76
+ styles.floatingAddButton,
77
+ { backgroundColor: floatingAddBackgroundColor },
78
+ ]}
79
+ onPress={resolvedOnAdd}
80
+ >
81
+ <Icon name="plus" size={24} color={floatingAddIconColor} />
82
+ </TouchableOpacity>
83
+ ) : null}
84
+ <Modal
85
+ animationType="fade"
86
+ onRequestClose={closeCreateForm}
87
+ transparent
88
+ visible={isCreateFormOpen}
89
+ >
90
+ <View style={styles.modalOverlay}>
91
+ <View style={styles.modalCard}>
92
+ <View style={styles.modalHeader}>
93
+ <Text style={styles.modalTitle}>{resolvedAddLabel}</Text>
94
+ <TouchableOpacity
95
+ accessibilityRole="button"
96
+ accessibilityLabel={global.t?.t(storeName, 'button', 'cancel') || 'Cancelar'}
97
+ onPress={closeCreateForm}
98
+ style={styles.modalCloseButton}
99
+ >
100
+ <Icon name="x" size={16} color={themeColors.textPrimary || '#0F172A'} />
101
+ </TouchableOpacity>
102
+ </View>
103
+ <DefaultForm
104
+ actions={resolvedActions}
105
+ columns={columnsForTable}
106
+ getOptionsForColumn={getOptionsForColumn}
107
+ mode="create"
108
+ onCancel={closeCreateForm}
109
+ onSaved={handleDefaultCreateSaved}
110
+ row={requestParamsSeed}
111
+ storeName={storeName}
112
+ />
113
+ </View>
114
+ </View>
115
+ </Modal>
116
+ </View>
117
+ );
118
+ }
@@ -28,8 +28,6 @@ const DefaultUpload = ({
28
28
  attachments = [],
29
29
  entityId,
30
30
  companyId,
31
- company = null,
32
- showAttachmentActions = true,
33
31
  context = 'products',
34
32
  libraryContexts = DEFAULT_LIBRARY_CONTEXTS,
35
33
  fileStoreName = 'file',
@@ -162,17 +160,10 @@ const DefaultUpload = ({
162
160
  setCoverId(coverRelationId || null);
163
161
  }, [coverRelationId]);
164
162
 
165
- const peopleStoreForMedia = useStore('people');
166
- const peopleActionsForLibrary = (() => {
167
- const fromRelation =
168
- relationStoreName === 'people' && typeof relationActions?.getPeopleMedia === 'function'
169
- ? relationActions
170
- : null;
171
- if (fromRelation) return fromRelation;
172
- const fromPeople = peopleStoreForMedia?.actions;
173
- if (typeof fromPeople?.getPeopleMedia === 'function') return fromPeople;
174
- return null;
175
- })();
163
+ const peopleActionsForLibrary =
164
+ relationStoreName === 'people' && typeof relationActions?.getPeopleMedia === 'function'
165
+ ? relationActions
166
+ : null;
176
167
 
177
168
  const loadLibrary = useCallback(async (extraFiles = []) => {
178
169
  setLibraryLoading(true);
@@ -186,26 +177,7 @@ const DefaultUpload = ({
186
177
  peopleActions: peopleActionsForLibrary,
187
178
  knownFileIds: resolvedKnownIds,
188
179
  });
189
- const preferImageMeta = String(fileType || '').toLowerCase() === 'image';
190
- const withMeta = (list) =>
191
- (Array.isArray(list) ? list : []).map(item => {
192
- if (!item || typeof item !== 'object') return item;
193
- const id = extractFileId(item);
194
- if (!id) return item;
195
- return {
196
- ...item,
197
- id: item.id || id,
198
- '@id': item['@id'] || `/files/${id}`,
199
- context: item.context || context || 'people_media',
200
- fileType: item.fileType || item.mimeType || (preferImageMeta ? 'image' : item.fileType),
201
- fileName: item.fileName || item.name || item.originalName || `Arquivo ${id}`,
202
- };
203
- });
204
- setLibraryFiles(
205
- dedupeFiles(
206
- withMeta([...(Array.isArray(extraFiles) ? extraFiles : []), ...files]),
207
- ),
208
- );
180
+ setLibraryFiles(dedupeFiles([...(Array.isArray(extraFiles) ? extraFiles : []), ...files]));
209
181
  } catch (e) {
210
182
  setLibraryError(e?.message || 'Falha ao carregar biblioteca de arquivos.');
211
183
  setLibraryFiles(dedupeFiles(extraFiles));
@@ -400,8 +372,6 @@ const DefaultUpload = ({
400
372
  emptyAttachmentsLabel={emptyAttachmentLabel}
401
373
  status={status}
402
374
  error={error}
403
- fileType={fileType}
404
- company={company || (companyId ? {id: companyId} : null)}
405
375
  />
406
376
  );
407
377
 
@@ -427,7 +397,7 @@ const DefaultUpload = ({
427
397
  handleRemove={handleRemove}
428
398
  buttonPalette={buttonPalette}
429
399
  managerModal={managerModal}
430
- company={company || (companyId ? {id: companyId} : null)}
400
+ company={company}
431
401
  showAttachmentActions={showAttachmentActions}
432
402
  />
433
403
  );
@@ -63,7 +63,7 @@ export const defaultUploadStyles = {
63
63
  modalList: {maxHeight: 520},
64
64
  libraryGrid: {flexDirection: 'row', flexWrap: 'wrap', gap: 10, paddingHorizontal: 14, paddingBottom: 18},
65
65
  fileCard: {
66
- width: 176,
66
+ width: 320,
67
67
  borderWidth: 1,
68
68
  borderColor: '#E2E8F0',
69
69
  borderRadius: 8,
@@ -71,7 +71,7 @@ export const defaultUploadStyles = {
71
71
  overflow: 'hidden',
72
72
  },
73
73
  fileCardAttached: {borderColor: '#86EFAC', backgroundColor: '#F0FDF4'},
74
- fileThumb: {height: 112, backgroundColor: '#F1F5F9', alignItems: 'center', justifyContent: 'center'},
74
+ fileThumb: {width: 320, height: 320, backgroundColor: '#F1F5F9', alignItems: 'center', justifyContent: 'center'},
75
75
  fileThumbFallback: {alignItems: 'center', justifyContent: 'center', gap: 6, height: '100%', width: '100%', backgroundColor: '#EEF2FF'},
76
76
  fileExtBadge: {
77
77
  color: '#312E81',
@@ -130,9 +130,9 @@ export const defaultUploadStyles = {
130
130
  attachmentsEmpty: {borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 10},
131
131
  attachmentsEmptyText: {color: '#666'},
132
132
  attachmentsList: {flexDirection: 'row', gap: 10},
133
- attachmentCard: {width: 170, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 8, backgroundColor: '#fff'},
134
- attachmentThumb: {height: 120, backgroundColor: '#f5f5f5', borderRadius: 6, overflow: 'hidden', marginBottom: 8, alignItems: 'center', justifyContent: 'center'},
135
- attachmentImage: {width: '100%', height: '100%'},
133
+ attachmentCard: {width: 320, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 0, overflow: 'hidden', backgroundColor: '#fff'},
134
+ attachmentThumb: {width: 320, height: 320, backgroundColor: '#f5f5f5', overflow: 'hidden', alignItems: 'center', justifyContent: 'center'},
135
+ attachmentImage: {width: 320, height: 320},
136
136
  attachmentActionsRow: {flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, marginTop: 2},
137
137
  attachmentIconButton: {width: 36, height: 36, borderRadius: 8, alignItems: 'center', justifyContent: 'center'},
138
138
  attachmentRemoveButton: {backgroundColor: '#b00020', paddingVertical: 6, borderRadius: 4},
@@ -74,14 +74,22 @@ export default function DefaultUploadAttachmentsList({
74
74
  return (
75
75
  <View
76
76
  key={row.id || fileId || index}
77
- style={styles.attachmentCard}>
78
- <View style={styles.attachmentThumb}>
77
+ style={[
78
+ styles.attachmentCard,
79
+ // app-community#813: card principal 320x320
80
+ {width: 320, padding: 0, overflow: 'hidden'},
81
+ ]}>
82
+ <View
83
+ style={[
84
+ styles.attachmentThumb,
85
+ {width: 320, height: 320, marginBottom: 0},
86
+ ]}>
79
87
  {file ? (
80
88
  <DefaultFile
81
89
  file={file}
82
90
  company={company}
83
91
  resizeMode="cover"
84
- style={styles.attachmentImage}
92
+ style={{width: 320, height: 320}}
85
93
  />
86
94
  ) : null}
87
95
  </View>
@@ -1,6 +1,8 @@
1
1
  import {useCallback, useEffect, useRef, useState} from 'react';
2
2
  import {lookupPostalCode, listStates} from '../services/addressGeo';
3
3
  import {
4
+ clearPostalCodeDerivedFields,
5
+ isGeocodeMiss,
4
6
  mergePostalCodeData,
5
7
  onlyDigits,
6
8
  } from '../services/addressFormUtils';
@@ -9,7 +11,8 @@ const DEBOUNCE_MS = 450;
9
11
 
10
12
  /**
11
13
  * CEP lookup with debounce (8 digits), race-token protection and loading/error state.
12
- * Never overwrites number/complement/nickname (via mergePostalCodeData).
14
+ * Clears number on lookup; clears derived address/coords on failure (#746).
15
+ * Preserves complement/nickname.
13
16
  */
14
17
  export default function usePostalCodeLookup({
15
18
  formCep,
@@ -20,6 +23,7 @@ export default function usePostalCodeLookup({
20
23
  }) {
21
24
  const [loadingCep, setLoadingCep] = useState(false);
22
25
  const [cepError, setCepError] = useState(null);
26
+ const [geocodeMiss, setGeocodeMiss] = useState(false);
23
27
  const requestIdRef = useRef(0);
24
28
  const debounceRef = useRef(null);
25
29
  const lastRequestedRef = useRef('');
@@ -35,12 +39,14 @@ export default function usePostalCodeLookup({
35
39
  lastRequestedRef.current = digits;
36
40
  setLoadingCep(true);
37
41
  setCepError(null);
42
+ setGeocodeMiss(false);
38
43
 
39
44
  try {
40
45
  const data = await lookupPostalCode(digits);
41
46
  if (requestId !== requestIdRef.current) {
42
47
  return null; // stale response
43
48
  }
49
+ setGeocodeMiss(isGeocodeMiss(data));
44
50
  setForm(prev => {
45
51
  const next = mergePostalCodeData(
46
52
  {...prev, cep: digits},
@@ -76,6 +82,12 @@ export default function usePostalCodeLookup({
76
82
  e?.response?.data?.detail ||
77
83
  'CEP inválido ou serviço indisponível';
78
84
  setCepError(message);
85
+ // CEP não encontrado: zera residual de endereço/coords/número (#746)
86
+ setForm(prev => {
87
+ const next = clearPostalCodeDerivedFields(prev);
88
+ onFormChange?.(next);
89
+ return next;
90
+ });
79
91
  return null;
80
92
  } finally {
81
93
  if (requestId === requestIdRef.current) {
@@ -138,6 +150,8 @@ export default function usePostalCodeLookup({
138
150
  loadingCep,
139
151
  cepError,
140
152
  setCepError,
153
+ geocodeMiss,
154
+ setGeocodeMiss,
141
155
  onCepBlur,
142
156
  runLookup,
143
157
  cancelPending,
@@ -1,20 +1,5 @@
1
1
  const onlyDigits = value => String(value || '').replace(/\D+/g, '');
2
2
 
3
- /** Keep at most 8 CEP digits (Brazilian postal code). */
4
- export const normalizeCepDigits = value => onlyDigits(value).slice(0, 8);
5
-
6
- /**
7
- * Display mask #####-### (hyphen is visual only; max 8 digits).
8
- * @example formatCepMask('01310100') => '01310-100'
9
- */
10
- export const formatCepMask = value => {
11
- const digits = normalizeCepDigits(value);
12
- if (digits.length <= 5) {
13
- return digits;
14
- }
15
- return `${digits.slice(0, 5)}-${digits.slice(5)}`;
16
- };
17
-
18
3
  export const emptyAddressForm = {
19
4
  nickname: '',
20
5
  cep: '',
@@ -73,15 +58,21 @@ export function hasAddressText(form) {
73
58
  }
74
59
 
75
60
  export function hasCoordinates(form) {
61
+ const lat = Number(form?.latitude);
62
+ const lng = Number(form?.longitude);
63
+ // 0,0 is the API/entity default for "no coordinates"
76
64
  return (
77
- Number.isFinite(Number(form?.latitude)) &&
78
- Number.isFinite(Number(form?.longitude))
65
+ Number.isFinite(lat) &&
66
+ Number.isFinite(lng) &&
67
+ !(Math.abs(lat) < 0.000001 && Math.abs(lng) < 0.000001)
79
68
  );
80
69
  }
81
70
 
82
71
  /**
83
72
  * Merge postal-code API payload into form state.
84
- * Always preserves number, complement and nickname (manual fields).
73
+ * Preserves complement and nickname (manual).
74
+ * On CEP lookup (preserveFilledFields=false): clears number and replaces
75
+ * auto-filled address/coords — no residual from previous CEP (#746).
85
76
  * When preserveFilledFields is true, also keeps non-empty street/city/etc.
86
77
  */
87
78
  export function mergePostalCodeData(
@@ -89,36 +80,88 @@ export function mergePostalCodeData(
89
80
  data,
90
81
  {preserveFilledFields = false} = {},
91
82
  ) {
92
- const keep = (key, nextValue) =>
93
- preserveFilledFields && String(prev[key] || '').trim()
94
- ? prev[key]
95
- : nextValue || prev[key] || '';
83
+ const pick = (key, nextValue) => {
84
+ if (preserveFilledFields && String(prev[key] || '').trim()) {
85
+ return prev[key];
86
+ }
87
+ if (
88
+ nextValue !== undefined &&
89
+ nextValue !== null &&
90
+ String(nextValue).trim() !== ''
91
+ ) {
92
+ return nextValue;
93
+ }
94
+ // Fresh CEP lookup must not keep residual street/city/etc.
95
+ if (!preserveFilledFields) {
96
+ return nextValue == null ? '' : String(nextValue);
97
+ }
98
+ return prev[key] || '';
99
+ };
96
100
 
97
101
  const countryRaw = data?.country;
98
102
  const isBrazil =
99
103
  countryRaw === 'Brasil' || countryRaw === 'Brazil' || countryRaw === 'BR';
100
104
 
105
+ const latitude =
106
+ data?.latitude !== undefined
107
+ ? data.latitude
108
+ : data?.map?.latitude !== undefined
109
+ ? data.map.latitude
110
+ : preserveFilledFields
111
+ ? prev.latitude
112
+ : null;
113
+ const longitude =
114
+ data?.longitude !== undefined
115
+ ? data.longitude
116
+ : data?.map?.longitude !== undefined
117
+ ? data.map.longitude
118
+ : preserveFilledFields
119
+ ? prev.longitude
120
+ : null;
121
+
101
122
  return {
102
123
  ...prev,
103
- // Manual fields never overwritten by lookup
104
- number: prev.number,
124
+ // Número do imóvel anterior não se aplica ao novo CEP (#746)
125
+ number: preserveFilledFields ? prev.number : '',
105
126
  complement: prev.complement,
106
127
  nickname: prev.nickname,
107
- cep: normalizeCepDigits(data?.cep || prev.cep),
108
- street: keep('street', data?.street),
109
- district: keep('district', data?.district),
110
- city: keep('city', data?.city),
111
- uf: keep('uf', data?.uf || data?.state),
112
- stateName: keep('stateName', data?.state),
113
- countryCode: isBrazil ? 'BR' : countryRaw || prev.countryCode,
128
+ cep: onlyDigits(data?.cep || prev.cep),
129
+ street: pick('street', data?.street),
130
+ district: pick('district', data?.district),
131
+ city: pick('city', data?.city),
132
+ uf: pick('uf', data?.uf || data?.state),
133
+ stateName: pick('stateName', data?.state),
134
+ countryCode: isBrazil ? 'BR' : countryRaw || prev.countryCode || 'BR',
114
135
  countryName: isBrazil
115
136
  ? 'Brazil'
116
- : countryRaw || prev.countryName,
117
- latitude: data?.latitude ?? data?.map?.latitude ?? prev.latitude,
118
- longitude: data?.longitude ?? data?.map?.longitude ?? prev.longitude,
119
- mapStaticUrl: data?.map?.staticUrl || prev.mapStaticUrl || null,
120
- facadeUrl: data?.facade?.streetViewUrl || prev.facadeUrl || null,
121
- provider: data?.provider || prev.provider || null,
137
+ : countryRaw || prev.countryName || 'Brazil',
138
+ latitude,
139
+ longitude,
140
+ mapStaticUrl: preserveFilledFields
141
+ ? data?.map?.staticUrl || prev.mapStaticUrl || null
142
+ : data?.map?.staticUrl || null,
143
+ facadeUrl: preserveFilledFields
144
+ ? data?.facade?.streetViewUrl || prev.facadeUrl || null
145
+ : data?.facade?.streetViewUrl || null,
146
+ provider: data?.provider || (preserveFilledFields ? prev.provider : null) || null,
147
+ };
148
+ }
149
+
150
+ /** Clear fields derived from a previous CEP when lookup fails (#746). */
151
+ export function clearPostalCodeDerivedFields(prev) {
152
+ return {
153
+ ...prev,
154
+ number: '',
155
+ street: '',
156
+ district: '',
157
+ city: '',
158
+ uf: '',
159
+ stateName: '',
160
+ latitude: null,
161
+ longitude: null,
162
+ mapStaticUrl: null,
163
+ facadeUrl: null,
164
+ provider: null,
122
165
  };
123
166
  }
124
167
 
@@ -157,16 +200,39 @@ export function buildMapMarkerPayload(form) {
157
200
 
158
201
  export {onlyDigits};
159
202
 
203
+ export const GEOCODE_MISS_MESSAGE =
204
+ 'Não foi possível obter a localização no mapa para este endereço. Você pode ajustar latitude e longitude manualmente.';
205
+
160
206
  export function parseOptionalCoordinate(value) {
161
207
  if (value === null || value === undefined || value === '') {
162
208
  return null;
163
209
  }
164
210
 
165
- const parsed = Number(String(value).trim());
211
+ const parsed = Number(String(value).trim().replace(',', '.'));
166
212
 
167
213
  return Number.isFinite(parsed) ? parsed : null;
168
214
  }
169
215
 
216
+ /** True when CEP/address text arrived but Nominatim (or API) left coords empty. */
217
+ export function isGeocodeMiss(data) {
218
+ if (!data || typeof data !== 'object') {
219
+ return false;
220
+ }
221
+ const hasText = [
222
+ data.street,
223
+ data.district,
224
+ data.city,
225
+ data.uf,
226
+ data.state,
227
+ ].some(value => String(value || '').trim().length > 0);
228
+ if (!hasText) {
229
+ return false;
230
+ }
231
+ const lat = data.latitude ?? data.map?.latitude;
232
+ const lon = data.longitude ?? data.map?.longitude;
233
+ return !Number.isFinite(Number(lat)) || !Number.isFinite(Number(lon));
234
+ }
235
+
170
236
  export function getCurrentCoordinates() {
171
237
  return new Promise(resolve => {
172
238
  const geolocation =
@@ -151,7 +151,9 @@ export async function lookupPostalCode(cep) {
151
151
  const viaCep = await lookupViaCep(digits);
152
152
  payload = mergePostalLookupPayload(payload, viaCep, viaCep.provider);
153
153
  } catch (e) {
154
- if (apiError && isPostalLookupIncomplete(payload)) {
154
+ // ViaCEP "CEP não encontrado" (or HTTP error) must surface so the form
155
+ // clears residual address/coords/number (#746) — do not swallow.
156
+ if (isPostalLookupIncomplete(payload)) {
155
157
  throw new Error(
156
158
  e?.message ||
157
159
  apiError?.message ||
@@ -176,11 +178,12 @@ export async function lookupPostalCode(cep) {
176
178
  }
177
179
  }
178
180
 
179
- if (isPostalLookupIncomplete(payload) && apiError && !hasText(payload.street)) {
181
+ // No usable address text after all providers → treat as not found (#746)
182
+ if (!hasText(payload.street) && !hasText(payload.city)) {
180
183
  throw new Error(
181
184
  apiError?.message ||
182
185
  apiError?.response?.data?.detail ||
183
- 'CEP inválido ou serviço indisponível',
186
+ 'CEP não encontrado',
184
187
  );
185
188
  }
186
189
 
@@ -3,12 +3,45 @@ export const TABLE_VIEW_MODE_PREFERENCES_KEY = 'viewMode';
3
3
  export const TABLE_SORT_PREFERENCES_KEY = 'sort';
4
4
  export const TABLE_FILTER_PREFERENCES_KEY = 'filters';
5
5
  export const DEFAULT_TABLE_PREFERENCES_STORAGE_KEY = 'default-table';
6
+ export const REQUIRED_VISIBLE_COLUMN_KEYS = ['id'];
6
7
 
7
8
  const isPlainObject = value =>
8
9
  !!value && typeof value === 'object' && !Array.isArray(value);
9
10
 
10
11
  const getColumnKey = column => column?.key || column?.name || '';
11
12
 
13
+ const normalizeColumnKey = value => String(value || '').trim().toLowerCase();
14
+
15
+ export const isRequiredVisibleColumn = column => {
16
+ const key = normalizeColumnKey(getColumnKey(column));
17
+ if (!key) {
18
+ return false;
19
+ }
20
+
21
+ if (column?.required === true || column?.hideable === false) {
22
+ return true;
23
+ }
24
+
25
+ return REQUIRED_VISIBLE_COLUMN_KEYS.includes(key);
26
+ };
27
+
28
+ export const isRequiredVisibleColumnKey = (key, columns = []) => {
29
+ const normalizedKey = normalizeColumnKey(key);
30
+ if (!normalizedKey) {
31
+ return false;
32
+ }
33
+
34
+ const column = (Array.isArray(columns) ? columns : []).find(
35
+ item => normalizeColumnKey(getColumnKey(item)) === normalizedKey,
36
+ );
37
+
38
+ if (column) {
39
+ return isRequiredVisibleColumn(column);
40
+ }
41
+
42
+ return REQUIRED_VISIBLE_COLUMN_KEYS.includes(normalizedKey);
43
+ };
44
+
12
45
  const normalizePreferenceSegment = value =>
13
46
  String(value || '')
14
47
  .trim()
@@ -141,11 +174,41 @@ export const buildDefaultVisibleColumns = columns =>
141
174
  (Array.isArray(columns) ? columns : []).reduce((accumulator, column) => {
142
175
  const key = getColumnKey(column);
143
176
  if (key) {
144
- accumulator[key] = column?.visible !== false;
177
+ accumulator[key] = isRequiredVisibleColumn(column)
178
+ ? true
179
+ : column?.visible !== false;
145
180
  }
146
181
  return accumulator;
147
182
  }, {});
148
183
 
184
+ const enforceRequiredVisibleColumns = (columns = [], visibleColumns = {}) => {
185
+ const nextVisibleColumns = {...visibleColumns};
186
+ const columnList = Array.isArray(columns) ? columns : [];
187
+
188
+ columnList.forEach(column => {
189
+ const key = getColumnKey(column);
190
+ if (key && isRequiredVisibleColumn(column)) {
191
+ nextVisibleColumns[key] = true;
192
+ }
193
+ });
194
+
195
+ const hasVisibleColumn = Object.keys(nextVisibleColumns).some(
196
+ key => nextVisibleColumns[key] !== false,
197
+ );
198
+
199
+ if (!hasVisibleColumn) {
200
+ const fallbackColumn =
201
+ columnList.find(column => isRequiredVisibleColumn(column)) ||
202
+ columnList.find(column => getColumnKey(column));
203
+ const fallbackKey = getColumnKey(fallbackColumn);
204
+ if (fallbackKey) {
205
+ nextVisibleColumns[fallbackKey] = true;
206
+ }
207
+ }
208
+
209
+ return nextVisibleColumns;
210
+ };
211
+
149
212
  export const sanitizeVisibleColumnsPreference = ({
150
213
  columns = [],
151
214
  visibleColumns = null,
@@ -153,16 +216,34 @@ export const sanitizeVisibleColumnsPreference = ({
153
216
  const defaultVisibleColumns = buildDefaultVisibleColumns(columns);
154
217
 
155
218
  if (!isPlainObject(visibleColumns)) {
156
- return defaultVisibleColumns;
219
+ return enforceRequiredVisibleColumns(columns, defaultVisibleColumns);
157
220
  }
158
221
 
159
- return Object.keys(defaultVisibleColumns).reduce((accumulator, key) => {
222
+ const sanitized = Object.keys(defaultVisibleColumns).reduce((accumulator, key) => {
160
223
  accumulator[key] =
161
224
  typeof visibleColumns[key] === 'boolean'
162
225
  ? visibleColumns[key]
163
226
  : defaultVisibleColumns[key];
164
227
  return accumulator;
165
228
  }, {});
229
+
230
+ return enforceRequiredVisibleColumns(columns, sanitized);
231
+ };
232
+
233
+ export const canHideVisibleColumn = ({
234
+ columns = [],
235
+ fieldName = '',
236
+ visibleColumns = {},
237
+ } = {}) => {
238
+ if (isRequiredVisibleColumnKey(fieldName, columns)) {
239
+ return false;
240
+ }
241
+
242
+ const visibleCount = Object.keys(visibleColumns || {}).filter(
243
+ key => visibleColumns[key] !== false,
244
+ ).length;
245
+
246
+ return visibleCount > 1;
166
247
  };
167
248
 
168
249
  export const sanitizeTableFiltersPreference = ({