Explorar o código

finishe now send to b3 and work

gdias hai 2 semanas
pai
achega
635ecc4c2a

+ 283 - 66
src/lib/components/commodities/cpr/EmissionCpr.svelte

@@ -21,9 +21,12 @@
 
   const STATE_FIELD = 'cpr_deliveryPlace_state_acronym';
   const CITY_FIELD = 'cpr_deliveryPlace_city_name';
+  const CITY_IBGE_FIELD = 'cpr_deliveryPlace_ibge_code';
   const ISSUER_STATE_FIELD = 'cpr_issuers_state_acronym';
   const ISSUER_CITY_FIELD = 'cpr_issuers_city_name';
-  const CIDADES_ESTADOS_SRC = 'https://cdn.jsdelivr.net/npm/cidades-estados@1.4.1/cidades-estados.js';
+  const BRASIL_API_BASE = 'https://brasilapi.com.br/api';
+  const UF_ENDPOINT = `${BRASIL_API_BASE}/ibge/uf/v1`;
+  const MUNICIPIOS_ENDPOINT = `${BRASIL_API_BASE}/ibge/municipios/v1`;
   const isBrowser = typeof window !== 'undefined';
   const apiUrl = import.meta.env.VITE_API_URL;
 
@@ -34,6 +37,8 @@
   let cidadesPorEstado = {};
   let estadosCarregando = false;
   let estadosErro = '';
+  let cidadesCarregando = false;
+  let cidadesErro = '';
   let selectedStateValue = '';
   let selectedCityValue = '';
   let availableCities = [];
@@ -44,6 +49,12 @@
   let currentProductValue = '';
   let selectedCommodityOption = null;
   let selectedCommodityId = '';
+  let harvestOptions = [];
+  let harvestLoading = false;
+  let harvestError = '';
+  let lastSelectedState = '';
+  let pendingIbgeValue = '';
+  let deliveryIbgeValue = '';
 
   const sections = [
     {
@@ -64,7 +75,7 @@
         { key: 'cpr_document_deadline_days_number', label: 'Prazo para documentos (dias)' },
         { key: STATE_FIELD, label: 'Estado da entrega' },
         { key: CITY_FIELD, label: 'Cidade da entrega' },
-        // { key: 'cpr_deliveryPlace_ibge_code', label: 'IBGE da entrega' }
+        { key: CITY_IBGE_FIELD, label: 'IBGE da entrega' }
       ]
     },
     {
@@ -75,10 +86,44 @@
         { key: 'cpr_product_name', label: 'Produto' },
         { key: 'cpr_product_harvest', label: 'Safra' },
         { key: 'cpr_product_quantity', label: 'Quantidade' },
-        { key: 'cpr_measure_unit_name', label: 'Unidade de medida' },
-        { key: 'cpr_packaging_way_name', label: 'Empacotamento' },
-        { key: 'cpr_product_status_code', label: 'Status do produto' },
-        { key: 'cpr_production_type_code', label: 'Tipo de produção' }
+        {
+          key: 'cpr_measure_unit_name',
+          label: 'Unidade de medida',
+          type: 'select',
+          options: [
+            { label: 'Selecione...', value: '' },
+            { label: 'QUILO', value: 'QUILO' }
+          ]
+        },
+        {
+          key: 'cpr_packaging_way_name',
+          label: 'Empacotamento',
+          type: 'select',
+          options: [
+            { label: 'Selecione...', value: '' },
+            { label: 'SACA (60 KG)', value: 'SACA(60 KG)' }
+          ]
+        },
+        {
+          key: 'cpr_product_status_code',
+          label: 'Status do produto',
+          type: 'select',
+          options: [
+            { label: 'Selecione...', value: '' },
+            { label: 'A PRODUZIR', value: '0' },
+            { label: 'PRODUZIDO', value: '1' }
+          ]
+        },
+        {
+          key: 'cpr_production_type_code',
+          label: 'Tipo de produção',
+          type: 'select',
+          options: [
+            { label: 'Selecione...', value: '' },
+            { label: 'PRÓPRIA', value: '0' },
+            { label: 'TERCEIROS', value: '1' }
+          ]
+        }
       ],
       textarea: { key: 'cpr_product_description', label: 'Descrição do produto' }
     },
