gdias 1 неделя назад
Родитель
Сommit
121e26dc47

Разница между файлами не показана из-за своего большого размера
+ 272 - 259
package-lock.json


+ 1 - 1
src/lib/components/DashboardGuard.svelte

@@ -52,7 +52,7 @@
 
     onMount(() => {
         validate();
-        intervalId = setInterval(validate, 60000);
+        intervalId = setInterval(validate, 600000);
     });
 
     onDestroy(() => {

+ 1 - 0
src/lib/components/Tabs.svelte

@@ -39,6 +39,7 @@
         class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-colors"
         on:click={handleClose}
         title="Cancelar criação"
+        aria-label="Cancelar criação"
       >
         <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
           <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>

+ 4 - 2
src/lib/components/commodities/CommodityEditModal.svelte

@@ -58,8 +58,9 @@
           <div class="rounded border border-red-300 bg-red-50 text-red-700 px-3 py-2 text-sm">{error}</div>
         {/if}
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nome *</label>
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="commodity-edit-nome">Nome *</label>
           <input
+            id="commodity-edit-nome"
             type="text"
             bind:value={nome}
             class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
@@ -69,8 +70,9 @@
           {/if}
         </div>
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Flag *</label>
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="commodity-edit-flag">Flag *</label>
           <select
+            id="commodity-edit-flag"
             bind:value={flag}
             class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
           >

+ 19 - 15
src/lib/components/commodities/RegisterCommodity.svelte

@@ -36,26 +36,30 @@
   <form class="space-y-4 p-4" on:submit|preventDefault={submitForm}>
     <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nome *</label>
-        <input
-          type="text"
-          bind:value={nome}
-          class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-400 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-          placeholder="Nome da commodity"
-        />
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+          Nome *
+          <input
+            type="text"
+            bind:value={nome}
+            class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-400 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+            placeholder="Nome da commodity"
+          />
+        </label>
         {#if errors.nome}
           <p class="mt-1 text-sm text-red-600">{errors.nome}</p>
         {/if}
       </div>
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Flag *</label>
-        <select
-          bind:value={flag}
-          class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-400 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-        >
-          <option value="a">Ativo</option>
-          <option value="i">Inativo</option>
-        </select>
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+          Flag *
+          <select
+            bind:value={flag}
+            class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-400 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+          >
+            <option value="a">Ativo</option>
+            <option value="i">Inativo</option>
+          </select>
+        </label>
       </div>
     </div>
 

+ 5 - 2
src/lib/components/commodities/cpr/ContractCpr.svelte

@@ -109,7 +109,7 @@
         <div class={`grid gap-4 ${section.columns === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2'} grid-cols-1`}>
           {#each section.fields as field}
             <div class="space-y-1">
-              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for={`contract-cpr-${field.key}`}>
                 {field.label}
                 {#if requiredFields?.has(field.key)}
                   <span class="text-red-500">*</span>
@@ -117,6 +117,7 @@
               </label>
               {#if field.type === 'date'}
                 <input
+                  id={`contract-cpr-${field.key}`}
                   type="date"
                   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"
                   value={getValue(field.key)}
@@ -125,6 +126,7 @@
                 />
               {:else}
                 <input
+                  id={`contract-cpr-${field.key}`}
                   type="text"
                   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"
                   value={getValue(field.key)}
@@ -139,13 +141,14 @@
     {/each}
 
     <div class="space-y-1">
-      <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+      <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for={notesField.key}>
         {notesField.label}
         {#if requiredFields?.has(notesField.key)}
           <span class="text-red-500">*</span>
         {/if}
       </label>
       <!-- <textarea
+        id={notesField.key}
         rows="3"
         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"
         value={getValue(notesField.key)}

+ 11 - 3
src/lib/components/commodities/cpr/EmissionCpr.svelte

@@ -516,7 +516,7 @@
         <div class={`grid gap-4 ${section.columns === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2'} grid-cols-1`}>
           {#each section.fields as field}
             <div class="space-y-1">
-              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for={`emission-${field.key}`}>
                 {field.label}
                 {#if requiredFields?.has(field.key)}
                   <span class="text-red-500">*</span>
@@ -525,6 +525,7 @@
 
               {#if isDeliveryStateField(field.key)}
                 <select
+                  id={`emission-${field.key}`}
                   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) => handleDeliveryStateChange(event.currentTarget.value)}
                   bind:value={selectedStateValue}
@@ -571,6 +572,7 @@
                 {/if}
               {:else if isDeliveryIbgeField(field.key)}
                 <input
+                  id={`emission-${field.key}`}
                   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}
@@ -633,6 +635,7 @@
                 {/if}
               {:else if field.type === 'select'}
                 <select
+                  id={`emission-${field.key}`}
                   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"
                   value={getValue(field.key)}
                   on:change={handleInput(field.key)}
@@ -644,6 +647,7 @@
                 </select>
               {:else}
                 <input
+                  id={`emission-${field.key}`}
                   type="text"
                   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"
                   value={getValue(field.key)}
@@ -656,13 +660,14 @@
         </div>
         {#if section.textarea}
           <div class="space-y-1">
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for={`emission-${section.textarea.key}`}>
               {section.textarea.label}
               {#if requiredFields?.has(section.textarea.key)}
                 <span class="text-red-500">*</span>
               {/if}
             </label>
             <textarea
+              id={`emission-${section.textarea.key}`}
               rows="3"
               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"
               value={getValue(section.textarea.key)}
@@ -712,7 +717,7 @@
                   <div class={`grid gap-4 ${config.columns === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2'} grid-cols-1`}>
                     {#each config.fields as field}
                       <div class="space-y-1">
-                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for={`emission-${config.key}-${field.key}-${index}`}>
                           {field.label}
                           {#if requiredFields?.has(field.key)}
                             <span class="text-red-500">*</span>
@@ -720,6 +725,7 @@
                         </label>
                         {#if isIssuerStateField(field.key)}
                           <select
+                            id={`emission-${config.key}-${field.key}-${index}`}
                             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={entry[field.key] ?? ''}
                             on:change={(event) => handleRepeatingStateChange(config.key, index, field.key, ISSUER_CITY_FIELD, event.currentTarget.value)}
@@ -762,6 +768,7 @@
                           </select>
                         {:else if field.type === 'select'}
                           <select
+                            id={`emission-${config.key}-${field.key}-${index}`}
                             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"
                             value={entry[field.key] ?? ''}
                             on:change={handleRepeatingInput(config.key, index, field.key)}
@@ -773,6 +780,7 @@
                           </select>
                         {:else}
                           <input
+                            id={`emission-${config.key}-${field.key}-${index}`}
                             type="text"
                             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"
                             value={entry[field.key] ?? ''}

+ 5 - 1
src/lib/components/commodities/cpr/RegisterCpr.svelte

@@ -103,7 +103,7 @@
         <div class={`grid gap-4 ${section.columns === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2'} grid-cols-1`}>
           {#each section.fields as field}
             <div class="space-y-1">
-              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for={`register-cpr-${field.key}`}>
                 {field.label}
                 {#if requiredFields?.has(field.key)}
                   <span class="text-red-500">*</span>
@@ -111,6 +111,7 @@
               </label>
               {#if field.type === 'date'}
                 <input
+                  id={`register-cpr-${field.key}`}
                   type="date"
                   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"
                   value={getValue(field.key)}
@@ -119,6 +120,7 @@
                 />
               {:else if field.type === 'textarea'}
                 <textarea
+                  id={`register-cpr-${field.key}`}
                   rows="3"
                   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"
                   on:input={handleInput(field.key)}
@@ -126,6 +128,7 @@
                 >{getValue(field.key)}</textarea>
               {:else if field.type === 'select'}
                 <select
+                  id={`register-cpr-${field.key}`}
                   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"
                   value={getValue(field.key)}
                   on:change={handleInput(field.key)}
@@ -137,6 +140,7 @@
                 </select>
               {:else}
                 <input
+                  id={`register-cpr-${field.key}`}
                   type="text"
                   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"
                   value={getValue(field.key)}

+ 4 - 2
src/lib/components/settings/ChangeEmail.svelte

@@ -41,8 +41,9 @@
 
     <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Novo e-mail</label>
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="change-email-new">Novo e-mail</label>
         <input
+          id="change-email-new"
           bind:value={email}
           type="email"
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
@@ -53,8 +54,9 @@
         {/if}
       </div>
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Senha atual</label>
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="change-email-password">Senha atual</label>
         <input
+          id="change-email-password"
           bind:value={currentPassword}
           type="password"
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"

+ 6 - 3
src/lib/components/settings/ChangePassword.svelte

@@ -48,8 +48,9 @@
 
     <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
       <div class="relative">
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Senha atual</label>
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="change-password-current">Senha atual</label>
         <input
+          id="change-password-current"
           bind:value={currentPassword}
           type={showCurrent ? 'text' : 'password'}
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
@@ -80,8 +81,9 @@
         {/if}
       </div>
       <div class="relative">
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nova senha</label>
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="change-password-new">Nova senha</label>
         <input
+          id="change-password-new"
           bind:value={newPassword}
           type={showNew ? 'text' : 'password'}
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
@@ -112,8 +114,9 @@
         {/if}
       </div>
       <div class="relative">
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirmar nova senha</label>
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="change-password-confirm">Confirmar nova senha</label>
         <input
+          id="change-password-confirm"
           bind:value={confirmPassword}
           type={showConfirm ? 'text' : 'password'}
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"

+ 13 - 13
src/lib/components/trading/BoletaCompra.svelte

@@ -82,13 +82,13 @@
     <!-- VALOR E QUANTIDADE -->
     <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor/saca</label>
-        <input type="number" min="0" step="0.01" bind:value={valorSaca}
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="buy-valor-saca">Valor/saca</label>
+        <input id="buy-valor-saca" type="number" min="0" step="0.01" bind:value={valorSaca}
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-500" />
       </div>
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Quantidade em saca</label>
-        <input type="number" min="0" step="1" bind:value={quantidade}
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="buy-quantidade">Quantidade em saca</label>
+        <input id="buy-quantidade" type="number" min="0" step="1" bind:value={quantidade}
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-500" />
       </div>
     </div>
@@ -96,10 +96,10 @@
     <!-- REFERÊNCIA E TOTAL -->
     <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">eBRL: {referencia}</label>
+        <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">eBRL: {referencia}</p>
       </div>
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor total: {formatBRL(total)}</label>
+        <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor total: {formatBRL(total)}</p>
       </div>
     </div>
 
@@ -107,26 +107,26 @@
     <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
       <!-- input de validade -->
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Validade</label>
-        <input type="date" bind:value={validadeInput} on:input={handleValidadeInput}
+        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="buy-validade">Validade</label>
+        <input id="buy-validade" type="date" bind:value={validadeInput} on:input={handleValidadeInput}
           class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-500" />
 
         <!-- ESTADO E COMMODITY -->
         <div class="mt-4">
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Estado</label>
-          <input value={state} readonly
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="buy-estado">Estado</label>
+          <input id="buy-estado" value={state} readonly
             class="mt-1 block w-full rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 text-gray-900 dark:text-gray-200 px-3 py-2" />
         </div>
         <div class="mt-4">
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Commodity</label>
-          <input value={commodity} readonly
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="buy-commodity">Commodity</label>
+          <input id="buy-commodity" value={commodity} readonly
             class="mt-1 block w-full rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 text-gray-900 dark:text-gray-200 px-3 py-2" />
         </div>
       </div>
 
       <!-- Calendário inline -->
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Calendário</label>
+        <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">Calendário</p>
         <div bind:this={calendarEl} class="mt-1 w-56 text-sm calendar-override rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 p-2" use:initCalendar></div>
       </div>
     </div>

+ 13 - 10
src/lib/components/trading/BoletaVenda.svelte

@@ -125,12 +125,13 @@
     <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
       <div class="space-y-4">
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor/saca</label>
-          <input type="number" min="0" step="0.01" bind:value={valorSaca} class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500" />
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-price">Valor/saca</label>
+          <input id="sell-price" type="number" min="0" step="0.01" bind:value={valorSaca} class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500" />
         </div>
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Quantidade em saca</label>
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-quantity">Quantidade em saca</label>
           <input
+            id="sell-quantity"
             type="number"
             min="0"
             step="1"
@@ -141,8 +142,9 @@
           <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Quantidade fixa conforme o token selecionado.</p>
         </div>
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Estado</label>
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-state">Estado</label>
           <select
+            id="sell-state"
             class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500 disabled:opacity-60"
             bind:value={selectedState}
             disabled={!!tokenLockedState || !states?.length}
@@ -163,11 +165,12 @@
           {/if}
         </div>
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Commodity</label>
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-commodity">Commodity</label>
           {#if tokensError}
             <div class="text-xs text-red-500 mb-1">{tokensError}</div>
           {/if}
           <select
+            id="sell-commodity"
             class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500 disabled:opacity-60"
             bind:value={selectedTokenId}
             disabled={tokensLoading || !availableTokens.length}
@@ -198,21 +201,21 @@
       </div>
       <div class="space-y-4">
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Validade</label>
-          <input type="date" bind:value={validadeInput} on:input={handleValidadeInput} class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500" />
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-validade">Validade</label>
+          <input id="sell-validade" type="date" bind:value={validadeInput} on:input={handleValidadeInput} class="mt-1 block w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500" />
         </div>
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Calendário</label>
+          <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">Calendário</p>
           <div bind:this={calendarEl} class="mt-1 w-full lg:w-56 text-xs calendar-override rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700/70 text-gray-900 dark:text-gray-200 p-1.5" use:initCalendar></div>
         </div>
       </div>
     </div>
     <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">eBRL: {referencia}</label>
+        <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">eBRL: {referencia}</p>
       </div>
       <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor total: {formatBRL(total)}</label>
+        <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor total: {formatBRL(total)}</p>
       </div>
     </div>
     <div class="flex justify-end">

+ 14 - 2
src/lib/components/trading/ModalBase.svelte

@@ -8,8 +8,20 @@
   function overlayClick(e) { if (e.target === e.currentTarget) close(); }
 </script>
 {#if visible}
-<div class="fixed inset-0 z-50 flex items-center justify-center">
-  <div class="absolute inset-0 bg-black/50" on:click={overlayClick}></div>
+<div class="fixed inset-0 z-50 flex items-center justify-center" role="dialog" aria-modal="true" aria-label={title || 'Janela modal'}>
+  <div
+    class="absolute inset-0 bg-black/50"
+    role="button"
+    tabindex="0"
+    aria-label="Fechar modal"
+    on:click={overlayClick}
+    on:keydown={(event) => {
+      if (event.key === 'Enter' || event.key === ' ') {
+        event.preventDefault();
+        overlayClick(event);
+      }
+    }}
+  ></div>
   <div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-lg mx-4 max-h-[80vh] flex flex-col">
     <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
       <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{title}</h3>

+ 7 - 3
src/lib/components/trading/TabelaOrdens.svelte

@@ -108,6 +108,7 @@
           {#each listaCompraView as o}
           <div
             class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 ${flashCompra.has(o.__idx) ? 'bg-green-100 dark:bg-green-900/30' : ''}`}
+            role="listitem"
             on:mouseenter={(event) => scheduleTooltip(event, o)}
             on:mouseleave={hideTooltip}
           >
@@ -130,10 +131,13 @@
           </div>
         {:else}
           {#each listaVendaView as o}
-          <div
-            class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 cursor-pointer hover:bg-red-50 dark:hover:bg-red-900/40 ${flashVenda.has(o.__idx) ? 'bg-red-100 dark:bg-red-900/30' : ''}`}
+          <button
+            type="button"
+            class={`flex w-full items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 cursor-pointer hover:bg-red-50 dark:hover:bg-red-900/40 ${flashVenda.has(o.__idx) ? 'bg-red-100 dark:bg-red-900/30' : ''}`}
             on:mouseenter={(event) => scheduleTooltip(event, o)}
             on:mouseleave={hideTooltip}
+            on:focus={(event) => scheduleTooltip(event, o)}
+            on:blur={hideTooltip}
             on:click={() =>
               dispatch('selectSellOrder', {
                 valor: o.valor,
@@ -148,7 +152,7 @@
           >
             <span class="text-red-600">{formatBRL(o.valor)}</span>
             <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
-          </div>
+          </button>
           {/each}
         {/if}
       </div>

+ 19 - 1
src/lib/components/ui/PopUpDelete.svelte

@@ -24,7 +24,25 @@
 </script>
 
 {#if visible}
-  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" on:click={backdropClick}>
+  <div
+    class="fixed inset-0 z-50 flex items-center justify-center"
+    aria-modal="true"
+    role="dialog"
+    aria-label={title}
+  >
+    <div
+      class="absolute inset-0 bg-black/50"
+      role="button"
+      tabindex="0"
+      aria-label="Fechar modal"
+      on:click={backdropClick}
+      on:keydown={(event) => {
+        if (event.key === 'Enter' || event.key === ' ') {
+          event.preventDefault();
+          backdropClick(event);
+        }
+      }}
+    ></div>
     <div class="bg-white dark:bg-gray-800 w-full max-w-sm rounded-lg shadow-lg">
       <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
         <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">{title}</h3>

+ 13 - 1
src/lib/components/ui/PopUpToken.svelte

@@ -18,7 +18,19 @@
     aria-modal="true"
     aria-label={title}
   >
-    <div class="absolute inset-0 bg-black/50" on:click={onConfirm}></div>
+    <div
+      class="absolute inset-0 bg-black/50"
+      role="button"
+      tabindex="0"
+      aria-label="Fechar aviso"
+      on:click={onConfirm}
+      on:keydown={(event) => {
+        if (event.key === 'Enter' || event.key === ' ') {
+          event.preventDefault();
+          onConfirm();
+        }
+      }}
+    ></div>
     <div class="relative bg-white dark:bg-gray-800 w-full max-w-sm rounded-lg shadow-lg">
       <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
         <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">{title}</h3>

+ 15 - 2
src/lib/components/wallet/PaymentMenu.svelte

@@ -40,8 +40,21 @@
 </script>
 
 {#if isOpen}
-  <div class="fixed inset-0 z-50" aria-modal="true" role="dialog">
-    <div class="absolute inset-0 bg-black/40" on:click={close} transition:fade></div>
+  <div class="fixed inset-0 z-50" aria-modal="true" role="dialog" aria-label="Selecionar forma de pagamento">
+    <div
+      class="absolute inset-0 bg-black/40"
+      role="button"
+      tabindex="0"
+      aria-label="Fechar seleção de pagamento"
+      on:click={close}
+      on:keydown={(event) => {
+        if (event.key === 'Enter' || event.key === ' ') {
+          event.preventDefault();
+          close();
+        }
+      }}
+      transition:fade
+    ></div>
 
     <div
       class="absolute inset-y-0 right-0 w-full max-w-md bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 shadow-xl flex flex-col"

+ 20 - 5
src/lib/components/wallet/SellMenu.svelte

@@ -33,8 +33,21 @@
 </script>
 
 {#if isOpen}
-  <div class="fixed inset-0 z-50" aria-modal="true" role="dialog">
-    <div class="absolute inset-0 bg-black/40" on:click={close} transition:fade></div>
+  <div class="fixed inset-0 z-50" aria-modal="true" role="dialog" aria-label="Vender EasyCoin">
+    <div
+      class="absolute inset-0 bg-black/40"
+      role="button"
+      tabindex="0"
+      aria-label="Fechar menu de venda"
+      on:click={close}
+      on:keydown={(event) => {
+        if (event.key === 'Enter' || event.key === ' ') {
+          event.preventDefault();
+          close();
+        }
+      }}
+      transition:fade
+    ></div>
 
     <div
       class="absolute inset-y-0 right-0 w-full max-w-md bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 shadow-xl flex flex-col"
@@ -63,8 +76,9 @@
           </p>
         </div>
         <div class="space-y-1">
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Chave Pix</label>
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-pix-key">Chave Pix</label>
           <input
+            id="sell-pix-key"
             class="w-full px-3 py-2 rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
             bind:value={pixKey}
             placeholder="Informe sua chave Pix" />
@@ -72,15 +86,16 @@
 
         <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
           <div class="space-y-1">
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Quantidade (EasyCoin)</label>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="sell-amount">Quantidade (EasyCoin)</label>
             <input
+              id="sell-amount"
               type="number" min="0" step="0.01"
               class="w-full px-3 py-2 rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
               bind:value={amount}
               placeholder="0,00" />
           </div>
           <div class="space-y-1">
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor em Reais</label>
+            <p class="block text-sm font-medium text-gray-700 dark:text-gray-300">Valor em Reais</p>
             <div class="w-full px-3 py-2 rounded-md bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100">
               {formatBRL(totalBRL)}
             </div>

+ 45 - 3
src/routes/cpr/+page.svelte

@@ -290,6 +290,8 @@
   let paymentCountdownIntervalId = null;
   let paymentCopyTimeoutId = null;
   let isCheckingPayment = false;
+  let paymentSuccessOverlayVisible = false;
+  let paymentSuccessMessage = '';
 
   const breadcrumb = [{ label: 'Início' }, { label: 'CPR', active: true }];
   let activeTab = 4;
@@ -613,10 +615,19 @@
   function handlePaymentConfirmationSuccess(payload) {
     const message = payload?.data?.message ?? payload?.message ?? 'Pagamento confirmado com sucesso.';
     submitSuccess = message;
+    paymentSuccessMessage = message;
     paymentError = '';
     paymentStatusMessage = '';
-    resetPaymentTracking({ hideModal: true });
-    void fetchHistory();
+    paymentSuccessOverlayVisible = true;
+    stopPaymentPolling();
+    stopPaymentCountdown();
+    stopPaymentCopyTimeout();
+    removePaymentStateFromStorage();
+    paymentId = null;
+    paymentCode = '';
+    paymentStartedAt = null;
+    paymentCountdownMs = PAYMENT_TIMEOUT_MS;
+    paymentCopyFeedback = '';
   }
 
   function resetPaymentTracking({ hideModal = true } = {}) {
@@ -629,11 +640,20 @@
     paymentStartedAt = null;
     paymentCountdownMs = PAYMENT_TIMEOUT_MS;
     paymentCopyFeedback = '';
+    paymentSuccessOverlayVisible = false;
+    paymentSuccessMessage = '';
     if (hideModal) {
       paymentModalVisible = false;
     }
   }
 
+  function handlePaymentSuccessAcknowledge() {
+    paymentSuccessOverlayVisible = false;
+    paymentModalVisible = false;
+    activeTab = 4;
+    void fetchHistory();
+  }
+
   function handleCancelPaymentFlow() {
     resetPaymentTracking({ hideModal: true });
     paymentStatusMessage = '';
@@ -1211,8 +1231,9 @@
               </div>
 
               <div class="space-y-2">
-                <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Código Pix copia e cola</label>
+                <label class="text-sm font-medium text-gray-700 dark:text-gray-300" for="payment-code-copy">Código Pix copia e cola</label>
                 <textarea
+                  id="payment-code-copy"
                   class="w-full text-sm rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-2 resize-none"
                   rows="4"
                   readonly
@@ -1263,4 +1284,25 @@
       </div>
     </div>
   {/if}
+
+  {#if paymentSuccessOverlayVisible}
+    <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm px-4">
+      <div class="w-full max-w-md rounded-2xl bg-white dark:bg-gray-900 p-6 text-center space-y-4 shadow-2xl">
+        <div class="text-green-600 dark:text-green-400 flex justify-center">
+          <svg class="w-16 h-16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M20 6L9 17l-5-5" />
+          </svg>
+        </div>
+        <h3 class="text-2xl font-semibold text-gray-900 dark:text-gray-50">Pagamento confirmado!</h3>
+        <p class="text-gray-600 dark:text-gray-300">{paymentSuccessMessage || 'A CPR foi emitida com sucesso. Você será redirecionado para o histórico.'}</p>
+        <button
+          type="button"
+          class="w-full rounded-lg bg-green-600 hover:bg-green-700 text-white font-semibold py-2"
+          on:click={handlePaymentSuccessAcknowledge}
+        >
+          Ir para histórico de CPRs
+        </button>
+      </div>
+    </div>
+  {/if}
 </div>

+ 114 - 19
src/routes/dashboard/+page.svelte

@@ -1,24 +1,119 @@
 <script>
-    import Header from '$lib/layout/Header.svelte';
-    import StatsCard from '$lib/components/StatsCard.svelte';
-    import commoditiesIcon from '$lib/assets/icons/sidebar/commodities.svg?raw';
-    import operationsIcon from '$lib/assets/icons/sidebar/operations.svg?raw';
-    import cprIcon from '$lib/assets/icons/sidebar/cpr.svg?raw';
-    import usersIcon from '$lib/assets/icons/sidebar/users.svg?raw';
-
-  const stats = [
-    { title: 'Total em Commodities', value: 'R$ 2.847.520', change: '+12.5% este mês', iconSvg: commoditiesIcon },
-    { title: 'Operações Ativas', value: '47', change: '8 novas hoje', iconSvg: operationsIcon },
-    { title: 'CPRs Emitidas', value: '23', change: '5 pendentes', iconSvg: cprIcon },
-    { title: 'Usuários Ativos', value: '1.245', change: '+32 este mês', iconSvg: usersIcon },
-  ];
-
-  const breadcrumb = [{ label: 'Início', active: true }];
+	import { onMount } from 'svelte';
+	import { get } from 'svelte/store';
+	import Header from '$lib/layout/Header.svelte';
+	import StatsCard from '$lib/components/StatsCard.svelte';
+	import commoditiesIcon from '$lib/assets/icons/sidebar/commodities.svg?raw';
+	import operationsIcon from '$lib/assets/icons/sidebar/operations.svg?raw';
+	import cprIcon from '$lib/assets/icons/sidebar/cpr.svg?raw';
+	import usersIcon from '$lib/assets/icons/sidebar/users.svg?raw';
+	import { authToken, companyId as companyIdStore } from '$lib/utils/stores';
+
+	const apiUrl = import.meta.env.VITE_API_URL;
+	const breadcrumb = [{ label: 'Início', active: true }];
+
+	let summaryLoading = false;
+	let summaryError = '';
+	let summary = null;
+
+	const fallbackStats = [
+		{ title: 'Total em Commodities', value: '-', change: 'Carregando...', iconSvg: commoditiesIcon },
+		{ title: 'Operações Ativas', value: '—', change: 'Aguardando dados', iconSvg: operationsIcon },
+		{ title: 'CPRs Emitidas', value: '—', change: 'Aguardando dados', iconSvg: cprIcon },
+		{ title: 'Usuários Ativos', value: '—', change: 'Aguardando dados', iconSvg: usersIcon }
+	];
+
+	onMount(() => {
+		void fetchCompanySummary();
+	});
+
+	function formatCurrency(value) {
+		return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(value || 0));
+	}
+
+	function formatInteger(value) {
+		return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(
+			Number(value || 0)
+		);
+	}
+
+	async function fetchCompanySummary() {
+		if (!apiUrl) return;
+		summaryLoading = true;
+		summaryError = '';
+		try {
+			const token = get(authToken);
+			const companyId = get(companyIdStore);
+			const headers = {
+				'content-type': 'application/json',
+				...(token ? { Authorization: `Bearer ${token}` } : {})
+			};
+			const bodyPayload = companyId ? { company_id: companyId } : {};
+			const res = await fetch(`${apiUrl}/company/summary`, {
+				method: 'POST',
+				headers,
+				body: JSON.stringify(bodyPayload)
+			});
+			const raw = await res.text();
+			const data = raw ? JSON.parse(raw) : null;
+			if (!res.ok) {
+				throw new Error(data?.msg ?? data?.message ?? 'Falha ao carregar resumo da empresa.');
+			}
+			const resolvedSummary = data?.summary ?? data?.data?.summary ?? data;
+			if (!resolvedSummary || typeof resolvedSummary !== 'object') {
+				throw new Error('Resposta inválida do resumo da empresa.');
+			}
+			summary = resolvedSummary;
+		} catch (err) {
+			console.error('[Dashboard] Erro ao buscar resumo da empresa:', err);
+			summary = null;
+			summaryError = err?.message ?? 'Não foi possível carregar o resumo da empresa.';
+		} finally {
+			summaryLoading = false;
+		}
+	}
+
+	$: stats = summary
+		? [
+			{
+				title: 'Total em tokens disponíveis',
+				value: formatInteger(summary.total_tokens),
+				change: '',
+				iconSvg: commoditiesIcon
+			},
+			{
+				title: 'Operações Ativas',
+				value: formatInteger(summary.active_operations),
+				change: '',
+				iconSvg: operationsIcon
+			},
+			{
+				title: 'CPRs Emitidas',
+				value: formatInteger(summary.total_cprs),
+				change: '',
+				iconSvg: cprIcon
+			},
+			{
+				title: 'Usuários Ativos',
+				value: formatInteger(summary.total_users),
+				change: 'Usuários vinculados à empresa',
+				iconSvg: usersIcon
+			}
+		  ]
+		: fallbackStats;
 </script>
 
 <Header title="Início" subtitle="Visão geral do sistema de commodities" breadcrumb={breadcrumb} />
-<div class="p-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
-  {#each stats as stat}
-    <StatsCard {...stat} />
-  {/each}
+
+<div class="p-8 space-y-4">
+	{#if summaryError}
+		<div class="rounded border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-sm">
+			{summaryError}
+		</div>
+	{/if}
+	<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
+		{#each stats as stat}
+			<StatsCard {...stat} loading={summaryLoading && !summary} />
+		{/each}
+	</div>
 </div>

+ 152 - 8
src/routes/trading/+page.svelte

@@ -123,16 +123,160 @@
       .replace(/^./, (c) => c.toUpperCase());
   }
 
+  const hiddenOrderDetailKeys = new Set([
+    'orderbook_id',
+    'orderbookId',
+    'orderbook_ts',
+    'orderbookTs',
+    'orderbook_is_token',
+    'orderbookIsToken',
+    'orderbook_amount',
+    'orderbookAmount',
+    'orderbook_state',
+    'orderbookState',
+    'orderbook_commodity_type',
+    'orderbookCommodityType',
+    'token_external_id',
+    'tokenExternalId',
+    'status_id',
+    'statusId',
+    'user_id',
+    'userId',
+    'wallet_id',
+    'walletId',
+    'token_id',
+    'tokenId',
+    'chain_id',
+    'chainId',
+    'currency_id',
+    'currencyId',
+    'Currency_id',
+    'CurrencyId',
+    'CURRENCY_ID'
+  ]);
+
+  const orderDetailFieldConfig = [
+    {
+      keys: ['orderbook_flag', 'orderbookFlag'],
+      label: 'Tipo da ordem',
+      formatter: (value) => {
+        const normalized = String(value).toUpperCase();
+        if (normalized === 'SELL' || normalized === 'VENDA') return 'Venda';
+        if (normalized === 'BUY' || normalized === 'COMPRA') return 'Compra';
+        return value;
+      }
+    },
+    {
+      keys: ['cityname', 'city_name', 'cityName'],
+      label: 'Cidade da oferta'
+    },
+    {
+      keys: ['token_commodities_value', 'tokenCommoditiesValue'],
+      label: 'Valor do lote',
+      formatter: (value) => formatBRL(value)
+    },
+    {
+      keys: ['token_commodities_amount', 'tokenCommoditiesAmount'],
+      label: 'Quantidade total',
+      formatter: (value, order) => {
+        const unit = order?.measureunitname ?? order?.measureUnitName ?? order?.raw?.measureunitname ?? '';
+        const formatted = formatQty(value);
+        return unit ? `${formatted} ${unit}` : formatted;
+      }
+    },
+    {
+      keys: ['cprproductquantity', 'cpr_product_quantity', 'cprProductQuantity'],
+      label: 'Quantidade da CPR',
+      formatter: (value, order) => {
+        const unit = order?.measureunitname ?? order?.measureUnitName ?? order?.raw?.measureunitname ?? '';
+        const formatted = formatQty(value);
+        return unit ? `${formatted} ${unit}` : formatted;
+      }
+    },
+    {
+      keys: ['measureunitname', 'measure_unit_name', 'measureUnitName', 'Measureunitname'],
+      label: 'Unidade de medida'
+    },
+    {
+      keys: ['packagingName', 'packaging_name', 'Packagingname', 'PACKAGINGNAME'],
+      label: 'Tipo de embalagem'
+    },
+    {
+      keys: ['maturityDate', 'maturity_date', 'Maturitydate', 'MATURITYDATE'],
+      label: 'Data de vencimento',
+      formatter: (value) => {
+        const date = value ? new Date(value) : null;
+        if (!date || Number.isNaN(date.getTime())) {
+          return value;
+        }
+        return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' }).format(date);
+      }
+    }
+  ];
+
+  const orderDetailKeyTranslations = {};
+  const orderDetailKeyTranslationsNormalized = {};
+  orderDetailFieldConfig.forEach((field) => {
+    const keys = field.keys ?? (field.key ? [field.key] : []);
+    keys
+      .filter(Boolean)
+      .forEach((key) => {
+        orderDetailKeyTranslations[key] = field.label;
+        orderDetailKeyTranslationsNormalized[key.toLowerCase()] = field.label;
+      });
+  });
+
+  function pickOrderDetailValue(source, keys = []) {
+    if (!source || !Array.isArray(keys)) return undefined;
+    for (const key of keys) {
+      if (key != null && Object.prototype.hasOwnProperty.call(source, key)) {
+        const value = source[key];
+        if (value !== undefined) {
+          return value;
+        }
+      }
+    }
+    return undefined;
+  }
+
   function buildOrderDetailEntries(order = {}) {
     const raw = order?.raw && typeof order.raw === 'object' ? order.raw : order;
     if (!raw || typeof raw !== 'object') return [];
-    return Object.entries(raw)
-      .filter(([, value]) => value !== null && value !== undefined && value !== '')
-      .map(([key, value]) => ({
-        key,
-        label: formatOrderDetailKey(key),
-        value: typeof value === 'number' ? value : Array.isArray(value) ? value.join(', ') : String(value)
-      }));
+    const entries = [];
+    const usedKeys = new Set();
+
+    for (const field of orderDetailFieldConfig) {
+      const keys = field.keys ?? (field.key ? [field.key] : []);
+      const rawValue = pickOrderDetailValue(raw, keys);
+      if (rawValue === null || rawValue === undefined || rawValue === '') continue;
+      keys.forEach((key) => usedKeys.add(key));
+      const value = field.formatter
+        ? field.formatter(rawValue, order, raw)
+        : Array.isArray(rawValue)
+          ? rawValue.join(', ')
+          : typeof rawValue === 'number'
+            ? formatQty(rawValue)
+            : String(rawValue);
+      entries.push({ key: field.keys?.[0] ?? field.key, label: field.label, value });
+    }
+
+    Object.entries(raw)
+      .filter(([key]) => !hiddenOrderDetailKeys.has(key) && !usedKeys.has(key))
+      .forEach(([key, value]) => {
+        const formattedValue = value === null || value === undefined || value === ''
+          ? '—'
+          : Array.isArray(value)
+            ? value.join(', ')
+            : typeof value === 'number'
+              ? formatQty(value)
+              : String(value);
+        const label = orderDetailKeyTranslations[key]
+          ?? orderDetailKeyTranslationsNormalized[key?.toLowerCase?.()]
+          ?? formatOrderDetailKey(key);
+        entries.push({ key, label, value: formattedValue });
+      });
+
+    return entries;
   }
 
   function openOrderDetailModal(order = {}) {
@@ -928,7 +1072,7 @@
         token_external_id: tokenExternalId
       };
 
-      const res = await fetch(`${apiUrl}/token/orderbook`, {
+      const res = await fetch(`${apiUrl}  `, {
         method: 'POST',
         headers: {
           'content-type': 'application/json',

+ 168 - 6
src/routes/wallet/+page.svelte

@@ -8,6 +8,7 @@
   import tokensIcon from '$lib/assets/icons/sidebar/tokens.svg?raw';
   import walletIcon from '$lib/assets/icons/sidebar/wallet.svg?raw';
   import { authToken } from '$lib/utils/stores';
+  import ModalBase from '$lib/components/trading/ModalBase.svelte';
 
   const breadcrumb = [{ label: 'Início' }, { label: 'wallet', active: true }];
   const apiUrl = import.meta.env.VITE_API_URL;
@@ -26,6 +27,10 @@
     void fetchWalletTokens();
   });
 
+  let tokenDetailModalVisible = false;
+  let tokenDetailSelected = null;
+  let tokenDetailEntries = [];
+
   function openPayment() {
     isPaymentOpen = true;
   }
@@ -105,6 +110,10 @@
     return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(n || 0));
   }
 
+  function formatCurrencyBRL(n) {
+    return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n || 0));
+  }
+
   function tokenCardLabel(token) {
     return token?.cpr_product_name ?? token?.token_content ?? token?.token_external_id ?? token?.token_id ?? 'EasyToken';
   }
@@ -130,6 +139,108 @@
     const value = candidates.find((item) => item !== undefined && item !== null && item !== '');
     return Number(value ?? 0);
   }
+
+  function formatTokenDetailKey(key = '') {
+    return key
+      .replace(/_/g, ' ')
+      .replace(/([a-z])([A-Z])/g, '$1 $2')
+      .replace(/\s+/g, ' ')
+      .trim()
+      .replace(/^./, (c) => c.toUpperCase());
+  }
+
+  const hiddenTokenDetailKeys = new Set([
+    'token_id',
+    'token_external_id',
+    'token_commodities_amount',
+    'token_commodities_value',
+    'token_uf',
+    'token_city',
+    'token_content',
+    'wallet_id',
+    'chain_id',
+    'commodities_id'
+  ]);
+
+  const tokenDetailFieldConfig = [
+    { key: 'cpr_product_name', label: 'Produto' },
+    {
+      key: 'cpr_product_quantity',
+      label: 'Quantidade do Produto',
+      formatter: (value, token) => {
+        const formattedQuantity = formatToken(value);
+        const unit = token?.cpr_measure_unit_name ?? token?.cpr_packaging_way_name;
+        return unit ? `${formattedQuantity} ${unit}` : formattedQuantity;
+      }
+    },
+    { key: 'cpr_packaging_way_name', label: 'Forma de Embalagem' },
+    { key: 'cpr_measure_unit_name', label: 'Unidade de Medida' },
+    {
+      key: 'cpr_issue_value',
+      label: 'Valor da Emissão',
+      formatter: (value) => formatCurrencyBRL(value)
+    },
+    { key: 'cpr_delivery_place_city_name', label: 'Cidade de Entrega' },
+    { key: 'cpr_delivery_place_state_acronym', label: 'Estado de Entrega' },
+    { key: 'cpr_production_place_name', label: 'Local de Produção' },
+    { key: 'cpr_id', label: 'Número da CPR' },
+    { key: 'user_id', label: 'Código do Usuário' }
+  ];
+
+  const tokenDetailKeyTranslations = tokenDetailFieldConfig.reduce((acc, field) => {
+    if (field.key) {
+      acc[field.key] = field.label;
+    }
+    return acc;
+  }, {});
+
+  function buildTokenDetailEntries(token = {}) {
+    if (!token || typeof token !== 'object') return [];
+
+    const entries = [];
+    const usedKeys = new Set();
+
+    for (const field of tokenDetailFieldConfig) {
+      const rawValue = token?.[field.key];
+      if (rawValue === null || rawValue === undefined || rawValue === '') continue;
+      usedKeys.add(field.key);
+      const formattedValue = field.formatter
+        ? field.formatter(rawValue, token)
+        : typeof rawValue === 'number'
+          ? formatToken(rawValue)
+          : Array.isArray(rawValue)
+            ? rawValue.join(', ')
+            : String(rawValue);
+      entries.push({ key: field.key, label: field.label, value: formattedValue });
+    }
+
+    Object.entries(token)
+      .filter(([key, value]) => !hiddenTokenDetailKeys.has(key) && !usedKeys.has(key) && value !== null && value !== undefined && value !== '')
+      .forEach(([key, value]) => {
+        const formattedValue = typeof value === 'number'
+          ? formatToken(value)
+          : Array.isArray(value)
+            ? value.join(', ')
+            : String(value);
+        const label = tokenDetailKeyTranslations[key] ?? formatTokenDetailKey(key);
+        entries.push({ key, label, value: formattedValue });
+      });
+
+    return entries;
+  }
+
+  function openTokenDetailModal(token) {
+    if (!token) return;
+    tokenDetailSelected = token;
+    tokenDetailEntries = buildTokenDetailEntries(token);
+    tokenDetailModalVisible = true;
+  }
+
+  function closeTokenDetailModal() {
+    tokenDetailModalVisible = false;
+    tokenDetailSelected = null;
+    tokenDetailEntries = [];
+  }
 </script>
 
 <div>
@@ -148,12 +259,18 @@
       {:else if walletTokens.length}
         {#each walletTokens as token (token?.token_external_id ?? token?.token_id ?? token?.cpr_id ?? token)}
           <div class="rounded-lg overflow-hidden">
-            <StatsCard
-              title={tokenCardLabel(token)}
-              value={formatToken(tokenAmount(token))}
-              change={tokenCardSubtitle(token)}
-              iconSvg={tokensIcon}
-            />
+            <button
+              type="button"
+              class="block w-full text-left focus:outline-none"
+              on:click={() => openTokenDetailModal(token)}
+            >
+              <StatsCard
+                title={tokenCardLabel(token)}
+                value={formatToken(tokenAmount(token))}
+                change={tokenCardSubtitle(token)}
+                iconSvg={tokensIcon}
+              />
+            </button>
           </div>
         {/each}
       {:else}
@@ -200,4 +317,49 @@
     onConfirm={(e) => confirmSell(e.detail || e)}
     rateBRLPerEasyCoin={rateBRLPerEasyCoin}
   />
+
+  <ModalBase
+    title="Detalhes do token"
+    visible={tokenDetailModalVisible}
+    onClose={closeTokenDetailModal}
+  >
+    {#if tokenDetailSelected}
+      <div class="space-y-4 text-sm text-gray-800 dark:text-gray-100">
+        <div class="rounded border border-gray-200 dark:border-gray-700 p-3 space-y-2 bg-gray-50 dark:bg-gray-800/60">
+          <div class="text-base font-semibold">{tokenCardLabel(tokenDetailSelected)}</div>
+          <div class="flex justify-between">
+            <span>Quantidade</span>
+            <span>{formatToken(tokenAmount(tokenDetailSelected))}</span>
+          </div>
+          <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400">
+            <span>Local</span>
+            <span>
+              {#if tokenDetailSelected?.token_city || tokenDetailSelected?.token_uf}
+                {tokenDetailSelected?.token_city ?? '—'}/{tokenDetailSelected?.token_uf ?? '—'}
+              {:else}
+                Não informado
+              {/if}
+            </span>
+          </div>
+        </div>
+
+        <div class="max-h-64 overflow-y-auto pr-1 space-y-1">
+          {#if tokenDetailEntries.length === 0}
+            <p class="text-xs text-gray-500">Sem dados adicionais para este token.</p>
+          {:else}
+            <dl class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded">
+              {#each tokenDetailEntries as entry}
+                <div class="flex items-start justify-between px-3 py-2">
+                  <dt class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 w-1/2 pr-2">{entry.label}</dt>
+                  <dd class="text-sm text-gray-900 dark:text-gray-100 w-1/2 text-right break-words">{entry.value}</dd>
+                </div>
+              {/each}
+            </dl>
+          {/if}
+        </div>
+      </div>
+    {:else}
+      <p class="text-sm text-gray-500">Selecione um token para ver detalhes.</p>
+    {/if}
+  </ModalBase>
 </div>

Некоторые файлы не были показаны из-за большого количества измененных файлов