2 Commits b8caa7a476 ... 2e1b6325b8

Tác giả SHA1 Thông báo Ngày
  gdias 2e1b6325b8 add most endpoints 2 tuần trước cách đây
  gdias 159398ed64 add the delete 1 tháng trước cách đây

+ 46 - 18
src/lib/components/DashboardGuard.svelte

@@ -1,27 +1,55 @@
 <script>
-	import { onMount } from 'svelte';
-	import { goto } from '$app/navigation';
-	import { browser } from '$app/environment';
-	import { writable } from 'svelte/store';
+    import { onMount, onDestroy } from 'svelte';
+    import { goto } from '$app/navigation';
+    import { browser } from '$app/environment';
+    import { writable } from 'svelte/store';
 
-	const authorized = writable(false);
+    const authorized = writable(true);
 
     //TODO: When will has a token validation we need to validate the token here
-	//tenho que verificar se realmente vai ser feito essa validacao a cada minuto
-	let flag = 'approved';
+    //tenho que verificar se realmente vai ser feito essa validacao a cada minuto
+    const apiUrl = import.meta.env.VITE_API_URL;
+    let intervalId = null;
 
-	onMount(async () => {
-		if (flag !== 'approved') {
-			goto('/');
-		} else {
-			authorized.set(true);
-		}
-		 setInterval(() => {
-            console.log('passou pelo dashboard guard');
-		 }, 100000);
-	});
+    // async function validate() {
+    //     if (!browser) return;
+    //     const m1 = document.cookie.match(/(?:^|; )auth_token=([^;]+)/);
+    //     const m2 = document.cookie.match(/(?:^|; )company_id=([^;]+)/);
+    //     const token = m1 ? decodeURIComponent(m1[1]) : null;
+    //     const v = m2 ? decodeURIComponent(m2[1]) : null;
+    //     const companyId = v && /^-?\d+$/.test(v) ? Number(v) : v;
+    //     if (!token || companyId == null) {
+    //         authorized.set(false);
+    //         goto('/');
+    //         return;
+    //     }
+    //     try {
+    //         const res = await fetch(`${apiUrl}/auth/validate-token`, {
+    //             method: 'POST',
+    //             headers: {
+    //                 'content-type': 'application/json',
+    //                 'Authorization': `Bearer ${token}`
+    //             },
+    //             body: JSON.stringify({ token, companyId })
+    //         });
+    //         if (!res.ok) throw new Error('invalid');
+    //         authorized.set(true);
+    //     } catch (e) {
+    //         authorized.set(false);
+    //         goto('/');
+    //     }
+    // }
+
+    // onMount(() => {
+    //     validate();
+    //     intervalId = setInterval(validate, 60000);
+    // });
+
+    // onDestroy(() => {
+    //     if (intervalId) clearInterval(intervalId);
+    // });
 </script>
 
 {#if $authorized}
-	<slot />
+    <slot />
 {/if}

+ 46 - 184
src/lib/components/commodities/CommodityEditModal.svelte

@@ -1,96 +1,37 @@
 <script>
-  import { createEventDispatcher, onDestroy } from 'svelte';
+  import { createEventDispatcher } from 'svelte';
 
-  // Control props
   export let visible = false;
   export let title = 'Editar Commodity';
   export let saveText = 'Salvar';
   export let cancelText = 'Cancelar';
-
-  // Initial value
-  // Expected shape:
-  // {
-  //   tipo: 'graos_60kg',
-  //   descricao: '',
-  //   quantidade: number|string,
-  //   preco: number|string,
-  //   vencimentoPagamento: 'YYYY-MM-DD',
-  //   dataLimiteEntrega: 'YYYY-MM-DD',
-  //   cpr: boolean,
-  //   arquivos: File[] | Array<{ name: string, url?: string, type?: string, size?: number }>
-  // }
-  export let value = {};
+  export let value = { nome: '', flag: 'a' };
+  export let loading = false;
+  export let error = '';
 
   const dispatch = createEventDispatcher();
 
-  const defaults = {
-    tipo: 'graos_60kg',
-    descricao: '',
-    quantidade: '',
-    preco: '',
-    vencimentoPagamento: '',
-    dataLimiteEntrega: '',
-    cpr: false,
-    arquivos: []
-  };
-
-  let local = { ...defaults };
+  let nome = '';
+  let flag = 'a';
+  let localError = { nome: '' };
 
-  // Manage object URLs for previews
-  let objectUrls = [];
-  onDestroy(() => {
-    objectUrls.forEach((u) => URL.revokeObjectURL(u));
-    objectUrls = [];
-  });
-
-  // Sync when modal opens
   $: if (visible) {
-    objectUrls.forEach((u) => URL.revokeObjectURL(u));
-    objectUrls = [];
-    local = {
-      ...defaults,
-      ...value,
-      arquivos: value?.arquivos ? [...value.arquivos] : []
-    };
-  }
-
-  function isFileLike(item) {
-    return typeof File !== 'undefined' && item instanceof File;
-  }
-
-  function filePreview(item) {
-    try {
-      if (isFileLike(item)) {
-        const url = URL.createObjectURL(item);
-        objectUrls.push(url);
-        return url;
-      }
-      if (item && item.url) return item.url;
-      return null;
-    } catch {
-      return null;
-    }
+    nome = value?.nome ?? '';
+    flag = value?.flag ?? 'a';
+    localError = { nome: '' };
   }
 
-  function formatSize(bytes) {
-    if (!bytes && bytes !== 0) return '';
-    const units = ['B', 'KB', 'MB', 'GB'];
-    let i = 0;
-    let size = bytes;
-    while (size >= 1024 && i < units.length - 1) {
-      size /= 1024;
-      i++;
+  function validate() {
+    localError = { nome: '' };
+    if (!nome.trim()) {
+      localError.nome = 'Informe o nome da commodity';
     }
-    return `${size.toFixed(1)} ${units[i]}`;
-  }
-
-  function handleFilesChange(event) {
-    const files = Array.from(event.currentTarget.files || []);
-    local.arquivos = files;
+    return !localError.nome;
   }
 
   function handleSave() {
-    dispatch('save', { value: local });
+    if (!validate()) return;
+    dispatch('save', { value: { nome: nome.trim(), flag } });
   }
 
   function handleCancel() {
@@ -100,7 +41,7 @@
 
 {#if visible}
   <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-    <div class="bg-white dark:bg-gray-800 w-full max-w-3xl rounded-lg shadow-lg">
+    <div class="bg-white dark:bg-gray-800 w-full max-w-md rounded-lg shadow-lg">
       <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
         <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</h3>
         <button
@@ -112,116 +53,31 @@
         </button>
       </div>
 
-      <div class="p-6 space-y-5 max-h-[75vh] overflow-y-auto">
-        <!-- Tipo -->
+      <div class="p-6 space-y-4">
+        {#if error}
+          <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 mb-1" for="tipo">Tipo</label>
-          <select
-            id="tipo"
-            class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
-            bind:value={local.tipo}
-          >
-            <option value="graos_60kg">Grãos em sacas de 60kg</option>
-          </select>
-        </div>
-
-        <!-- Descrição, Quantidade, Preço -->
-        <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
-          <div>
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="descricao">Descrição</label>
-            <input
-              id="descricao"
-              class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
-              bind:value={local.descricao}
-              placeholder="Descreva a commodity"
-            />
-          </div>
-          <div>
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="quantidade">Quantidade</label>
-            <input
-              id="quantidade"
-              type="number"
-              class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
-              bind:value={local.quantidade}
-              min="0"
-              step="1"
-              placeholder="0"
-            />
-          </div>
-          <div>
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="preco">Preço (R$)</label>
-            <input
-              id="preco"
-              type="number"
-              class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
-              bind:value={local.preco}
-              min="0"
-              step="0.01"
-              placeholder="0,00"
-            />
-          </div>
-        </div>
-
-        <!-- Datas -->
-        <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 mb-1" for="vencimentoPagamento">Vencimento do Pagamento</label>
-            <input
-              id="vencimentoPagamento"
-              type="date"
-              class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
-              bind:value={local.vencimentoPagamento}
-            />
-          </div>
-          <div>
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="dataLimiteEntrega">Data Limite da Entrega</label>
-            <input
-              id="dataLimiteEntrega"
-              type="date"
-              class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
-              bind:value={local.dataLimiteEntrega}
-            />
-          </div>
-        </div>
-
-        <!-- CPR Checkbox -->
-        <div class="flex items-center gap-2">
+          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nome *</label>
           <input
-            id="cpr"
-            type="checkbox"
-            class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
-            bind:checked={local.cpr}
+            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"
           />
-          <label for="cpr" class="text-sm text-gray-700 dark:text-gray-300">CPR</label>
+          {#if localError.nome}
+            <p class="mt-1 text-sm text-red-600">{localError.nome}</p>
+          {/if}
         </div>
-
-        <!-- Arquivos -->
         <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="arquivos">Anexos</label>
-          <input
-            id="arquivos"
-            type="file"
-            class="block w-full text-sm text-gray-700 dark:text-gray-200 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
-            multiple
-            on:change={handleFilesChange}
-          />
-          {#if local.arquivos && local.arquivos.length}
-            <ul class="mt-3 space-y-2">
-              {#each local.arquivos as item}
-                <li class="flex items-center gap-3 p-2 rounded-md border border-gray-200 dark:border-gray-700">
-                  {#if (isFileLike(item) && item.type?.startsWith('image')) || (!isFileLike(item) && item.type?.startsWith?.('image'))}
-                    {#if filePreview(item)}
-                      <img src={filePreview(item)} alt={isFileLike(item) ? item.name : item.name} class="w-10 h-10 object-cover rounded" />
-                    {/if}
-                  {/if}
-                  <div class="flex-1 min-w-0">
-                    <div class="text-sm text-gray-800 dark:text-gray-100 truncate">{isFileLike(item) ? item.name : item.name}</div>
-                    <div class="text-xs text-gray-500 dark:text-gray-400">{isFileLike(item) ? formatSize(item.size) : (item.size ? formatSize(item.size) : (item.type || ''))}</div>
-                  </div>
-                </li>
-              {/each}
-            </ul>
-          {/if}
+          <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 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+          >
+            <option value="a">Ativo</option>
+            <option value="i">Inativo</option>
+            <option value="b">Bloqueado</option>
+          </select>
         </div>
       </div>
 
@@ -229,14 +85,20 @@
         <button
           class="px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
           on:click={handleCancel}
+          disabled={loading}
         >
           {cancelText}
         </button>
         <button
-          class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white"
+          class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60"
           on:click={handleSave}
+          disabled={loading}
         >
-          {saveText}
+          {#if loading}
+            Salvando...
+          {:else}
+            {saveText}
+          {/if}
         </button>
       </div>
     </div>

+ 45 - 136
src/lib/components/commodities/RegisterCommodity.svelte

@@ -1,76 +1,41 @@
 <script>
-    import { createEventDispatcher } from 'svelte';
-    let tipo = "";
-    let descricao = "";
-    let quantidade = "";
-    let preco = "";
-    let nome = "";
-    let registro = "";
-    let vencimentoPagamento = "";
-    let dataLimiteEntrega = "";
-    let cpr = false;
-    let arquivos = [];
-    let fileInput;
-    const dispatch = createEventDispatcher();
+  import { createEventDispatcher } from 'svelte';
 
-    function addFiles(files) {
-      if (!files || !files.length) return;
-      arquivos = [...arquivos, ...Array.from(files)];
-    }
+  export let loading = false;
+  export let resetToken = 0;
 
-    function handleFiles(event) {
-      addFiles(event?.target?.files);
-    }
+  const dispatch = createEventDispatcher();
 
-    function handleDrop(event) {
-      event.preventDefault();
-      addFiles(event?.dataTransfer?.files);
-    }
+  let nome = '';
+  let flag = 'a';
+  let errors = { nome: '' };
+  let lastResetToken = resetToken;
 
-    function handleDragOver(event) {
-      event.preventDefault();
-    }
+  $: if (resetToken !== lastResetToken) {
+    lastResetToken = resetToken;
+    nome = '';
+    flag = 'a';
+    errors = { nome: '' };
+  }
 
-    function removeFile(index) {
-      arquivos = arquivos.filter((_, i) => i !== index);
+  function validate() {
+    errors = { nome: '' };
+    if (!nome.trim()) {
+      errors.nome = 'Informe o nome da commodity';
     }
+    return !errors.nome;
+  }
 
-    function submitForm() {
-      const payload = {
-        tipo,
-        descricao,
-        quantidade,
-        preco,
-        vencimentoPagamento,
-        dataLimiteEntrega,
-        cpr,
-        arquivos
-      };
-      dispatch('submit', payload);
-    }
-  </script>
-  
-  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
-  <form class="space-y-4 p-4" on:submit|preventDefault={submitForm}>
-    
+  function submitForm() {
+    if (!validate()) return;
+    dispatch('submit', { nome: nome.trim(), flag });
+  }
+</script>
 
-    <!-- Tipo -->
+<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
+  <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">Tipo *</label>
-        <select
-          bind:value={tipo}
-          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"
-        >
-          <option value="">Selecione...</option>
-          <option value="graos_60kg">Grãos em sacas de 60kg</option>
-        </select>
-      </div>
-    </div>
-
-    <!-- Descrição, Quantidade, Preço -->
-    <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
-            <div>
         <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nome *</label>
         <input
           type="text"
@@ -78,90 +43,34 @@
           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"
         />
+        {#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">Descrição</label>
-        <input
-          bind:value={descricao}
+        <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"
-          placeholder="Descreva a commodity"
-        />
-      </div>
-      
-    </div>
-          <div>
-        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Numero de registro</label>
-        <input
-          bind:value={registro}
-          class="mt-1 block w-1/3 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="Numero de registro da commodity"
-        />
-      </div>
-
-    <!-- Vencimento do Pagamento, Data Limite da Entrega, CPR -->
-    <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
-      
-      <div class="flex items-end">
-        <!-- <label class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
-          <input type="checkbox" bind:checked={cpr} class="accent-blue-600" />
-          CPR
-        </label> -->
-      </div>
-    </div>
-
-    <!-- Anexos / Arquivos -->
-    <!-- <div>
-      <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Anexos</label>
-      <div
-        class="mt-1 rounded border-2 border-dashed border-gray-300 dark:border-gray-600 p-6 text-center hover:border-blue-400 transition-colors"
-        on:drop={handleDrop}
-        on:dragover={handleDragOver}
-      >
-        <p class="text-sm text-gray-600 dark:text-gray-400">Arraste e solte os ficheiros aqui</p>
-        <p class="text-sm text-gray-600 dark:text-gray-400">ou</p>
-        <button
-          type="button"
-          class="mt-2 inline-flex items-center gap-2 rounded bg-blue-600 px-3 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
-          on:click={() => fileInput.click()}
         >
-          Escolher ficheiros
-        </button>
-        <input
-          id="file-input"
-          bind:this={fileInput}
-          type="file"
-          multiple
-          accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx,.csv"
-          on:change={handleFiles}
-          class="hidden"
-        />
+          <option value="a">Ativo</option>
+          <option value="i">Inativo</option>
+        </select>
       </div>
-      {#if arquivos.length}
-        <ul class="mt-3 divide-y divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700">
-          {#each arquivos as file, i}
-            <li class="flex items-center justify-between px-3 py-2 text-sm">
-              <span class="text-gray-700 dark:text-gray-300 truncate">{file.name} ({Math.round(file.size / 1024)} KB)</span>
-              <button
-                type="button"
-                class="ml-4 text-red-600 hover:text-red-700"
-                on:click={() => removeFile(i)}
-              >
-                Remover
-              </button>
-            </li>
-          {/each}
-        </ul>
-      {/if}
-    </div> -->
+    </div>
 
     <div class="mt-6 flex justify-end">
       <button
         type="submit"
-        class="inline-flex items-center rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
+        class="inline-flex items-center rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
+        disabled={loading}
       >
-        Salvar
+        {#if loading}
+          Salvando...
+        {:else}
+          Salvar
+        {/if}
       </button>
     </div>
-
   </form>
-  </div>
+</div>

+ 22 - 1
src/lib/components/settings/ChangeEmail.svelte

@@ -2,10 +2,21 @@
   import { createEventDispatcher } from 'svelte';
   const dispatch = createEventDispatcher();
 
+  export let loading = false;
+  export let resetToken = 0;
+
   let email = '';
   let currentPassword = '';
   let errors = { email: '', currentPassword: '' };
 
+  let lastResetToken = resetToken;
+  $: if (resetToken !== lastResetToken) {
+    lastResetToken = resetToken;
+    email = '';
+    currentPassword = '';
+    errors = { email: '', currentPassword: '' };
+  }
+
   function validate() {
     errors = { email: '', currentPassword: '' };
     const emailRegex = /[^@\s]+@[^@\s]+\.[^@\s]+/;
@@ -56,7 +67,17 @@
     </div>
 
     <div class="flex justify-end">
-      <button type="submit" class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white">Salvar e-mail</button>
+      <button
+        type="submit"
+        class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60"
+        disabled={loading}
+      >
+        {#if loading}
+          Salvando...
+        {:else}
+          Salvar e-mail
+        {/if}
+      </button>
     </div>
   </form>
 </div>

+ 92 - 7
src/lib/components/settings/ChangePassword.svelte

@@ -2,11 +2,26 @@
   import { createEventDispatcher } from 'svelte';
   const dispatch = createEventDispatcher();
 
+  export let loading = false;
+  export let resetToken = 0;
+
   let currentPassword = '';
   let newPassword = '';
   let confirmPassword = '';
+  let showCurrent = false;
+  let showNew = false;
+  let showConfirm = false;
   let errors = { currentPassword: '', newPassword: '', confirmPassword: '' };
 
+  let lastResetToken = resetToken;
+  $: if (resetToken !== lastResetToken) {
+    lastResetToken = resetToken;
+    currentPassword = '';
+    newPassword = '';
+    confirmPassword = '';
+    errors = { currentPassword: '', newPassword: '', confirmPassword: '' };
+  }
+
   function validate() {
     errors = { currentPassword: '', newPassword: '', confirmPassword: '' };
     if (!currentPassword || currentPassword.length < 6) {
@@ -32,38 +47,98 @@
     <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">Alterar senha</h3>
 
     <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
-      <div>
+      <div class="relative">
         <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Senha atual</label>
         <input
           bind:value={currentPassword}
-          type="password"
+          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"
           placeholder="********"
         />
+        <button
+          type="button"
+          class="absolute right-2 top-9 text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-100"
+          on:click={() => showCurrent = !showCurrent}
+          aria-label={showCurrent ? 'Ocultar senha atual' : 'Mostrar senha atual'}
+          title={showCurrent ? 'Ocultar senha atual' : 'Mostrar senha atual'}
+        >
+          {#if showCurrent}
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M17.94 17.94A10.94 10.94 0 0 0 21 12c-1.5-3.5-4.5-6-9-6a9.77 9.77 0 0 0-2.31.27" />
+              <path d="M6.06 6.06A10.94 10.94 0 0 0 3 12c1.5 3.5 4.5 6 9 6a9.77 9.77 0 0 0 2.31-.27" />
+              <line x1="1" y1="1" x2="23" y2="23" />
+            </svg>
+          {:else}
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12Z" />
+              <circle cx="12" cy="12" r="3" />
+            </svg>
+          {/if}
+        </button>
         {#if errors.currentPassword}
           <p class="mt-1 text-sm text-red-600">{errors.currentPassword}</p>
         {/if}
       </div>
-      <div>
+      <div class="relative">
         <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nova senha</label>
         <input
           bind:value={newPassword}
-          type="password"
+          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"
           placeholder="********"
         />
+        <button
+          type="button"
+          class="absolute right-2 top-9 text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-100"
+          on:click={() => showNew = !showNew}
+          aria-label={showNew ? 'Ocultar nova senha' : 'Mostrar nova senha'}
+          title={showNew ? 'Ocultar nova senha' : 'Mostrar nova senha'}
+        >
+          {#if showNew}
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M17.94 17.94A10.94 10.94 0 0 0 21 12c-1.5-3.5-4.5-6-9-6a9.77 9.77 0 0 0-2.31.27" />
+              <path d="M6.06 6.06A10.94 10.94 0 0 0 3 12c1.5 3.5 4.5 6 9 6a9.77 9.77 0 0 0 2.31-.27" />
+              <line x1="1" y1="1" x2="23" y2="23" />
+            </svg>
+          {:else}
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12Z" />
+              <circle cx="12" cy="12" r="3" />
+            </svg>
+          {/if}
+        </button>
         {#if errors.newPassword}
           <p class="mt-1 text-sm text-red-600">{errors.newPassword}</p>
         {/if}
       </div>
-      <div>
+      <div class="relative">
         <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirmar nova senha</label>
         <input
           bind:value={confirmPassword}
-          type="password"
+          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"
           placeholder="********"
         />
+        <button
+          type="button"
+          class="absolute right-2 top-9 text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-100"
+          on:click={() => showConfirm = !showConfirm}
+          aria-label={showConfirm ? 'Ocultar confirmação de senha' : 'Mostrar confirmação de senha'}
+          title={showConfirm ? 'Ocultar confirmação de senha' : 'Mostrar confirmação de senha'}
+        >
+          {#if showConfirm}
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M17.94 17.94A10.94 10.94 0 0 0 21 12c-1.5-3.5-4.5-6-9-6a9.77 9.77 0 0 0-2.31.27" />
+              <path d="M6.06 6.06A10.94 10.94 0 0 0 3 12c1.5 3.5 4.5 6 9 6a9.77 9.77 0 0 0 2.31-.27" />
+              <line x1="1" y1="1" x2="23" y2="23" />
+            </svg>
+          {:else}
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12Z" />
+              <circle cx="12" cy="12" r="3" />
+            </svg>
+          {/if}
+        </button>
         {#if errors.confirmPassword}
           <p class="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
         {/if}
@@ -71,7 +146,17 @@
     </div>
 
     <div class="flex justify-end">
-      <button type="submit" class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white">Salvar senha</button>
+      <button
+        type="submit"
+        class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60"
+        disabled={loading}
+      >
+        {#if loading}
+          Salvando...
+        {:else}
+          Salvar senha
+        {/if}
+      </button>
     </div>
   </form>
 </div>

+ 51 - 2
src/lib/components/users/UserCreateModal.svelte

@@ -6,6 +6,9 @@
   export let title = 'Novo Usuário';
   export let saveText = 'Criar usuário';
   export let cancelText = 'Cancelar';
+  export let loading = false;
+  export let errorMessage = '';
+  export let resetToken = 0;
 
   let name = '';
   let email = '';
@@ -22,6 +25,29 @@
   let kyc = false;
 
   let errors = { name: '', email: '', password: '', confirmPassword: '' };
+  let lastResetToken = 0;
+
+  function resetForm() {
+    name = '';
+    email = '';
+    password = '';
+    confirmPassword = '';
+    phone = '';
+    cpf = '';
+    birthdate = '';
+    address = '';
+    city = '';
+    state = '';
+    zip = '';
+    country = 'BR';
+    kyc = false;
+    errors = { name: '', email: '', password: '', confirmPassword: '' };
+  }
+
+  $: if (resetToken !== lastResetToken) {
+    lastResetToken = resetToken;
+    resetForm();
+  }
 
   function validate() {
     errors = { name: '', email: '', password: '', confirmPassword: '' };
@@ -35,12 +61,35 @@
 
   function handleSave() {
     if (!validate()) return;
-    const birthdateNum = birthdate ? Number(birthdate.replace(/-/g, '')) : undefined;
+    let birthdateUnix = undefined;
+    if (birthdate) {
+      const dateObj = new Date(`${birthdate}T00:00:00Z`);
+      if (!Number.isNaN(dateObj.getTime())) {
+        birthdateUnix = Math.floor(dateObj.getTime() / 1000);
+      }
+    }
     const kycNum = kyc ? 1 : 0;
-    dispatch('submit', { name, email, password, phone, address, city, state, zip, country, kyc: kycNum, birthdate: birthdateNum, cpf });
+    const sanitizedCpf = cpf ? cpf.replace(/\D+/g, '') : undefined;
+    const payload = {
+      name: name?.trim() ?? '',
+      username: name?.trim() ?? '',
+      email: email?.trim() ?? '',
+      password,
+      phone: phone?.trim() ?? '',
+      address: address?.trim() ?? '',
+      city: city?.trim() ?? '',
+      state: state?.trim() ?? '',
+      zip: zip?.trim() ?? '',
+      country: country?.trim() ?? '',
+      kyc: kycNum,
+      birthdate: birthdateUnix,
+      cpf: sanitizedCpf
+    };
+    dispatch('submit', payload);
   }
 
   function handleCancel() {
+    resetForm();
     dispatch('cancel');
   }
 </script>

+ 34 - 1
src/lib/layout/SideBar.svelte

@@ -1,5 +1,6 @@
 <script>
   import { page } from '$app/stores';
+  import { goto } from '$app/navigation';
   import dashboardIcon from '$lib/assets/icons/sidebar/dashboard.svg?raw';
   import cprIcon from '$lib/assets/icons/sidebar/cpr.svg?raw';
   import commoditiesIcon from '$lib/assets/icons/sidebar/commodities.svg?raw';
@@ -12,7 +13,7 @@
   import settingsIcon from '$lib/assets/icons/sidebar/settings.svg?raw';
   import logo from '$lib/assets/logo2.png';
   import { onMount } from 'svelte';
-  import { easyCoinBalance } from '$lib/utils/stores.js';
+  import { easyCoinBalance, authToken, companyId } from '$lib/utils/stores.js';
 
   const navItems = [
     { id: 'dashboard', href: '/dashboard', label: 'Início', icon: dashboardIcon },
@@ -44,6 +45,25 @@
     }
   }
 
+  function clearAllCookies() {
+    if (typeof document === 'undefined') return;
+    const cookies = document.cookie.split(';');
+    for (const cookie of cookies) {
+      const eqIndex = cookie.indexOf('=');
+      const name = (eqIndex > -1 ? cookie.slice(0, eqIndex) : cookie).trim();
+      if (!name) continue;
+      document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`;
+    }
+  }
+
+  function handleLogout() {
+    clearAllCookies();
+    authToken.set(null);
+    companyId.set(null);
+    closeSidebar();
+    goto('/');
+  }
+
   onMount(() => {
     syncWithViewport();
     const onResize = () => syncWithViewport();
@@ -80,6 +100,12 @@
     <h1 class="text-lg font-semibold text-gray-800 dark:text-gray-100 ml-2">TooEasy Commodities</h1>
     <!-- Fechar removido: o botão fixo acima faz o toggle no mobile -->
   </div>
+  <div class="mx-4 mt-4 rounded-md border border-red-500 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-200 px-3 py-2 text-sm font-semibold flex items-center gap-2 animate-pulse">
+    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
+      <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M10.29 3.86l-8.17 14.14A1 1 0 0 0 3 19h18a1 1 0 0 0 .88-1.5L13.71 3.86a1 1 0 0 0-1.71 0Z" />
+    </svg>
+    Aplicação em desenvolvimento
+  </div>
   <div class="flex-1 overflow-auto">
     <ul class="mt-4">
       {#each navItems as item}
@@ -106,6 +132,13 @@
       <div class="font-medium text-gray-900 dark:text-gray-100">{formatCoin($easyCoinBalance)}</div>
     </div>
   </div>
+
+  <button
+    class="m-2 px-4 py-2 rounded-md bg-gray-900 text-white text-sm font-medium transition-colors duration-200 hover:bg-red-600 hover:text-white dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-red-400 dark:hover:text-white"
+    on:click={handleLogout}
+  >
+    Sair
+  </button>
 </aside>
 
 {#if isOpen}

+ 35 - 7
src/routes/+page.svelte

@@ -10,6 +10,8 @@
   let remember = true;
   let loading = false;
   let error = '';
+  let success = '';
+  let successTimeout;
 
   function setDark(enabled) {
     if (enabled) document.documentElement.classList.add('dark');
@@ -33,8 +35,21 @@
       if (e.key === 'darkmode') applyDarkModeFromStorage();
     };
     window.addEventListener('storage', onStorage);
+    try {
+      const stored = localStorage.getItem('registrationSuccessMessage');
+      if (stored) {
+        success = stored;
+        localStorage.removeItem('registrationSuccessMessage');
+        successTimeout = setTimeout(() => success = '', 4000);
+      }
+    } catch (err) {
+      console.warn('Falha ao recuperar mensagem de sucesso:', err);
+    }
     onDestroy(() => {
       window.removeEventListener('storage', onStorage);
+      if (successTimeout) {
+        clearTimeout(successTimeout);
+      }
     });
   });
 
@@ -51,7 +66,7 @@
         throw new Error('Ação disponível apenas no navegador.');
       }
 
-      const res = await fetch(`${apiUrl}/auth/login`, {
+      const res = await fetch(`${apiUrl}/login`, {
         method: 'POST',
         headers: { 'content-type': 'application/json' },
         body: JSON.stringify({ email, password })
@@ -64,14 +79,16 @@
           if (err?.message) msg = err.message;
         } catch {}
         throw new Error(msg);
-      } else {
-        console.log(res)
       }
 
-      const data = await res.json();
-      console.log(data)
-      const token = data?.token;
-      const companyIdFromApi = data?.companyId;
+      const payload = await res.json();
+      const statusOk = payload?.status === 'ok' && payload?.code === 'S_OK';
+      if (!statusOk) {
+        throw new Error(payload?.msg ?? 'Falha ao autenticar.');
+      }
+
+      const token = payload?.data?.token;
+      const companyIdFromApi = payload?.data?.company_id ?? payload?.data?.companyId;
       if (!token) {
         throw new Error('Resposta inválida do servidor.');
       }
@@ -89,6 +106,14 @@
       }
       authToken.set(token);
 
+      if (browser) {
+        try {
+          localStorage.removeItem('registrationSuccessMessage');
+        } catch (err) {
+          console.warn('Falha ao apagar mensagem de sucesso:', err);
+        }
+      }
+
       await goto('/dashboard');
     } catch (err) {
       error = err?.message ?? 'Falha ao autenticar, tente novamente.';
@@ -107,6 +132,9 @@
       {#if error}
         <div class="mb-4 rounded border border-red-300 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{error}</div>
       {/if}
+      {#if success}
+        <div class="mb-4 rounded border border-green-300 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 px-3 py-2 text-sm">{success}</div>
+      {/if}
 
       <form on:submit|preventDefault={onSubmit} class="space-y-4">
         <div>

+ 226 - 39
src/routes/commodities/+page.svelte

@@ -1,10 +1,14 @@
 <script>
+  import { onMount } from 'svelte';
   import Header from '$lib/layout/Header.svelte';
   import Tables from '$lib/components/Tables.svelte';
   import RegisterCommodity from '$lib/components/commodities/RegisterCommodity.svelte';
   import Tabs from '$lib/components/Tabs.svelte';
   import CommodityEditModal from '$lib/components/commodities/CommodityEditModal.svelte';
   import ConfirmModal from '$lib/components/ui/PopUpDelete.svelte';
+  import { authToken } from '$lib/utils/stores';
+
+  const apiUrl = import.meta.env.VITE_API_URL;
 
   const breadcrumb = [{ label: 'Início' }, { label: 'Commodities', active: true }];
 
@@ -12,13 +16,19 @@
 
   let columns = [
     { key: "nome", label: "Nome" },
-    { key: "status", label: "Status" }
+    { key: "flag", label: "Flag" }
   ];
 
-  let data = [
-    { nome: "Commodity A", status: "Disponível" },
-    { nome: "Commodity B", status: "Indisponível" }
-  ];
+  let data = [];
+  let loading = false;
+  let error = '';
+  let serverResponse = '';
+  let createLoading = false;
+  let createError = '';
+  let createSuccess = '';
+  let registerResetToken = 0;
+  let updateLoading = false;
+  let updateError = '';
 
   const tabs = ["Registro"];
 
@@ -28,6 +38,50 @@
   let showDeleteConfirm = false;
   let rowToDelete = null;
 
+  onMount(fetchCommodities);
+
+  async function fetchCommodities() {
+    loading = true;
+    error = '';
+    try {
+      const token = $authToken;
+      if (!token) {
+        throw new Error('Sessão expirada. Faça login novamente.');
+      }
+      const res = await fetch(`${apiUrl}/commodities?flag=all`, {
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        }
+      });
+      const raw = await res.text();
+      serverResponse = raw?.trim() ?? '';
+      console.log('Resposta do backend /commodities?flag=all:', raw);
+      let payload = null;
+      if (raw) {
+        try {
+          payload = JSON.parse(raw);
+        } catch (parseErr) {
+          console.error('Resposta inválida ao buscar commodities:', parseErr, raw);
+          throw new Error('Resposta inválida do servidor ao buscar commodities.');
+        }
+      }
+      if (!res.ok || payload?.status !== 'ok' || payload?.code !== 'S_OK') {
+        throw new Error(payload?.msg ?? 'Falha ao buscar commodities.');
+      }
+      data = (payload?.data ?? []).map((item) => ({
+        id: item.commodities_id,
+        nome: item.commodities_name ?? item.name ?? '-',
+        flag: item.commodities_flag ?? item.flag ?? '-'
+      }));
+    } catch (err) {
+      error = err?.message ?? 'Falha ao buscar commodities.';
+      data = [];
+    } finally {
+      loading = false;
+    }
+  }
+
   function handleAddTop() {
     activeModal = 1;
   }
@@ -36,16 +90,57 @@
     activeModal = 0;
   }
 
-  function handleRegisterSubmit(event) {
+  async function handleRegisterSubmit(event) {
     const payload = event?.detail;
-    // TODO: substituir por chamada de API/ação do SvelteKit
-    if (payload?.descricao) {
-      data = [
-        ...data,
-        { nome: payload.descricao, status: payload.cpr ? 'Com CPR' : 'Sem CPR' }
-      ];
+    if (!payload?.nome) {
+      createError = 'Informe o nome da commodity.';
+      return;
+    }
+
+    createError = '';
+    createSuccess = '';
+    createLoading = true;
+    try {
+      const body = {
+        name: payload.nome,
+        flag: (payload.flag ?? 'a')
+      };
+
+      const token = $authToken;
+      const res = await fetch(`${apiUrl}/commodity/create`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          ...(token ? { Authorization: `Bearer ${token}` } : {})
+        },
+        body: JSON.stringify(body)
+      });
+
+      const raw = await res.text();
+      let response = null;
+      if (raw) {
+        try {
+          response = JSON.parse(raw);
+        } catch (parseErr) {
+          console.error('Resposta inválida ao criar commodity:', parseErr, raw);
+          throw new Error('Resposta inválida do servidor ao criar commodity.');
+        }
+      }
+
+      if (!res.ok || response?.status !== 'ok' || response?.code !== 'S_CREATED') {
+        throw new Error(response?.msg ?? 'Falha ao criar commodity.');
+      }
+
+      const backendMsg = response?.msg;
+      createSuccess = backendMsg && backendMsg !== '[100] Request ok.' ? backendMsg : 'Commodity criada com sucesso.';
+      registerResetToken = Date.now();
+      activeModal = 0;
+      await fetchCommodities();
+    } catch (err) {
+      createError = err?.message ?? 'Falha ao criar commodity.';
+    } finally {
+      createLoading = false;
     }
-    activeModal = 0;
   }
 
   function handleEditRow(e) {
@@ -53,15 +148,11 @@
     if (!row) return;
     selectedRow = row;
     editValue = {
-      tipo: row.tipo ?? 'graos_60kg',
-      descricao: row.descricao ?? row.nome ?? '',
-      quantidade: row.quantidade ?? '',
-      preco: row.preco ?? '',
-      vencimentoPagamento: row.vencimentoPagamento ?? '',
-      dataLimiteEntrega: row.dataLimiteEntrega ?? '',
-      cpr: row.cpr ?? (row.status === 'Com CPR'),
-      arquivos: row.arquivos ?? []
+      id: row.id,
+      nome: row.nome ?? '',
+      flag: row.flag ?? 'a'
     };
+    updateError = '';
     showEdit = true;
   }
 
@@ -72,38 +163,120 @@
     showDeleteConfirm = true;
   }
 
-  function confirmDelete() {
-    data = data.filter((r) => r !== rowToDelete);
-    showDeleteConfirm = false;
-    rowToDelete = null;
+  let deleteLoading = false;
+  let deleteError = '';
+
+  async function confirmDelete() {
+    if (!rowToDelete?.id) {
+      showDeleteConfirm = false;
+      rowToDelete = null;
+      return;
+    }
+
+    deleteError = '';
+    deleteLoading = true;
+    try {
+      const token = $authToken;
+      const res = await fetch(`${apiUrl}/commodity/delete`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          ...(token ? { Authorization: `Bearer ${token}` } : {})
+        },
+        body: JSON.stringify({ commodities_id: rowToDelete.id })
+      });
+
+      const raw = await res.text();
+      let response = null;
+      if (raw) {
+        try {
+          response = JSON.parse(raw);
+        } catch (parseErr) {
+          console.error('Resposta inválida ao excluir commodity:', parseErr, raw);
+          throw new Error('Resposta inválida do servidor ao excluir commodity.');
+        }
+      }
+
+      if (!res.ok || response?.status !== 'ok' || response?.code !== 'S_DELETED') {
+        throw new Error(response?.msg ?? 'Falha ao excluir commodity.');
+      }
+
+      const backendMsg = response?.msg;
+      createSuccess = backendMsg && backendMsg !== '[100] Request ok.' ? backendMsg : 'Commodity excluída com sucesso.';
+      await fetchCommodities();
+      showDeleteConfirm = false;
+      rowToDelete = null;
+    } catch (err) {
+      deleteError = err?.message ?? 'Falha ao excluir commodity.';
+    } finally {
+      deleteLoading = false;
+    }
   }
 
   function cancelDelete() {
     showDeleteConfirm = false;
     rowToDelete = null;
+    deleteError = '';
+    deleteLoading = false;
   }
 
-  function handleCommoditySave(e) {
+  async function handleCommoditySave(e) {
     const { value } = e.detail || {};
-    if (selectedRow && value) {
-      data = data.map((r) =>
-        r === selectedRow
-          ? {
-              ...r,
-              ...value,
-              nome: value.descricao && value.descricao.trim() ? value.descricao : r.nome,
-              status: value.cpr ? 'Com CPR' : 'Sem CPR'
-            }
-          : r
-      );
+    if (!selectedRow?.id || !value) return;
+
+    updateError = '';
+    updateLoading = true;
+    try {
+      const body = {
+        commodities_id: selectedRow.id,
+        name: value.nome,
+        flag: value.flag
+      };
+
+      const token = $authToken;
+      const res = await fetch(`${apiUrl}/commodity/update`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          ...(token ? { Authorization: `Bearer ${token}` } : {})
+        },
+        body: JSON.stringify(body)
+      });
+
+      const raw = await res.text();
+      let response = null;
+      if (raw) {
+        try {
+          response = JSON.parse(raw);
+        } catch (parseErr) {
+          console.error('Resposta inválida ao atualizar commodity:', parseErr, raw);
+          throw new Error('Resposta inválida do servidor ao atualizar commodity.');
+        }
+      }
+
+      if (!res.ok || response?.status !== 'ok' || response?.code !== 'S_UPDATED') {
+        throw new Error(response?.msg ?? 'Falha ao atualizar commodity.');
+      }
+
+      const backendMsg = response?.msg;
+      createSuccess = backendMsg && backendMsg !== '[100] Request ok.' ? backendMsg : 'Commodity atualizada com sucesso.';
+      showEdit = false;
+      selectedRow = null;
+      editValue = {};
+      await fetchCommodities();
+    } catch (err) {
+      updateError = err?.message ?? 'Falha ao atualizar commodity.';
+    } finally {
+      updateLoading = false;
     }
-    handleCommodityCancel();
   }
 
   function handleCommodityCancel() {
     showEdit = false;
     selectedRow = null;
     editValue = {};
+    updateError = '';
+    updateLoading = false;
   }
 </script>
 
@@ -111,11 +284,23 @@
   <Header title="Commodities" subtitle="Gestão de commodities" breadcrumb={breadcrumb} />
   <div class="p-4">
     <div class="max-w-6xl mx-auto mt-4">
+      {#if createError}
+        <div class="mb-4 rounded border border-red-300 bg-red-50 text-red-700 px-3 py-2 text-sm">{createError}</div>
+      {/if}
+      {#if createSuccess}
+        <div class="mb-4 rounded border border-green-300 bg-green-50 text-green-700 px-3 py-2 text-sm">{createSuccess}</div>
+      {/if}
       {#if activeModal === 0}
+      {#if error && serverResponse}
+        <div class="mb-4 rounded border border-red-300 bg-red-50 text-red-700 px-3 py-2 text-sm">
+          <pre class="text-xs bg-white/80 dark:bg-gray-900/30 text-gray-800 dark:text-gray-100 rounded p-2 overflow-auto max-h-40">{serverResponse}</pre>
+        </div>
+      {/if}
       <Tables
         title="Produtos"
         columns={columns}
         data={data}
+        loading={loading}
         on:addTop={handleAddTop}
         on:editRow={handleEditRow}
         on:deleteRow={handleDeleteRow}
@@ -134,12 +319,14 @@
         visible={showEdit}
         title="Editar Commodity"
         value={editValue}
+        loading={updateLoading}
+        error={updateError}
         on:save={handleCommoditySave}
         on:cancel={handleCommodityCancel}
       />
       {:else if activeModal === 1}
       <div class="mb-4"><Tabs {tabs} showCloseIcon={true} on:close={handleCancel} /></div>
-      <RegisterCommodity on:submit={handleRegisterSubmit} />
+      <RegisterCommodity on:submit={handleRegisterSubmit} loading={createLoading} resetToken={registerResetToken} />
       {/if}
     </div>
   </div>

+ 28 - 48
src/routes/register/+page.svelte

@@ -2,7 +2,6 @@
   import { goto } from '$app/navigation';
   import { browser } from '$app/environment';
   import { onMount, onDestroy } from 'svelte';
-  import { companyId as companyIdStore } from '$lib/utils/stores';
 
   const apiUrl = import.meta.env.VITE_API_URL;
 
@@ -17,8 +16,6 @@
   let step = 1;
 
   let companyName = '';
-  let companyId = '';
-  let companyFlag = 'ACTIVE';
   let companyCnpj = '';
 
   let phone = '';
@@ -30,10 +27,8 @@
   let kyc = 1;
   let birthdate = '';
   let cpf = '';
-  let roleId = 1;
-
-  const companyEndpoint = `${apiUrl}/companies`;
-  const ownerEndpoint = `${apiUrl}/users`;
+  
+  const companyWithUserEndpoint = `${apiUrl}/company/user/create`;
 
   function setDark(enabled) {
     if (enabled) document.documentElement.classList.add('dark');
@@ -61,6 +56,10 @@
     });
   });
 
+  function digitsOnly(value) {
+    return value ? value.replace(/\D/g, '') : '';
+  }
+
   async function onSubmit(e) {
     e.preventDefault();
     error = '';
@@ -86,25 +85,17 @@
         throw new Error('Ação disponível apenas no navegador.');
       }
 
-      const companyPayload = {
-        name: companyName,
-        companyCnpj: companyCnpj,
-        flag: "ACTIVE"
-      };
-
-      function toBirthdateInt(s) {
+      function toBirthdateEpoch(s) {
         if (!s) return 0;
-        const parts = s.split('-');
-        if (parts.length !== 3) return 0;
-        const yyyy = parseInt(parts[0], 10);
-        const mm = parseInt(parts[1], 10);
-        const dd = parseInt(parts[2], 10);
-        if (!yyyy || !mm || !dd) return 0;
-        return yyyy * 10000 + mm * 100 + dd;
+        const date = new Date(s + 'T00:00:00Z');
+        if (Number.isNaN(date.getTime())) return 0;
+        return Math.floor(date.getTime() / 1000);
       }
 
-      const ownerPayload = {
-        name,
+      const payload = {
+        company_name: companyName,
+        cnpj: digitsOnly(companyCnpj),
+        username: name,
         email,
         password,
         phone,
@@ -114,44 +105,34 @@
         zip,
         country,
         kyc: Number(kyc),
-        birthdate: toBirthdateInt(birthdate),
-        cpf,
-        companyId: Number($companyIdStore ?? companyId),
-        roleId: Number(roleId),
-        flag: "ACTIVE"
+        birthdate: toBirthdateEpoch(birthdate),
+        cpf: digitsOnly(cpf)
       };
 
-      const resCompany = await fetch(companyEndpoint, {
+      const res = await fetch(companyWithUserEndpoint, {
         method: 'POST',
         headers: { 'content-type': 'application/json' },
-        body: JSON.stringify(companyPayload)
+        body: JSON.stringify(payload)
       });
 
-      if (!resCompany.ok) {
-        let msg = 'Falha ao cadastrar empresa.';
+      if (!res.ok) {
+        let msg = 'Falha ao criar conta.';
         try {
-          const err = await resCompany.json();
+          const err = await res.json();
+          console.error('Erro retornado pelo backend:', err);
           if (err?.message) msg = err.message;
         } catch {}
         throw new Error(msg);
       }
 
-      const resOwner = await fetch(ownerEndpoint, {
-        method: 'POST',
-        headers: { 'content-type': 'application/json' },
-        body: JSON.stringify(ownerPayload)
-      });
-
-      if (!resOwner.ok) {
-        let msg = 'Falha ao cadastrar proprietário.';
+      success = 'Conta criada com sucesso. Você já pode entrar.';
+      if (browser) {
         try {
-          const err = await resOwner.json();
-          if (err?.message) msg = err.message;
-        } catch {}
-        throw new Error(msg);
+          localStorage.setItem('registrationSuccessMessage', success);
+        } catch (err) {
+          console.warn('Não foi possível salvar o estado de sucesso:', err);
+        }
       }
-
-      success = 'Conta criada com sucesso. Você já pode entrar.';
       setTimeout(() => goto('/'), 800);
     } catch (err) {
       error = err?.message ?? 'Falha no cadastro, tente novamente.';
@@ -200,7 +181,6 @@
             <input
               id="companyCnpj"
               name="companyCnpj"
-              type="number"
               inputmode="numeric"
               bind:value={companyCnpj}
               class="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"

+ 156 - 7
src/routes/settings/+page.svelte

@@ -2,16 +2,132 @@
   import Header from '$lib/layout/Header.svelte';
   import ChangeEmail from '$lib/components/settings/ChangeEmail.svelte';
   import ChangePassword from '$lib/components/settings/ChangePassword.svelte';
+  import { get } from 'svelte/store';
+  import { authToken } from '$lib/utils/stores';
 
+  const apiUrl = import.meta.env.VITE_API_URL;
   const breadcrumb = [{ label: 'Início' }, { label: 'Configurações', active: true }];
 
-  function handleEmailSubmit(e) {
-    const { email } = e.detail || {};
-    alert(`E-mail atualizado para: ${email}`);
+  let emailLoading = false;
+  let emailError = '';
+  let emailSuccess = '';
+  let emailResetToken = 0;
+
+  let passwordLoading = false;
+  let passwordError = '';
+  let passwordSuccess = '';
+  let passwordResetToken = 0;
+
+  async function handleEmailSubmit(e) {
+    const { email, currentPassword } = e.detail || {};
+    if (!email || !currentPassword) {
+      return;
+    }
+
+    emailError = '';
+    emailSuccess = '';
+    emailLoading = true;
+
+    try {
+      const token = get(authToken);
+      if (!token) {
+        throw new Error('Sessão expirada. Faça login novamente.');
+      }
+
+      const res = await fetch(`${apiUrl}/user/change-email`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({ email, current_password: currentPassword })
+      });
+
+      let data = null;
+      try {
+        data = await res.json();
+      } catch (err) {
+        console.error('Não foi possível interpretar a resposta de alteração de e-mail:', err);
+      }
+
+      if (!res.ok) {
+        throw new Error(data?.msg ?? 'Falha ao alterar e-mail.');
+      }
+
+      const ok = data?.status === 'ok' && data?.code === 'S_UPDATED';
+      if (!ok) {
+        throw new Error(data?.msg ?? 'Falha ao alterar e-mail.');
+      }
+
+      const backendMsg = data?.msg;
+      emailSuccess = backendMsg && backendMsg !== '[100] Request ok.' ? backendMsg : 'E-mail alterado com sucesso.';
+      emailResetToken = Date.now();
+      logoutAndRedirect();
+    } catch (err) {
+      emailError = err?.message ?? 'Falha ao alterar e-mail.';
+    } finally {
+      emailLoading = false;
+    }
   }
 
-  function handlePasswordSubmit(e) {
-    alert('Senha alterada com sucesso');
+  async function handlePasswordSubmit(e) {
+    const { currentPassword, newPassword } = e.detail || {};
+    if (!currentPassword || !newPassword) {
+      return;
+    }
+
+    passwordError = '';
+    passwordSuccess = '';
+    passwordLoading = true;
+
+    try {
+      const token = get(authToken);
+      if (!token) {
+        throw new Error('Sessão expirada. Faça login novamente.');
+      }
+
+      const res = await fetch(`${apiUrl}/user/change-password`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({ current_password: currentPassword, new_password: newPassword })
+      });
+
+      let data = null;
+      try {
+        data = await res.json();
+      } catch (err) {
+        console.error('Não foi possível interpretar a resposta de alteração de senha:', err);
+      }
+
+      if (!res.ok) {
+        throw new Error(data?.msg ?? 'Falha ao alterar senha.');
+      }
+
+      const ok = data?.status === 'ok' && data?.code === 'S_UPDATED';
+      if (!ok) {
+        throw new Error(data?.msg ?? 'Falha ao alterar senha.');
+      }
+
+      const backendMsg = data?.msg;
+      passwordSuccess = backendMsg && backendMsg !== '[100] Request ok.' ? backendMsg : 'Senha alterada com sucesso.';
+      passwordResetToken = Date.now();
+      logoutAndRedirect();
+    } catch (err) {
+      passwordError = err?.message ?? 'Falha ao alterar senha.';
+    } finally {
+      passwordLoading = false;
+    }
+  }
+
+  function logoutAndRedirect() {
+    if (typeof document === 'undefined') return;
+    document.cookie = 'auth_token=; Max-Age=0; Path=/; SameSite=Lax';
+    document.cookie = 'company_id=; Max-Age=0; Path=/; SameSite=Lax';
+    authToken.set(null);
+    goto('/');
   }
 </script>
 
@@ -19,8 +135,41 @@
   <Header title="Configurações" subtitle="Gerencie sua conta" breadcrumb={breadcrumb} />
   <div class="p-4">
     <div class="max-w-4xl mx-auto mt-4 space-y-6">
-      <ChangeEmail on:submit={handleEmailSubmit} />
-      <ChangePassword on:submit={handlePasswordSubmit} />
+      <div class="space-y-3">
+        {#if emailError}
+          <div class="rounded border border-red-300 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-200 px-3 py-2 text-sm">
+            {emailError}
+          </div>
+        {/if}
+        {#if emailSuccess}
+          <div class="rounded border border-green-300 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-200 px-3 py-2 text-sm">
+            {emailSuccess}
+          </div>
+        {/if}
+        <ChangeEmail
+          on:submit={handleEmailSubmit}
+          loading={emailLoading}
+          resetToken={emailResetToken}
+        />
+      </div>
+
+      <div class="space-y-3">
+        {#if passwordError}
+          <div class="rounded border border-red-300 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-200 px-3 py-2 text-sm">
+            {passwordError}
+          </div>
+        {/if}
+        {#if passwordSuccess}
+          <div class="rounded border border-green-300 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-200 px-3 py-2 text-sm">
+            {passwordSuccess}
+          </div>
+        {/if}
+        <ChangePassword
+          on:submit={handlePasswordSubmit}
+          loading={passwordLoading}
+          resetToken={passwordResetToken}
+        />
+      </div>
     </div>
   </div>
 </div>

+ 134 - 32
src/routes/users/+page.svelte

@@ -15,6 +15,14 @@
   ];
 
   let data = [];
+  let loadError = '';
+  let loadServerResponse = '';
+  let successMessage = '';
+  let isLoadingUsers = false;
+
+  let createLoading = false;
+  let createError = '';
+  let createResetToken = 0;
 
   // Modal criar usuário
   let showCreate = false;
@@ -43,69 +51,113 @@
     return /^-?\d+$/.test(fromCookie) ? Number(fromCookie) : fromCookie;
   }
 
-  onMount(async () => {
+  onMount(loadUsers);
+
+  async function loadUsers() {
+    isLoadingUsers = true;
     try {
+      loadError = '';
+      loadServerResponse = '';
       console.log('cookie company_id:', getCookie('company_id'));
       const cid = getCompanyIdValue();
       console.log('resolved companyId:', cid);
-      if (!cid) return;
-      const url = `${apiUrl}/auth/company/${cid}`;
+      if (!cid) throw new Error('company_id não encontrado.');
+      const url = `${apiUrl}/user/get`;
       console.log('fetch url:', url);
       const res = await fetch(url, {
+        method: 'POST',
         headers: {
+          'content-type': 'application/json',
           ...( $authToken ? { Authorization: `Bearer ${$authToken}` } : {} )
-        }
+        },
+        body: JSON.stringify({ company_id: cid })
       });
-      console.log('response status:', res.status);
-      if (!res.ok) {
-        try { console.log('error body:', await res.text()); } catch {}
-        return;
+      const raw = await res.text();
+      loadServerResponse = raw?.trim() ?? '';
+      console.log('response raw body:', raw);
+      let body = null;
+      if (raw) {
+        try {
+          body = JSON.parse(raw);
+        } catch (err) {
+          console.error('Resposta inválida do endpoint /users:', err);
+          throw new Error('Resposta inválida do servidor.');
+        }
+      }
+      if (!res.ok || body?.status !== 'ok') {
+        throw new Error(body?.msg ?? 'Falha ao carregar usuários.');
       }
-      const body = await res.json();
-      console.log('response body:', body);
-      const list = Array.isArray(body) ? body : (body?.users ?? []);
+      const list = Array.isArray(body?.data) ? body.data : [];
       console.log('parsed users length:', list.length);
       data = list.map((u) => ({
         __raw: u,
-        name: u?.userName ?? u?.name ?? u?.fullName ?? u?.username ?? '-',
-        email: u?.userEmail ?? u?.email ?? '-'
+        name: u?.name ?? u?.user_name ?? u?.userName ?? u?.fullName ?? '-',
+        email: u?.email ?? u?.user_email ?? u?.userEmail ?? '-'
       }));
-    } catch (e) { console.log('fetch users error:', e); }
-  });
+    } catch (e) {
+      console.log('fetch users error:', e);
+      loadError = e?.message ?? 'Falha ao carregar usuários.';
+      data = [];
+    } finally {
+      isLoadingUsers = false;
+    }
+  }
 
   async function handleCreateSubmit(e) {
     const payload = e?.detail;
     if (!payload) return;
+    const cid = getCompanyIdValue();
+    if (!cid) {
+      createError = 'company_id não encontrado.';
+      return;
+    }
+    createLoading = true;
+    createError = '';
     try {
-      const res = await fetch(`${apiUrl}/users`, {
+      const requestBody = {
+        ...payload,
+        company_id: cid,
+        role_id: payload?.role_id ?? 1
+      };
+      const res = await fetch(`${apiUrl}/register`, {
         method: 'POST',
         headers: {
           'content-type': 'application/json',
           ...( $authToken ? { Authorization: `Bearer ${$authToken}` } : {} )
         },
-        body: JSON.stringify(payload)
+        body: JSON.stringify(requestBody)
       });
-      if (!res.ok) {
+      const raw = await res.text();
+      console.log('create user raw body:', raw);
+      let body = null;
+      if (raw) {
         try {
-          const err = await res.json();
-          console.error('Falha ao criar usuário:', err);
-        } catch {}
-        // Fallback: ainda adiciona localmente para feedback imediato
-        data = [...data, { name: payload.name, email: payload.email }];
-      } else {
-        const created = await res.json();
-        data = [...data, { name: created?.name ?? payload.name, email: created?.email ?? payload.email }];
+          body = JSON.parse(raw);
+        } catch (err) {
+          console.error('Resposta inválida do endpoint /register:', err);
+          throw new Error('Resposta inválida do servidor.');
+        }
+      }
+      const isSuccess = body?.status === 'success' || body?.status === 'ok' || body?.code === 'S_CREATED';
+      if (!res.ok || !isSuccess) {
+        throw new Error(body?.message ?? body?.msg ?? 'Falha ao criar usuário.');
       }
+      successMessage = body?.message ?? body?.msg ?? 'Usuário criado com sucesso!';
+      showCreate = false;
+      createResetToken = Date.now();
+      await loadUsers();
     } catch (err) {
       console.error('Erro na criação do usuário:', err);
-      data = [...data, { name: payload.name, email: payload.email }];
+      createError = err?.message ?? 'Falha ao criar usuário.';
     } finally {
-      showCreate = false;
+      createLoading = false;
     }
   }
 
   function handleCreateCancel() {
     showCreate = false;
+    createError = '';
+    createResetToken = Date.now();
   }
 
   function handleDeleteRow(e) {
@@ -123,10 +175,60 @@
     showDetails = true;
   }
 
-  function confirmDelete() {
-    data = data.filter((r) => r !== rowToDelete);
-    showDeleteConfirm = false;
-    rowToDelete = null;
+  async function confirmDelete() {
+    if (!rowToDelete) { showDeleteConfirm = false; return; }
+    const userId =
+      rowToDelete?.__raw?.user_id ??
+      rowToDelete?.__raw?.userId ??
+      rowToDelete?.__raw?.id ??
+      rowToDelete?.user_id ??
+      rowToDelete?.userId ??
+      rowToDelete?.id;
+    if (userId == null) {
+      console.error('ID do usuário não encontrado para exclusão.');
+      showDeleteConfirm = false;
+      rowToDelete = null;
+      return;
+    }
+    const cid = getCompanyIdValue();
+    if (!cid) {
+      console.error('company_id não encontrado para exclusão de usuário.');
+      showDeleteConfirm = false;
+      rowToDelete = null;
+      return;
+    }
+    try {
+      const res = await fetch(`${apiUrl}/user/delete`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          ...( $authToken ? { Authorization: `Bearer ${$authToken}` } : {} )
+        },
+        body: JSON.stringify({ user_id: userId, company_id: cid })
+      });
+      const raw = await res.text();
+      console.log('delete user raw body:', raw);
+      let body = null;
+      if (raw) {
+        try {
+          body = JSON.parse(raw);
+        } catch (err) {
+          console.error('Resposta inválida do endpoint /user/delete:', err);
+          throw new Error('Resposta inválida do servidor.');
+        }
+      }
+      const isSuccess = body?.status === 'success' || body?.status === 'ok' || body?.code === 'S_DELETED';
+      if (!res.ok || !isSuccess) {
+        throw new Error(body?.message ?? body?.msg ?? 'Falha ao excluir usuário.');
+      }
+      successMessage = body?.message ?? body?.msg ?? 'Usuário excluído com sucesso!';
+      await loadUsers();
+    } catch (e) {
+      console.error('Erro ao excluir usuário:', e);
+    } finally {
+      showDeleteConfirm = false;
+      rowToDelete = null;
+    }
   }
 
   function cancelDelete() {