@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.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +19 -0
- package/.github/workflows/publish-npm.yml +91 -0
- package/AGENTS.md +46 -50
- package/docs/technical/Contrato-de-colunas.md +67 -0
- package/docs/technical/Default-Components.md +76 -0
- package/docs/technical/DefaultForm.md +80 -0
- package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
- package/docs/technical/DefaultTable.md +272 -0
- package/docs/wiki.md +1 -0
- package/package.json +4 -3
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +446 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -0
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
- package/src/react/components/address/DefaultAddressMapPane.js +55 -0
- package/src/react/components/address/LatLonReadonlyFields.js +51 -0
- package/src/react/components/address/defaultAddressHelpers.js +163 -0
- package/src/react/components/address/defaultAddressStyles.js +148 -0
- package/src/react/components/address/defaultAddressUtils.js +129 -0
- package/src/react/components/extra-data/DefaultExtraData.js +152 -0
- package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
- package/src/react/components/files/DefaultFile.js +231 -46
- package/src/react/components/files/DefaultFileColumn.js +123 -0
- package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
- package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
- package/src/react/components/filters/CompactFilterSelector.js +51 -35
- package/src/react/components/filters/DefaultColumnFilter.js +60 -48
- package/src/react/components/filters/DefaultExternalFilters.js +324 -265
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +48 -12
- package/src/react/components/help/DefaultTooltip.styles.js +20 -0
- package/src/react/components/inputs/DefaultColorInput.js +153 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
- package/src/react/components/inputs/DefaultInput.js +296 -141
- package/src/react/components/inputs/DefaultInput.styles.js +26 -0
- package/src/react/components/inputs/DefaultSelect.js +134 -123
- package/src/react/components/inputs/defaultInputUtils.js +262 -213
- package/src/react/components/map/DefaultMap.native.js +73 -73
- package/src/react/components/map/DefaultMap.shared.js +911 -762
- package/src/react/components/map/DefaultMap.web.js +124 -68
- package/src/react/components/table/DefaultColumnMenu.js +22 -3
- package/src/react/components/table/DefaultDebug.js +44 -37
- package/src/react/components/table/DefaultSearchModal.js +9 -1
- package/src/react/components/table/DefaultTable.grouping.js +98 -0
- package/src/react/components/table/DefaultTable.js +217 -111
- package/src/react/components/table/DefaultTable.styles.js +369 -310
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- package/src/react/components/table/DefaultTableBody.js +13 -8
- package/src/react/components/table/DefaultTableCards.js +20 -1
- package/src/react/components/table/DefaultTableControls.js +180 -205
- package/src/react/components/table/DefaultTableImportModal.js +108 -0
- package/src/react/components/table/DefaultTableInput.js +1 -0
- package/src/react/components/table/DefaultTableRowActions.js +1 -1
- package/src/react/components/table/DefaultTableRows.js +357 -325
- package/src/react/components/table/DefaultTableSearch.js +28 -5
- package/src/react/components/table/DefaultTableToolbar.js +92 -3
- package/src/react/components/table/useDefaultTablePagination.js +56 -7
- package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
- package/src/react/components/upload/DefaultUpload.js +406 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
- package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
- package/src/react/components/upload/defaultUploadHelpers.js +108 -0
- package/src/react/components/upload/defaultUploadLibrary.js +248 -0
- package/src/react/components/upload/fileUpload.js +93 -0
- package/src/react/hooks/usePostalCodeLookup.js +159 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +262 -0
- package/src/react/services/addressGeo.js +230 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +22 -6
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/form/DefaultForm.test.js +123 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
- package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
- package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
- package/src/tests/react/components/table/DefaultTable.test.js +53 -2
- package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
- package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
- package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
- package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
- package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
- package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
- package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +179 -0
- package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
- package/src/tests/react/services/addressGeoLookup.test.js +105 -0
- package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
- package/src/tests/react/services/smoke-637/index.html +132 -0
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {lookupPostalCode, listStates} from '../services/addressGeo';
|
|
3
|
+
import {
|
|
4
|
+
clearPostalCodeDerivedFields,
|
|
5
|
+
isGeocodeMiss,
|
|
6
|
+
mergePostalCodeData,
|
|
7
|
+
onlyDigits,
|
|
8
|
+
} from '../services/addressFormUtils';
|
|
9
|
+
|
|
10
|
+
const DEBOUNCE_MS = 450;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CEP lookup with debounce (8 digits), race-token protection and loading/error state.
|
|
14
|
+
* Clears number on lookup; clears derived address/coords on failure (#746).
|
|
15
|
+
* Preserves complement/nickname.
|
|
16
|
+
*/
|
|
17
|
+
export default function usePostalCodeLookup({
|
|
18
|
+
formCep,
|
|
19
|
+
setForm,
|
|
20
|
+
onFormChange,
|
|
21
|
+
setStates,
|
|
22
|
+
enabled = true,
|
|
23
|
+
}) {
|
|
24
|
+
const [loadingCep, setLoadingCep] = useState(false);
|
|
25
|
+
const [cepError, setCepError] = useState(null);
|
|
26
|
+
const [geocodeMiss, setGeocodeMiss] = useState(false);
|
|
27
|
+
const requestIdRef = useRef(0);
|
|
28
|
+
const debounceRef = useRef(null);
|
|
29
|
+
const lastRequestedRef = useRef('');
|
|
30
|
+
|
|
31
|
+
const runLookup = useCallback(
|
|
32
|
+
async (rawCep, {preserveFilledFields = false} = {}) => {
|
|
33
|
+
const digits = onlyDigits(rawCep).slice(0, 8);
|
|
34
|
+
if (digits.length !== 8) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const requestId = ++requestIdRef.current;
|
|
39
|
+
lastRequestedRef.current = digits;
|
|
40
|
+
setLoadingCep(true);
|
|
41
|
+
setCepError(null);
|
|
42
|
+
setGeocodeMiss(false);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const data = await lookupPostalCode(digits);
|
|
46
|
+
if (requestId !== requestIdRef.current) {
|
|
47
|
+
return null; // stale response
|
|
48
|
+
}
|
|
49
|
+
setGeocodeMiss(isGeocodeMiss(data));
|
|
50
|
+
setForm(prev => {
|
|
51
|
+
const next = mergePostalCodeData(
|
|
52
|
+
{...prev, cep: digits},
|
|
53
|
+
data,
|
|
54
|
+
{preserveFilledFields},
|
|
55
|
+
);
|
|
56
|
+
onFormChange?.(next);
|
|
57
|
+
return next;
|
|
58
|
+
});
|
|
59
|
+
if (data?.uf || data?.state) {
|
|
60
|
+
try {
|
|
61
|
+
const country =
|
|
62
|
+
data.country === 'Brasil' ||
|
|
63
|
+
data.country === 'Brazil' ||
|
|
64
|
+
data.country === 'BR'
|
|
65
|
+
? 'BR'
|
|
66
|
+
: data.country || 'BR';
|
|
67
|
+
const s = await listStates(country);
|
|
68
|
+
if (requestId === requestIdRef.current) {
|
|
69
|
+
setStates?.(s);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// states optional
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return data;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (requestId !== requestIdRef.current) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const message =
|
|
81
|
+
e?.message ||
|
|
82
|
+
e?.response?.data?.detail ||
|
|
83
|
+
'CEP inválido ou serviço indisponível';
|
|
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
|
+
});
|
|
91
|
+
return null;
|
|
92
|
+
} finally {
|
|
93
|
+
if (requestId === requestIdRef.current) {
|
|
94
|
+
setLoadingCep(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[setForm, onFormChange, setStates],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Debounced auto-lookup ONLY when CEP has exactly 8 digits (never partial).
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!enabled) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const digits = onlyDigits(formCep).slice(0, 8);
|
|
107
|
+
if (digits.length !== 8) {
|
|
108
|
+
if (debounceRef.current) {
|
|
109
|
+
clearTimeout(debounceRef.current);
|
|
110
|
+
debounceRef.current = null;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
if (digits === lastRequestedRef.current) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (debounceRef.current) {
|
|
118
|
+
clearTimeout(debounceRef.current);
|
|
119
|
+
}
|
|
120
|
+
debounceRef.current = setTimeout(() => {
|
|
121
|
+
runLookup(digits);
|
|
122
|
+
}, DEBOUNCE_MS);
|
|
123
|
+
return () => {
|
|
124
|
+
if (debounceRef.current) {
|
|
125
|
+
clearTimeout(debounceRef.current);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}, [formCep, enabled, runLookup]);
|
|
129
|
+
|
|
130
|
+
const onCepBlur = useCallback(() => {
|
|
131
|
+
const digits = onlyDigits(formCep);
|
|
132
|
+
if (digits.length !== 8) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (debounceRef.current) {
|
|
136
|
+
clearTimeout(debounceRef.current);
|
|
137
|
+
}
|
|
138
|
+
runLookup(digits);
|
|
139
|
+
}, [formCep, runLookup]);
|
|
140
|
+
|
|
141
|
+
const cancelPending = useCallback(() => {
|
|
142
|
+
requestIdRef.current += 1;
|
|
143
|
+
if (debounceRef.current) {
|
|
144
|
+
clearTimeout(debounceRef.current);
|
|
145
|
+
}
|
|
146
|
+
setLoadingCep(false);
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
loadingCep,
|
|
151
|
+
cepError,
|
|
152
|
+
setCepError,
|
|
153
|
+
geocodeMiss,
|
|
154
|
+
setGeocodeMiss,
|
|
155
|
+
onCepBlur,
|
|
156
|
+
runLookup,
|
|
157
|
+
cancelPending,
|
|
158
|
+
};
|
|
159
|
+
}
|
package/src/react/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// React entrypoint kept after Vue removal.
|
|
2
2
|
import DefaultFile from './components/files/DefaultFile';
|
|
3
|
+
import DefaultFeatherIconPicker from './components/inputs/DefaultFeatherIconPicker';
|
|
4
|
+
import DefaultUpload from './components/upload/DefaultUpload';
|
|
3
5
|
|
|
4
6
|
const moduleEntry = {
|
|
5
7
|
DefaultFile,
|
|
8
|
+
DefaultFeatherIconPicker,
|
|
9
|
+
DefaultUpload,
|
|
6
10
|
};
|
|
7
11
|
|
|
8
12
|
export {DefaultFile};
|
|
13
|
+
export {DefaultFeatherIconPicker};
|
|
14
|
+
export {DefaultUpload};
|
|
9
15
|
|
|
10
16
|
export default moduleEntry;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const onlyDigits = value => String(value || '').replace(/\D+/g, '');
|
|
2
|
+
|
|
3
|
+
export const emptyAddressForm = {
|
|
4
|
+
nickname: '',
|
|
5
|
+
cep: '',
|
|
6
|
+
street: '',
|
|
7
|
+
number: '',
|
|
8
|
+
complement: '',
|
|
9
|
+
district: '',
|
|
10
|
+
city: '',
|
|
11
|
+
uf: '',
|
|
12
|
+
stateName: '',
|
|
13
|
+
countryCode: 'BR',
|
|
14
|
+
countryName: 'Brazil',
|
|
15
|
+
latitude: null,
|
|
16
|
+
longitude: null,
|
|
17
|
+
mapStaticUrl: null,
|
|
18
|
+
facadeUrl: null,
|
|
19
|
+
provider: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function hydrateAddressFromRow(row) {
|
|
23
|
+
if (!row || typeof row !== 'object') {
|
|
24
|
+
return {...emptyAddressForm};
|
|
25
|
+
}
|
|
26
|
+
const street = row?.street?.street || row?.street || '';
|
|
27
|
+
const district = row?.street?.district?.district || row?.district || '';
|
|
28
|
+
const city = row?.street?.district?.city?.city || row?.city || '';
|
|
29
|
+
const stateEntity = row?.street?.district?.city?.state || row?.state;
|
|
30
|
+
const countryEntity = stateEntity?.country || row?.country;
|
|
31
|
+
return {
|
|
32
|
+
...emptyAddressForm,
|
|
33
|
+
nickname: row?.nickname || '',
|
|
34
|
+
cep: String(row?.street?.cep?.cep || row?.cep || row?.postal_code || ''),
|
|
35
|
+
street: typeof street === 'string' ? street : '',
|
|
36
|
+
number: row?.number != null ? String(row.number) : '',
|
|
37
|
+
complement: row?.complement || '',
|
|
38
|
+
district,
|
|
39
|
+
city,
|
|
40
|
+
uf: stateEntity?.uf || row?.uf || '',
|
|
41
|
+
stateName: stateEntity?.state || '',
|
|
42
|
+
countryCode: countryEntity?.countrycode || countryEntity?.code || 'BR',
|
|
43
|
+
countryName: countryEntity?.countryname || countryEntity?.name || 'Brazil',
|
|
44
|
+
latitude: row?.latitude ?? null,
|
|
45
|
+
longitude: row?.longitude ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function hasAddressText(form) {
|
|
50
|
+
return [
|
|
51
|
+
form?.street,
|
|
52
|
+
form?.number,
|
|
53
|
+
form?.district,
|
|
54
|
+
form?.city,
|
|
55
|
+
form?.uf,
|
|
56
|
+
form?.cep,
|
|
57
|
+
].some(value => String(value || '').trim().length > 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
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"
|
|
64
|
+
return (
|
|
65
|
+
Number.isFinite(lat) &&
|
|
66
|
+
Number.isFinite(lng) &&
|
|
67
|
+
!(Math.abs(lat) < 0.000001 && Math.abs(lng) < 0.000001)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Merge postal-code API payload into form state.
|
|
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).
|
|
76
|
+
* When preserveFilledFields is true, also keeps non-empty street/city/etc.
|
|
77
|
+
*/
|
|
78
|
+
export function mergePostalCodeData(
|
|
79
|
+
prev,
|
|
80
|
+
data,
|
|
81
|
+
{preserveFilledFields = false} = {},
|
|
82
|
+
) {
|
|
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
|
+
};
|
|
100
|
+
|
|
101
|
+
const countryRaw = data?.country;
|
|
102
|
+
const isBrazil =
|
|
103
|
+
countryRaw === 'Brasil' || countryRaw === 'Brazil' || countryRaw === 'BR';
|
|
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
|
+
|
|
122
|
+
return {
|
|
123
|
+
...prev,
|
|
124
|
+
// Número do imóvel anterior não se aplica ao novo CEP (#746)
|
|
125
|
+
number: preserveFilledFields ? prev.number : '',
|
|
126
|
+
complement: prev.complement,
|
|
127
|
+
nickname: prev.nickname,
|
|
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',
|
|
135
|
+
countryName: isBrazil
|
|
136
|
+
? 'Brazil'
|
|
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,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildMapMarkerPayload(form) {
|
|
169
|
+
if (!hasAddressText(form) && !hasCoordinates(form)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const addressLine = [form.street, form.number].filter(Boolean).join(', ');
|
|
173
|
+
const addressExtra = [
|
|
174
|
+
form.district,
|
|
175
|
+
[form.city, form.uf].filter(Boolean).join(' - '),
|
|
176
|
+
form.countryName || form.countryCode,
|
|
177
|
+
form.cep,
|
|
178
|
+
]
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(' • ');
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
id: 'default-address-preview',
|
|
184
|
+
title: form.nickname || 'Endereco',
|
|
185
|
+
addressLine: addressLine || addressExtra || 'Endereco',
|
|
186
|
+
addressExtra,
|
|
187
|
+
latitude: form.latitude,
|
|
188
|
+
longitude: form.longitude,
|
|
189
|
+
geocodeQuery: [
|
|
190
|
+
addressLine,
|
|
191
|
+
form.district,
|
|
192
|
+
[form.city, form.uf].filter(Boolean).join(' - '),
|
|
193
|
+
form.countryName || form.countryCode,
|
|
194
|
+
form.cep,
|
|
195
|
+
]
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
.join(', '),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export {onlyDigits};
|
|
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
|
+
|
|
206
|
+
export function parseOptionalCoordinate(value) {
|
|
207
|
+
if (value === null || value === undefined || value === '') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const parsed = Number(String(value).trim().replace(',', '.'));
|
|
212
|
+
|
|
213
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
214
|
+
}
|
|
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
|
+
|
|
236
|
+
export function getCurrentCoordinates() {
|
|
237
|
+
return new Promise(resolve => {
|
|
238
|
+
const geolocation =
|
|
239
|
+
typeof navigator !== 'undefined' ? navigator.geolocation : null;
|
|
240
|
+
|
|
241
|
+
if (!geolocation?.getCurrentPosition) {
|
|
242
|
+
resolve(null);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
geolocation.getCurrentPosition(
|
|
247
|
+
position => {
|
|
248
|
+
const latitude = Number(position?.coords?.latitude);
|
|
249
|
+
const longitude = Number(position?.coords?.longitude);
|
|
250
|
+
|
|
251
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
252
|
+
resolve(null);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
resolve({latitude, longitude});
|
|
257
|
+
},
|
|
258
|
+
() => resolve(null),
|
|
259
|
+
{enableHighAccuracy: true, timeout: 8000, maximumAge: 60000},
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {api} from '@controleonline/ui-common/src/api';
|
|
2
|
+
|
|
3
|
+
const onlyDigits = value => String(value || '').replace(/\D+/g, '');
|
|
4
|
+
|
|
5
|
+
const hasText = value => String(value || '').trim().length > 0;
|
|
6
|
+
|
|
7
|
+
export function isValidCoord(value) {
|
|
8
|
+
if (value === null || value === undefined || value === '') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
return Number.isFinite(n) && Math.abs(n) > 0.000001;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isPostalLookupIncomplete(data) {
|
|
16
|
+
if (!data || typeof data !== 'object') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const missingText = !hasText(data.street) || !hasText(data.city);
|
|
20
|
+
const missingCoords =
|
|
21
|
+
!isValidCoord(data.latitude ?? data?.map?.latitude) ||
|
|
22
|
+
!isValidCoord(data.longitude ?? data?.map?.longitude);
|
|
23
|
+
return missingText || missingCoords;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mergePostalLookupPayload(base, extra, providerSuffix) {
|
|
27
|
+
const src = extra && typeof extra === 'object' ? extra : {};
|
|
28
|
+
const next = {...(base && typeof base === 'object' ? base : {})};
|
|
29
|
+
const fill = (key, value) => {
|
|
30
|
+
if (!hasText(next[key]) && hasText(value)) {
|
|
31
|
+
next[key] = value;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
fill('street', src.street);
|
|
35
|
+
fill('district', src.district);
|
|
36
|
+
fill('city', src.city);
|
|
37
|
+
fill('state', src.state || src.uf);
|
|
38
|
+
fill('uf', src.uf || src.state);
|
|
39
|
+
fill('country', src.country);
|
|
40
|
+
if (!isValidCoord(next.latitude) && isValidCoord(src.latitude)) {
|
|
41
|
+
next.latitude = Number(src.latitude);
|
|
42
|
+
}
|
|
43
|
+
if (!isValidCoord(next.longitude) && isValidCoord(src.longitude)) {
|
|
44
|
+
next.longitude = Number(src.longitude);
|
|
45
|
+
}
|
|
46
|
+
if (providerSuffix) {
|
|
47
|
+
const current = String(next.provider || '').trim();
|
|
48
|
+
next.provider = current
|
|
49
|
+
? `${current}+${providerSuffix}`
|
|
50
|
+
: providerSuffix;
|
|
51
|
+
}
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mapViaCepPayload(json, digits) {
|
|
56
|
+
if (!json || json.erro) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
cep: digits,
|
|
61
|
+
street: json.logradouro || '',
|
|
62
|
+
district: json.bairro || '',
|
|
63
|
+
city: json.localidade || '',
|
|
64
|
+
state: json.uf || '',
|
|
65
|
+
uf: json.uf || '',
|
|
66
|
+
country: 'Brasil',
|
|
67
|
+
provider: 'viacep-client',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function lookupViaCep(cep) {
|
|
72
|
+
const digits = onlyDigits(cep);
|
|
73
|
+
if (digits.length !== 8) {
|
|
74
|
+
throw new Error('CEP deve ter 8 dígitos');
|
|
75
|
+
}
|
|
76
|
+
const response = await fetch(`https://viacep.com.br/ws/${digits}/json/`);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`ViaCEP HTTP ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
const json = await response.json();
|
|
81
|
+
const mapped = mapViaCepPayload(json, digits);
|
|
82
|
+
if (!mapped) {
|
|
83
|
+
throw new Error('CEP não encontrado');
|
|
84
|
+
}
|
|
85
|
+
return mapped;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildNominatimQuery(data, digits) {
|
|
89
|
+
const cepFormatted =
|
|
90
|
+
digits.length === 8 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;
|
|
91
|
+
return [
|
|
92
|
+
data?.street,
|
|
93
|
+
data?.district,
|
|
94
|
+
data?.city,
|
|
95
|
+
data?.uf || data?.state,
|
|
96
|
+
cepFormatted,
|
|
97
|
+
'Brasil',
|
|
98
|
+
]
|
|
99
|
+
.map(part => String(part || '').trim())
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(', ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function lookupNominatimCoords(data, digits) {
|
|
105
|
+
const q = buildNominatimQuery(data, digits);
|
|
106
|
+
if (!q) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const url = `https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(q)}`;
|
|
110
|
+
const response = await fetch(url, {
|
|
111
|
+
headers: {
|
|
112
|
+
Accept: 'application/json',
|
|
113
|
+
'User-Agent': 'ControleOnline-ui-default/task-637',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const rows = await response.json();
|
|
120
|
+
if (!Array.isArray(rows) || !rows[0]?.lat || !rows[0]?.lon) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
latitude: Number(rows[0].lat),
|
|
125
|
+
longitude: Number(rows[0].lon),
|
|
126
|
+
formatted: rows[0].display_name || null,
|
|
127
|
+
provider: 'nominatim-client',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function lookupPostalCode(cep) {
|
|
132
|
+
const digits = onlyDigits(cep);
|
|
133
|
+
if (digits.length !== 8) {
|
|
134
|
+
throw new Error('CEP deve ter 8 dígitos');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let payload = {cep: digits};
|
|
138
|
+
let apiError = null;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const apiData = await api.fetch(`postal-codes/${digits}`, {method: 'GET'});
|
|
142
|
+
if (apiData && typeof apiData === 'object') {
|
|
143
|
+
payload = {...payload, ...apiData, cep: digits};
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
apiError = e;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!hasText(payload.street) || !hasText(payload.city)) {
|
|
150
|
+
try {
|
|
151
|
+
const viaCep = await lookupViaCep(digits);
|
|
152
|
+
payload = mergePostalLookupPayload(payload, viaCep, viaCep.provider);
|
|
153
|
+
} catch (e) {
|
|
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)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
e?.message ||
|
|
159
|
+
apiError?.message ||
|
|
160
|
+
apiError?.response?.data?.detail ||
|
|
161
|
+
'CEP inválido ou serviço indisponível',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
!isValidCoord(payload.latitude ?? payload?.map?.latitude) ||
|
|
169
|
+
!isValidCoord(payload.longitude ?? payload?.map?.longitude)
|
|
170
|
+
) {
|
|
171
|
+
try {
|
|
172
|
+
const geo = await lookupNominatimCoords(payload, digits);
|
|
173
|
+
if (geo) {
|
|
174
|
+
payload = mergePostalLookupPayload(payload, geo, geo.provider);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// coords optional if text fields already filled
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// No usable address text after all providers → treat as not found (#746)
|
|
182
|
+
if (!hasText(payload.street) && !hasText(payload.city)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
apiError?.message ||
|
|
185
|
+
apiError?.response?.data?.detail ||
|
|
186
|
+
'CEP não encontrado',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return payload;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function listCountries(q = '') {
|
|
194
|
+
const params = q ? {q} : {};
|
|
195
|
+
const data = await api.fetch('address-geo/countries', {method: 'GET', params});
|
|
196
|
+
return Array.isArray(data?.member) ? data.member : [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function listStates(country = 'BR') {
|
|
200
|
+
const data = await api.fetch('address-geo/states', {
|
|
201
|
+
method: 'GET',
|
|
202
|
+
params: {country},
|
|
203
|
+
});
|
|
204
|
+
return Array.isArray(data?.member) ? data.member : [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function buildAddressSavePayload(form, peopleIri) {
|
|
208
|
+
const payload = {
|
|
209
|
+
street: form.street || '',
|
|
210
|
+
city: form.city || '',
|
|
211
|
+
district: form.district || '',
|
|
212
|
+
state: form.uf || form.state || '',
|
|
213
|
+
country: form.countryCode || form.country || 'BR',
|
|
214
|
+
people: peopleIri,
|
|
215
|
+
number: form.number ?? '',
|
|
216
|
+
complement: form.complement || '',
|
|
217
|
+
nickname: form.nickname || '',
|
|
218
|
+
cep: onlyDigits(form.cep),
|
|
219
|
+
latitude: form.latitude ?? 0,
|
|
220
|
+
longitude: form.longitude ?? 0,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (Array.isArray(form.categories)) {
|
|
224
|
+
payload.categories = form.categories;
|
|
225
|
+
} else if (Array.isArray(form.category)) {
|
|
226
|
+
payload.category = form.category;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return payload;
|
|
230
|
+
}
|