@@ -96,16 +141,99 @@
     // }
   ];
 
+  function synchronizeDeliveryIbge(stateValue, cityValue, derivedValue = '') {
+    const ibgeCode =
+      stateValue && cityValue ? findCityIbgeCode(stateValue, cityValue) : derivedValue || '';
+    if (formData?.[CITY_IBGE_FIELD] !== (ibgeCode ?? '')) {
+      onFieldChange(CITY_IBGE_FIELD, ibgeCode ?? '');
+    }
+  }
+
+  function findCityIbgeCode(stateValue, cityName) {
+    if (!stateValue || !cityName) return '';
+    const list = cidadesPorEstado[stateValue] ?? [];
+    const normalized = normalizeCityName(cityName);
+    const match = list.find((item) => item.normalizedName === normalized || item.name === cityName);
+    return match?.ibgeCode ?? '';
+  }
+
+  function normalizeCityName(name) {
+    if (!name) return '';
+    return name
+      .normalize('NFD')
+      .replace(/\p{Diacritic}/gu, '')
+      .trim()
+      .toUpperCase();
+  }
+
+  async function fetchHarvestOptions() {
+    if (!apiUrl) {
+      harvestError = 'URL da API não configurada.';
+      return;
+    }
+
+    harvestLoading = true;
+    harvestError = '';
+    try {
+      const token = resolveAuthToken();
+      if (!token) {
+        throw new Error('Sessão expirada. Faça login novamente.');
+      }
+
+      const res = await fetch(`${apiUrl}/harvest/list`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        }
+      });
+
+      let payload = null;
+      try {
+        payload = await res.json();
+      } catch (err) {
+        console.error('Falha ao interpretar resposta de /harvest/list:', err);
+      }
+
+      if (res.status === 204) {
+        harvestOptions = [];
+        return;
+      }
+
+      if (!res.ok || payload?.status !== 'ok') {
+        throw new Error(payload?.msg ?? 'Falha ao carregar safras.');
+      }
+
+      harvestOptions = Array.isArray(payload?.data)
+        ? payload.data
+            .map((item) => ({
+              value: item?.harvest_code != null ? String(item.harvest_code).trim() : '',
+              label: (item?.harvest_name ?? '').trim()
+            }))
+            .filter((item) => item.value && item.label)
+        : [];
+    } catch (error) {
+      console.error('Erro ao carregar safras (CPR):', error);
+      harvestError = error?.message ?? 'Falha ao carregar safras.';
+      harvestOptions = [];
+    } finally {
+      harvestLoading = false;
+    }
+  }
+
   onMount(() => {
     if (!isBrowser) return;
-    hydrateCidadesEstadosOptions();
-    void fetchCommodityOptions();
+    hydrateEstadosOptions();
+    void Promise.all([fetchCommodityOptions(), fetchHarvestOptions()]);
   });
 
   $: selectedStateValue = formData?.[STATE_FIELD] ?? '';
   $: selectedCityValue = formData?.[CITY_FIELD] ?? '';
+  $: if ((selectedStateValue ?? '') !== lastSelectedState) {
+    lastSelectedState = selectedStateValue ?? '';
+  }
   $: availableCities =
-    selectedStateValue && cidadesPorEstado[selectedStateValue]
+    selectedStateValue && Array.isArray(cidadesPorEstado[selectedStateValue])
       ? cidadesPorEstado[selectedStateValue]
       : [];
   $: currentProductValue = formData?.[PRODUCT_NAME_FIELD] ?? '';
