@controleonline/ui-default 1.0.270 → 1.0.272

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 (89) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/AGENTS.md +46 -50
  4. package/docs/technical/Contrato-de-colunas.md +67 -0
  5. package/docs/technical/Default-Components.md +76 -0
  6. package/docs/technical/DefaultForm.md +80 -0
  7. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  8. package/docs/technical/DefaultTable.md +203 -0
  9. package/docs/wiki.md +1 -0
  10. package/package.json +22 -21
  11. package/scripts/validate-wiki-pointer.cjs +13 -0
  12. package/src/react/components/address/DefaultAddress.js +435 -0
  13. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  14. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  15. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  16. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  17. package/src/react/components/address/defaultAddressStyles.js +136 -0
  18. package/src/react/components/address/defaultAddressUtils.js +129 -0
  19. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  20. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  21. package/src/react/components/files/DefaultFile.js +231 -46
  22. package/src/react/components/files/DefaultFileColumn.js +123 -0
  23. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  24. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  25. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  26. package/src/react/components/filters/DefaultColumnFilter.js +58 -41
  27. package/src/react/components/filters/DefaultExternalFilters.js +331 -265
  28. package/src/react/components/form/DefaultForm.js +99 -79
  29. package/src/react/components/help/DefaultTooltip.js +59 -33
  30. package/src/react/components/help/DefaultTooltip.styles.js +23 -3
  31. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  32. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  33. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  34. package/src/react/components/inputs/DefaultInput.js +69 -5
  35. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  36. package/src/react/components/inputs/DefaultSelect.js +134 -123
  37. package/src/react/components/inputs/defaultInputUtils.js +69 -43
  38. package/src/react/components/map/DefaultMap.native.js +73 -73
  39. package/src/react/components/map/DefaultMap.shared.js +911 -762
  40. package/src/react/components/map/DefaultMap.web.js +124 -68
  41. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  42. package/src/react/components/table/DefaultDebug.js +44 -37
  43. package/src/react/components/table/DefaultSearchModal.js +9 -1
  44. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  45. package/src/react/components/table/DefaultTable.js +213 -107
  46. package/src/react/components/table/DefaultTable.styles.js +367 -314
  47. package/src/react/components/table/DefaultTableBody.js +13 -8
  48. package/src/react/components/table/DefaultTableCards.js +20 -1
  49. package/src/react/components/table/DefaultTableControls.js +180 -205
  50. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  51. package/src/react/components/table/DefaultTableInput.js +1 -0
  52. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  53. package/src/react/components/table/DefaultTableRows.js +224 -363
  54. package/src/react/components/table/DefaultTableSearch.js +28 -5
  55. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  56. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  57. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  58. package/src/react/components/upload/DefaultUpload.js +436 -0
  59. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  60. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  61. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  62. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  63. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  64. package/src/react/components/upload/fileUpload.js +93 -0
  65. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  66. package/src/react/index.js +6 -0
  67. package/src/react/services/addressFormUtils.js +196 -0
  68. package/src/react/services/addressGeo.js +227 -0
  69. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  70. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  71. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  72. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  73. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  74. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  75. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  76. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  77. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  78. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  79. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  80. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  81. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  82. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  83. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  84. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  85. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  86. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  87. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  88. package/src/tests/react/services/smoke-637/index.html +132 -0
  89. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
@@ -0,0 +1,176 @@
1
+ import {
2
+ mergePostalCodeData,
3
+ onlyDigits,
4
+ hydrateAddressFromRow,
5
+ hasCoordinates,
6
+ parseOptionalCoordinate,
7
+ formatCepMask,
8
+ normalizeCepDigits,
9
+ } from '../../../react/services/addressFormUtils';
10
+
11
+ describe('addressFormUtils', () => {
12
+ test('onlyDigits strips non-digits and keeps order', () => {
13
+ expect(onlyDigits('01310-100')).toBe('01310100');
14
+ expect(onlyDigits('abc')).toBe('');
15
+ });
16
+
17
+ test('mergePostalCodeData fills empty fields from API', () => {
18
+ const prev = {
19
+ nickname: 'Casa',
20
+ number: '100',
21
+ complement: 'Apto 1',
22
+ cep: '',
23
+ street: '',
24
+ district: '',
25
+ city: '',
26
+ uf: '',
27
+ stateName: '',
28
+ countryCode: 'BR',
29
+ countryName: 'Brazil',
30
+ latitude: null,
31
+ longitude: null,
32
+ };
33
+ const data = {
34
+ cep: '01310100',
35
+ street: 'Av Paulista',
36
+ district: 'Bela Vista',
37
+ city: 'São Paulo',
38
+ uf: 'SP',
39
+ state: 'São Paulo',
40
+ country: 'Brasil',
41
+ latitude: -23.5,
42
+ longitude: -46.6,
43
+ };
44
+ const next = mergePostalCodeData(prev, data);
45
+ expect(next.street).toBe('Av Paulista');
46
+ expect(next.district).toBe('Bela Vista');
47
+ expect(next.city).toBe('São Paulo');
48
+ expect(next.uf).toBe('SP');
49
+ expect(next.cep).toBe('01310100');
50
+ expect(next.number).toBe('100');
51
+ expect(next.complement).toBe('Apto 1');
52
+ expect(next.nickname).toBe('Casa');
53
+ expect(next.countryCode).toBe('BR');
54
+ });
55
+
56
+ test('mergePostalCodeData never clears number/complement/nickname', () => {
57
+ const prev = {
58
+ nickname: 'Trabalho',
59
+ number: '42',
60
+ complement: 'Sala 3',
61
+ street: 'Rua X',
62
+ district: 'Centro',
63
+ city: 'Rio',
64
+ uf: 'RJ',
65
+ cep: '20000000',
66
+ countryCode: 'BR',
67
+ countryName: 'Brazil',
68
+ };
69
+ const data = {
70
+ cep: '01310100',
71
+ street: 'Av Paulista',
72
+ district: 'Bela Vista',
73
+ city: 'São Paulo',
74
+ uf: 'SP',
75
+ country: 'Brazil',
76
+ };
77
+ const next = mergePostalCodeData(prev, data);
78
+ expect(next.number).toBe('42');
79
+ expect(next.complement).toBe('Sala 3');
80
+ expect(next.nickname).toBe('Trabalho');
81
+ expect(next.street).toBe('Av Paulista');
82
+ });
83
+
84
+ test('preserveFilledFields keeps existing street/city', () => {
85
+ const prev = {
86
+ nickname: '',
87
+ number: '1',
88
+ complement: '',
89
+ street: 'Minha Rua',
90
+ district: '',
91
+ city: 'Minha Cidade',
92
+ uf: '',
93
+ cep: '01310100',
94
+ countryCode: 'BR',
95
+ countryName: 'Brazil',
96
+ };
97
+ const data = {
98
+ street: 'API Street',
99
+ district: 'API Dist',
100
+ city: 'API City',
101
+ uf: 'SP',
102
+ cep: '01310100',
103
+ };
104
+ const next = mergePostalCodeData(prev, data, {preserveFilledFields: true});
105
+ expect(next.street).toBe('Minha Rua');
106
+ expect(next.city).toBe('Minha Cidade');
107
+ expect(next.district).toBe('API Dist');
108
+ expect(next.uf).toBe('SP');
109
+ });
110
+
111
+ test('hydrateAddressFromRow maps nested street entities', () => {
112
+ const row = {
113
+ nickname: 'HQ',
114
+ number: 10,
115
+ complement: 'Bloco B',
116
+ street: {
117
+ street: 'Rua Nested',
118
+ district: {
119
+ district: 'Centro',
120
+ city: {
121
+ city: 'Campinas',
122
+ state: {uf: 'SP', state: 'São Paulo', country: {countrycode: 'BR', countryname: 'Brazil'}},
123
+ },
124
+ },
125
+ cep: {cep: '13000000'},
126
+ },
127
+ };
128
+ const form = hydrateAddressFromRow(row);
129
+ expect(form.street).toBe('Rua Nested');
130
+ expect(form.district).toBe('Centro');
131
+ expect(form.city).toBe('Campinas');
132
+ expect(form.uf).toBe('SP');
133
+ expect(form.cep).toBe('13000000');
134
+ expect(form.number).toBe('10');
135
+ });
136
+
137
+ test('hasCoordinates requires finite lat/lng', () => {
138
+ expect(hasCoordinates({latitude: -23, longitude: -46})).toBe(true);
139
+ expect(hasCoordinates({latitude: null, longitude: -46})).toBe(false);
140
+ });
141
+
142
+ test('parseOptionalCoordinate returns null for empty/invalid', () => {
143
+ expect(parseOptionalCoordinate('')).toBeNull();
144
+ expect(parseOptionalCoordinate(' ')).toBeNull();
145
+ expect(parseOptionalCoordinate(null)).toBeNull();
146
+ expect(parseOptionalCoordinate(undefined)).toBeNull();
147
+ expect(parseOptionalCoordinate('abc')).toBeNull();
148
+ });
149
+
150
+ test('parseOptionalCoordinate parses valid decimal coordinates', () => {
151
+ expect(parseOptionalCoordinate('-23.5505')).toBeCloseTo(-23.5505);
152
+ expect(parseOptionalCoordinate('-46.6333')).toBeCloseTo(-46.6333);
153
+ expect(parseOptionalCoordinate('0')).toBe(0);
154
+ expect(parseOptionalCoordinate(' 12.5 ')).toBeCloseTo(12.5);
155
+ });
156
+ });
157
+
158
+ describe('formatCepMask / normalizeCepDigits', () => {
159
+ test('normalizeCepDigits caps at 8 digits', () => {
160
+ expect(normalizeCepDigits('013101001')).toBe('01310100');
161
+ expect(normalizeCepDigits('01310-1001')).toBe('01310100');
162
+ expect(normalizeCepDigits('01310-100')).toBe('01310100');
163
+ });
164
+
165
+ test('formatCepMask applies #####-###', () => {
166
+ expect(formatCepMask('01310')).toBe('01310');
167
+ expect(formatCepMask('013101')).toBe('01310-1');
168
+ expect(formatCepMask('01310100')).toBe('01310-100');
169
+ expect(formatCepMask('013101001')).toBe('01310-100');
170
+ });
171
+
172
+ test('lookup gate: only 8 digits', () => {
173
+ expect(onlyDigits(formatCepMask('01310-10')).length).toBe(7);
174
+ expect(onlyDigits(formatCepMask('01310-100')).length).toBe(8);
175
+ });
176
+ });
@@ -0,0 +1,104 @@
1
+ /* global describe, expect, test, beforeEach */
2
+
3
+ jest.mock('@controleonline/ui-common/src/api', () => ({
4
+ api: {
5
+ fetch: jest.fn(),
6
+ },
7
+ }));
8
+
9
+ const {api} = require('@controleonline/ui-common/src/api');
10
+ const {lookupPostalCode} = require('../../../react/services/addressGeo');
11
+ const {mergePostalCodeData} = require('../../../react/services/addressFormUtils');
12
+
13
+ const CEP = '12941040';
14
+
15
+ const API_PROD_INCOMPLETE = {
16
+ cep: CEP,
17
+ street: '',
18
+ district: '',
19
+ city: '',
20
+ state: '',
21
+ uf: '',
22
+ country: 'Brasil',
23
+ latitude: -23.117082,
24
+ longitude: -46.5425915,
25
+ provider: 'viacep+googlemaps',
26
+ };
27
+
28
+ describe('lookupPostalCode #637 (prod payload + client fallback)', () => {
29
+ beforeEach(() => {
30
+ api.fetch.mockReset();
31
+ global.fetch = jest.fn();
32
+ });
33
+
34
+ test('API 200 with empty street/city is completed by ViaCEP; coords stay from API', async () => {
35
+ api.fetch.mockResolvedValue(API_PROD_INCOMPLETE);
36
+ global.fetch.mockResolvedValue({
37
+ ok: true,
38
+ json: async () => ({
39
+ erro: false,
40
+ logradouro: 'Rua Antônio Bonini',
41
+ bairro: 'Vila Santista',
42
+ localidade: 'Atibaia',
43
+ uf: 'SP',
44
+ }),
45
+ });
46
+
47
+ const payload = await lookupPostalCode(CEP);
48
+
49
+ expect(api.fetch).toHaveBeenCalledWith(`postal-codes/${CEP}`, {
50
+ method: 'GET',
51
+ });
52
+ expect(payload.street).toBe('Rua Antônio Bonini');
53
+ expect(payload.city).toBe('Atibaia');
54
+ expect(payload.uf).toBe('SP');
55
+ expect(payload.district).toBe('Vila Santista');
56
+ expect(Number(payload.latitude)).toBeCloseTo(-23.117, 2);
57
+ expect(Number(payload.longitude)).toBeCloseTo(-46.543, 2);
58
+ expect(String(payload.provider)).toContain('viacep-client');
59
+
60
+ const form = mergePostalCodeData(
61
+ {
62
+ nickname: '',
63
+ number: '',
64
+ complement: '',
65
+ cep: CEP,
66
+ street: '',
67
+ district: '',
68
+ city: '',
69
+ uf: '',
70
+ countryCode: 'BR',
71
+ latitude: null,
72
+ longitude: null,
73
+ },
74
+ payload,
75
+ );
76
+ expect(form.street).toBe('Rua Antônio Bonini');
77
+ expect(form.city).toBe('Atibaia');
78
+ expect(form.uf).toBe('SP');
79
+ expect(String(form.latitude)).not.toBe('');
80
+ expect(String(form.latitude)).not.toBe('—');
81
+ expect(String(form.longitude)).not.toBe('—');
82
+ });
83
+
84
+ test('does not throw Postalcode services are not available when ViaCEP fills text', async () => {
85
+ api.fetch.mockRejectedValue({
86
+ message: 'Postalcode services are not available',
87
+ response: {data: {detail: 'Postalcode services are not available'}},
88
+ });
89
+ global.fetch.mockResolvedValue({
90
+ ok: true,
91
+ json: async () => ({
92
+ logradouro: 'Rua Antônio Bonini',
93
+ bairro: 'Vila Santista',
94
+ localidade: 'Atibaia',
95
+ uf: 'SP',
96
+ }),
97
+ });
98
+
99
+ const payload = await lookupPostalCode(CEP);
100
+ expect(payload.street).toBe('Rua Antônio Bonini');
101
+ expect(payload.city).toBe('Atibaia');
102
+ expect(payload.uf).toBe('SP');
103
+ });
104
+ });
@@ -0,0 +1,105 @@
1
+ import {
2
+ isPostalLookupIncomplete,
3
+ isValidCoord,
4
+ mergePostalLookupPayload,
5
+ } from '../../../react/services/addressGeo';
6
+ import {mergePostalCodeData} from '../../../react/services/addressFormUtils';
7
+
8
+ describe('addressGeo CEP fallback helpers', () => {
9
+ test('isValidCoord rejects empty and 0,0 defaults', () => {
10
+ expect(isValidCoord(null)).toBe(false);
11
+ expect(isValidCoord(0)).toBe(false);
12
+ expect(isValidCoord(-23.117)).toBe(true);
13
+ });
14
+
15
+ test('incomplete when API returns coords without street/city', () => {
16
+ expect(
17
+ isPostalLookupIncomplete({
18
+ street: '',
19
+ city: '',
20
+ latitude: -23.117082,
21
+ longitude: -46.5425915,
22
+ provider: 'viacep+googlemaps',
23
+ }),
24
+ ).toBe(true);
25
+ });
26
+
27
+ test('merge fills ViaCEP text and keeps API coords', () => {
28
+ const merged = mergePostalLookupPayload(
29
+ {
30
+ street: '',
31
+ city: '',
32
+ latitude: -23.117082,
33
+ longitude: -46.5425915,
34
+ provider: 'viacep+googlemaps',
35
+ },
36
+ {
37
+ street: 'Rua Antônio Bonini',
38
+ district: 'Vila Santista',
39
+ city: 'Atibaia',
40
+ uf: 'SP',
41
+ country: 'Brasil',
42
+ },
43
+ 'viacep-client',
44
+ );
45
+ expect(merged.street).toBe('Rua Antônio Bonini');
46
+ expect(merged.city).toBe('Atibaia');
47
+ expect(merged.uf).toBe('SP');
48
+ expect(merged.latitude).toBe(-23.117082);
49
+ expect(merged.provider).toBe('viacep+googlemaps+viacep-client');
50
+ });
51
+
52
+ test('#637 form hydrate: API empty street + ViaCEP + coords → Atibaia fields', () => {
53
+ const apiProd = {
54
+ cep: '12941040',
55
+ street: '',
56
+ district: '',
57
+ city: '',
58
+ state: '',
59
+ uf: '',
60
+ country: 'Brasil',
61
+ latitude: -23.117082,
62
+ longitude: -46.5425915,
63
+ provider: 'viacep+googlemaps',
64
+ };
65
+ expect(isPostalLookupIncomplete(apiProd)).toBe(true);
66
+
67
+ const afterViaCep = mergePostalLookupPayload(
68
+ apiProd,
69
+ {
70
+ street: 'Rua Antônio Bonini',
71
+ district: 'Vila Santista',
72
+ city: 'Atibaia',
73
+ uf: 'SP',
74
+ country: 'Brasil',
75
+ provider: 'viacep-client',
76
+ },
77
+ 'viacep-client',
78
+ );
79
+ expect(isPostalLookupIncomplete(afterViaCep)).toBe(false);
80
+
81
+ const form = mergePostalCodeData(
82
+ {
83
+ nickname: '',
84
+ number: '',
85
+ complement: '',
86
+ cep: '12941040',
87
+ street: '',
88
+ district: '',
89
+ city: '',
90
+ uf: '',
91
+ countryCode: 'BR',
92
+ latitude: null,
93
+ longitude: null,
94
+ },
95
+ afterViaCep,
96
+ );
97
+ expect(form.street).toBe('Rua Antônio Bonini');
98
+ expect(form.city).toBe('Atibaia');
99
+ expect(form.uf).toBe('SP');
100
+ expect(Number(form.latitude)).toBeCloseTo(-23.117, 2);
101
+ expect(Number(form.longitude)).toBeCloseTo(-46.543, 2);
102
+ expect(form.street).not.toBe('—');
103
+ expect(form.city).not.toBe('—');
104
+ });
105
+ });
@@ -0,0 +1,132 @@
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Smoke #637 — address-cep-hydrate</title>
6
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
7
+ <style>
8
+ body { font-family: system-ui, sans-serif; margin: 16px; background: #f6f7f9; }
9
+ h1 { font-size: 18px; }
10
+ label { display: block; font-size: 12px; color: #555; margin-top: 8px; }
11
+ input { width: 100%; max-width: 420px; padding: 8px; font-size: 14px; }
12
+ #map { height: 320px; width: 100%; max-width: 720px; margin-top: 12px; border: 1px solid #ccc; }
13
+ .row { display: flex; gap: 12px; flex-wrap: wrap; }
14
+ .col { flex: 1; min-width: 200px; }
15
+ #status { margin-top: 8px; font-size: 13px; }
16
+ .ok { color: #0a7; }
17
+ .err { color: #c00; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <h1>DefaultAddress smoke — CEP hydrate (#637)</h1>
22
+ <p>fluxo: address-cep-hydrate · CEP 12941040 · pin ui-default lookupPostalCode</p>
23
+ <div class="row">
24
+ <div class="col">
25
+ <label>CEP</label>
26
+ <input id="address-cep-input" data-testid="address-cep-input" value="12941040" />
27
+ <label>Rua</label>
28
+ <input id="address-street-input" data-testid="address-street-input" />
29
+ <label>Bairro</label>
30
+ <input id="address-district-input" data-testid="address-district-input" />
31
+ <label>Cidade</label>
32
+ <input id="address-city-input" data-testid="address-city-input" />
33
+ <label>UF</label>
34
+ <input id="address-uf-input" data-testid="address-uf-input" />
35
+ </div>
36
+ <div class="col">
37
+ <label>Latitude</label>
38
+ <input id="address-latitude-input" data-testid="address-latitude-input" value="—" />
39
+ <label>Longitude</label>
40
+ <input id="address-longitude-input" data-testid="address-longitude-input" value="—" />
41
+ <label>Provider</label>
42
+ <input id="address-provider-input" />
43
+ </div>
44
+ </div>
45
+ <div id="status">aguardando lookup…</div>
46
+ <div id="map"></div>
47
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
48
+ <script>
49
+ const TOKEN = '4a073f4bf39f4a2ed19818baf44c4b3d';
50
+ const API = 'https://api.controleonline.com';
51
+ const onlyDigits = v => String(v || '').replace(/\D+/g, '');
52
+ const hasText = v => String(v || '').trim().length > 0;
53
+ const isValidCoord = v => {
54
+ if (v === null || v === undefined || v === '' || v === '—') return false;
55
+ const n = Number(v);
56
+ return Number.isFinite(n) && Math.abs(n) > 0.000001;
57
+ };
58
+ function merge(base, extra, suffix) {
59
+ const next = { ...(base || {}) };
60
+ const fill = (k, val) => { if (!hasText(next[k]) && hasText(val)) next[k] = val; };
61
+ fill('street', extra.street);
62
+ fill('district', extra.district);
63
+ fill('city', extra.city);
64
+ fill('state', extra.state || extra.uf);
65
+ fill('uf', extra.uf || extra.state);
66
+ if (!isValidCoord(next.latitude) && isValidCoord(extra.latitude)) next.latitude = Number(extra.latitude);
67
+ if (!isValidCoord(next.longitude) && isValidCoord(extra.longitude)) next.longitude = Number(extra.longitude);
68
+ if (suffix) next.provider = next.provider ? next.provider + '+' + suffix : suffix;
69
+ return next;
70
+ }
71
+ async function lookupPostalCode(cep) {
72
+ const digits = onlyDigits(cep);
73
+ let payload = { cep: digits };
74
+ try {
75
+ const r = await fetch(API + '/postal-codes/' + digits, {
76
+ headers: {
77
+ accept: 'application/ld+json',
78
+ 'api-token': TOKEN,
79
+ 'app-domain': 'admin.controleonline.com',
80
+ device: 'web-7',
81
+ 'device-type': 'ADMIN'
82
+ }
83
+ });
84
+ if (r.ok) {
85
+ const apiData = await r.json();
86
+ payload = { ...payload, ...apiData, cep: digits };
87
+ }
88
+ } catch (e) {}
89
+ if (!hasText(payload.street) || !hasText(payload.city)) {
90
+ const vr = await fetch('https://viacep.com.br/ws/' + digits + '/json/');
91
+ const json = await vr.json();
92
+ payload = merge(payload, {
93
+ street: json.logradouro, district: json.bairro, city: json.localidade, uf: json.uf, state: json.uf
94
+ }, 'viacep-client');
95
+ }
96
+ if (!isValidCoord(payload.latitude) || !isValidCoord(payload.longitude)) {
97
+ const q = encodeURIComponent([payload.street, payload.district, payload.city, payload.uf, 'Brasil'].filter(Boolean).join(', '));
98
+ const nr = await fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + q, {
99
+ headers: { 'Accept-Language': 'pt-BR' }
100
+ });
101
+ const arr = await nr.json();
102
+ if (arr[0]) payload = merge(payload, { latitude: arr[0].lat, longitude: arr[0].lon }, 'nominatim-client');
103
+ }
104
+ return payload;
105
+ }
106
+ const map = L.map('map').setView([0, 0], 2);
107
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM' }).addTo(map);
108
+ lookupPostalCode('12941040').then(p => {
109
+ document.getElementById('address-street-input').value = p.street || '';
110
+ document.getElementById('address-district-input').value = p.district || '';
111
+ document.getElementById('address-city-input').value = p.city || '';
112
+ document.getElementById('address-uf-input').value = p.uf || p.state || '';
113
+ document.getElementById('address-latitude-input').value = p.latitude;
114
+ document.getElementById('address-longitude-input').value = p.longitude;
115
+ document.getElementById('address-provider-input').value = p.provider || '';
116
+ const lat = Number(p.latitude), lon = Number(p.longitude);
117
+ map.setView([lat, lon], 16);
118
+ L.marker([lat, lon]).addTo(map);
119
+ const ok = p.city === 'Atibaia' && Math.abs(lat + 23.11) < 0.05;
120
+ const st = document.getElementById('status');
121
+ st.className = ok ? 'ok' : 'err';
122
+ st.textContent = ok
123
+ ? 'OK: rua=' + p.street + ' cidade=' + p.city + ' UF=' + (p.uf||p.state) + ' lat/lon preenchidos mapa Atibaia'
124
+ : 'FAIL payload=' + JSON.stringify(p);
125
+ window.__SMOKE637__ = p;
126
+ }).catch(e => {
127
+ document.getElementById('status').className = 'err';
128
+ document.getElementById('status').textContent = String(e);
129
+ });
130
+ </script>
131
+ </body>
132
+ </html>