@@ -113,31 +241,86 @@
     commodityOptions.find((item) => item.name === currentProductValue) ?? null;
   $: selectedCommodityId = selectedCommodityOption?.id ?? '';
   $: synchronizeProductCommodityFields(selectedCommodityOption);
-
-  async function hydrateCidadesEstadosOptions() {
+  $: if (isBrowser && selectedStateValue) {
+    void loadCitiesForState(selectedStateValue);
+  }
+  $: stateCitiesDependency = cidadesPorEstado[lastSelectedState] ?? [];
+  $: deliveryIbgeValue =
+    formData?.[CITY_IBGE_FIELD] && formData[CITY_IBGE_FIELD] !== ''
+      ? formData[CITY_IBGE_FIELD]
+      : pendingIbgeValue || (lastSelectedState && selectedCityValue && stateCitiesDependency
+          ? findCityIbgeCode(lastSelectedState, selectedCityValue)
+          : '');
+  $: synchronizeDeliveryIbge(lastSelectedState, selectedCityValue, deliveryIbgeValue);
+
+  async function hydrateEstadosOptions() {
+    if (!isBrowser) return;
+    estadosCarregando = true;
+    estadosErro = '';
     try {
-      estadosCarregando = true;
-      const ctor = await loadCidadesEstadosScript();
-      const proto = ctor?.prototype;
-      if (!proto?.estados || !proto?.cidades) {
-        throw new Error('Estrutura inválida da biblioteca cidades-estados.');
+      const res = await fetch(UF_ENDPOINT);
+      if (!res.ok) {
+        throw new Error('Não foi possível carregar estados.');
       }
-
+      const data = await res.json();
+      estadosOptions = Array.isArray(data)
+        ? data
+            .map((item) => ({
+              value: (item?.sigla ?? '').trim(),
+              label: (item?.nome ?? '').trim()
+            }))
+            .filter((item) => item.value && item.label)
+            .sort((a, b) => a.label.localeCompare(b.label, 'pt-BR'))
+        : [];
+    } catch (error) {
+      console.error('Erro ao carregar estados (BrasilAPI):', error);
       estadosOptions = [];
-      cidadesPorEstado = {};
+      estadosErro = error?.message ?? 'Não foi possível carregar estados.';
+    } finally {
+      estadosCarregando = false;
+    }
+  }
 
-      proto.estados.forEach(([value, label], index) => {
-        if (!value) return;
-        estadosOptions.push({ value, label });
-        const cidades = proto.cidades?.[index] ?? [];
-        cidadesPorEstado[value] = cidades.filter(Boolean);
-      });
-      estadosErro = '';
+  async function loadCitiesForState(stateAcronym) {
+    if (!isBrowser || !stateAcronym) return;
+    if (Array.isArray(cidadesPorEstado[stateAcronym]) && cidadesPorEstado[stateAcronym].length) {
+      cidadesErro = '';
+      return;
+    }
+
+    cidadesCarregando = true;
+    cidadesErro = '';
+    try {
+      const res = await fetch(`${MUNICIPIOS_ENDPOINT}/${stateAcronym}`);
+      if (!res.ok) {
+        throw new Error('Não foi possível carregar cidades.');
+      }
+      const data = await res.json();
+      const mapped = Array.isArray(data)
+        ? data
+            .map((item) => {
+              const name = (item?.nome ?? '').trim();
+              const ibgeCode =
+                item?.codigo_ibge != null ? String(item.codigo_ibge).trim() : '';
+              const normalizedName = normalizeCityName(name);
+              return { name, ibgeCode, normalizedName };
+            })
+            .filter((item) => item.name && item.ibgeCode)
+            .sort((a, b) => a.name.localeCompare(b.name, 'pt-BR'))
+        : [];
+      cidadesPorEstado = {
+        ...cidadesPorEstado,
+        [stateAcronym]: mapped
+      };
     } catch (error) {
-      console.error('Erro ao carregar cidades-estados:', error);
-      estadosErro = error?.message ?? 'Não foi possível carregar estados e cidades.';
+      console.error('Erro ao carregar cidades (BrasilAPI):', error);
+      cidadesErro = error?.message ?? 'Não foi possível carregar cidades.';
+      cidadesPorEstado = {
+        ...cidadesPorEstado,
+        [stateAcronym]: []
+      };
     } finally {
-      estadosCarregando = false;
+      cidadesCarregando = false;
     }
   }
 
@@ -204,38 +387,6 @@
     return match ? decodeURIComponent(match[1]) : null;
   }
 
-  function loadCidadesEstadosScript() {
-    if (!isBrowser) {
-      return Promise.reject(new Error('Ambiente indisponível para carregar cidades-estados.'));
-    }
-
-    if (window.__cidadesEstadosPromise) {
-      return window.__cidadesEstadosPromise;
-    }
-
-    window.__cidadesEstadosPromise = new Promise((resolve, reject) => {
-      if (window.dgCidadesEstados) {
-        resolve(window.dgCidadesEstados);
-        return;
-      }
-
-      const script = document.createElement('script');
-      script.src = CIDADES_ESTADOS_SRC;
-      script.async = true;
-      script.onload = () => {
-        if (window.dgCidadesEstados) {
-          resolve(window.dgCidadesEstados);
-        } else {
-          reject(new Error('Biblioteca cidades-estados não expôs dgCidadesEstados.'));
-        }
-      };
-      script.onerror = () => reject(new Error('Falha ao carregar biblioteca cidades-estados.'));
-      document.head.appendChild(script);
-    });
-
-    return window.__cidadesEstadosPromise;
-  }
-
   function getValue(key) {
     return formData?.[key] ?? '';
   }
@@ -260,13 +411,34 @@
     return key === CITY_FIELD;
   }
 
+  function isDeliveryIbgeField(key) {
+    return key === CITY_IBGE_FIELD;
+  }
+
   function handleDeliveryStateChange(value) {
     onFieldChange(STATE_FIELD, value);
     onFieldChange(CITY_FIELD, '');
+    onFieldChange(CITY_IBGE_FIELD, '');
+    pendingIbgeValue = '';
+    lastSelectedState = value ?? '';
+    void loadCitiesForState(value);
+  }
+
+  function handleDeliveryCitySelect(event) {
+    const value = event?.currentTarget?.value ?? '';
+    const selectEl = event?.currentTarget;
+    const selectedOption = selectEl?.selectedOptions?.[0] ??
+      (selectEl && selectEl.options ? selectEl.options[selectEl.selectedIndex ?? -1] : null);
+    const ibgeFromOption = selectedOption?.dataset?.ibge ?? '';
+    handleDeliveryCityChange(value, ibgeFromOption);
   }
 
-  function handleDeliveryCityChange(value) {
+  function handleDeliveryCityChange(value, ibgeOverride = '') {
     onFieldChange(CITY_FIELD, value);
+    const stateLookup = lastSelectedState || selectedStateValue;
+    const ibgeCode = ibgeOverride || findCityIbgeCode(stateLookup, value);
+    onFieldChange(CITY_IBGE_FIELD, ibgeCode ?? '');
+    pendingIbgeValue = ibgeCode ?? '';
   }
 
   function isIssuerStateField(key) {
@@ -279,11 +451,13 @@
 
   function getCitiesForState(value) {
     if (!value) return [];
-    return cidadesPorEstado[value] ?? [];
+    const list = cidadesPorEstado[value] ?? [];
+    return list.map((item) => item.name);
   }
 
   function handleRepeatingStateChange(groupKey, index, stateField, cityField, value) {
     onRepeatingFieldChange(groupKey, index, stateField, value);
+    void loadCitiesForState(value);
     if (cityField) {
       onRepeatingFieldChange(groupKey, index, cityField, '');
     }
@@ -291,12 +465,18 @@
 
   function handleRepeatingCityChange(groupKey, index, cityField, value) {
     onRepeatingFieldChange(groupKey, index, cityField, value);
+    const ibgeCode = findCityIbgeCode(selectedStateValue, value);
+    onRepeatingFieldChange(groupKey, index, CITY_IBGE_FIELD, ibgeCode ?? '');
   }
 
   function isProductField(key) {
     return key === PRODUCT_NAME_FIELD;
   }
 
+  function isHarvestField(key) {
+    return key === 'cpr_product_harvest';
+  }
+
   function synchronizeProductCommodityFields(option) {
     if (!option) {
       const hasClassValue = Boolean(formData?.[PRODUCT_CLASS_FIELD]);
@@ -362,16 +542,18 @@
               {:else if isDeliveryCityField(field.key)}
                 <select
                   class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
-                  on:change={(event) => handleDeliveryCityChange(event.currentTarget.value)}
+                  on:change={handleDeliveryCitySelect}
                   bind:value={selectedCityValue}
                   required={requiredFields?.has(field.key)}
-                  disabled={!selectedStateValue || !availableCities.length || !!estadosErro}
+                  disabled={!selectedStateValue || !!estadosErro || (!!cidadesErro && !availableCities.length)}
                 >
                   <option value="">
                     {#if !selectedStateValue}
                       Selecione um estado primeiro
-                    {:else if estadosCarregando}
+                    {:else if cidadesCarregando}
                       Carregando cidades...
+                    {:else if cidadesErro}
+                      Não foi possível carregar cidades
                     {:else if !availableCities.length}
                       Nenhuma cidade disponível
                     {:else}
@@ -379,12 +561,23 @@
                     {/if}
                   </option>
                   {#each availableCities as cidade}
-                    <option value={cidade}>{cidade}</option>
+                    <option value={cidade.name} data-ibge={cidade.ibgeCode}>{cidade.name}</option>
                   {/each}
                 </select>
                 {#if estadosErro}
                   <p class="text-xs text-red-500 mt-1">{estadosErro}</p>
+                {:else if cidadesErro}
+                  <p class="text-xs text-red-500 mt-1">{cidadesErro}</p>
                 {/if}
+              {:else if isDeliveryIbgeField(field.key)}
+                <input
+                  type="text"
+                  class="w-full rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+                  value={deliveryIbgeValue}
+                  readonly
+                  tabindex="-1"
+                  placeholder="IBGE vinculado à cidade"
+                />
               {:else if isProductField(field.key)}
                 <select
                   class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
@@ -414,6 +607,30 @@
                 {#if commoditiesError}
                   <p class="text-xs text-red-500 mt-1">{commoditiesError}</p>
                 {/if}
+              {:else if isHarvestField(field.key)}
+                <select
+                  class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
+                  value={getValue(field.key)}
+                  on:change={handleInput(field.key)}
+                  required={requiredFields?.has(field.key)}
+                  disabled={harvestLoading || (!!harvestError && !harvestOptions.length)}
+                >
+                  <option value="">
+                    {#if harvestLoading}
+                      Carregando safras...
+                    {:else if harvestError && !harvestOptions.length}
+                      Não foi possível carregar
+                    {:else}
+                      Selecione uma safra
+                    {/if}
+                  </option>
+                  {#each harvestOptions as option}
+                    <option value={option.value}>{option.label}</option>
+                  {/each}
+                </select>
+                {#if harvestError}
+                  <p class="text-xs text-red-500 mt-1">{harvestError}</p>
+                {/if}
               {:else if field.type === 'select'}
                 <select
                   class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"

+ 2 - 2
src/routes/cpr/+page.svelte

@@ -204,8 +204,8 @@
           type: 'select',
           options: [
             { label: 'Selecione...', value: '' },
-            { label: 'Penhor', value: 'Penhor' },
-            { label: 'Alienação', value: 'Alienacao' }
+            { label: 'Penhor', value: '6' },
+            { label: 'Alienação', value: '7' }
           ]
         },
         { key: 'cpr_collateral_type_name', label: 'Descrição do colateral' },