Quellcode durchsuchen

add more connections

gdias vor 1 Monat
Ursprung
Commit
b8caa7a476

+ 7 - 3
src/lib/components/Tables.svelte

@@ -34,6 +34,10 @@
   function handleDeleteRow(row, index) {
     dispatch('deleteRow', { row, index });
   }
+
+  function handleRowClick(row, index) {
+    dispatch('rowClick', { row, index });
+  }
 </script>
 
 <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
@@ -69,7 +73,7 @@
       <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
         {#if data.length > 0}
           {#each data as row, index}
-            <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/60" on:dblclick={() => handleEditRow(row, index)}>
+            <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/60 cursor-pointer" on:click={() => handleRowClick(row, index)} on:dblclick={() => handleEditRow(row, index)}>
               {#each columns as col}
                 <td class="px-6 py-3 text-base text-gray-700 dark:text-gray-300">
                   {row[col.key]}
@@ -83,7 +87,7 @@
                         class="inline-flex items-center justify-center w-9 h-9 rounded-md border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
                         type="button"
                         title="Editar"
-                        on:click={() => handleEditRow(row, index)}
+                        on:click|stopPropagation={() => handleEditRow(row, index)}
                       >
                         <img src={editLight} alt="Editar" class="w-5 h-5 block dark:hidden" />
                         <img src={editDark} alt="Editar" class="w-5 h-5 hidden dark:block" />
@@ -95,7 +99,7 @@
                         class="inline-flex items-center justify-center w-9 h-9 rounded-md border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
                         type="button"
                         title="Excluir"
-                        on:click={() => handleDeleteRow(row, index)}
+                        on:click|stopPropagation={() => handleDeleteRow(row, index)}
                       >
                         <img src={trashLight} alt="Excluir" class="w-5 h-5 block dark:hidden" />
                         <img src={trashDark} alt="Excluir" class="w-5 h-5 hidden dark:block" />

+ 8 - 0
src/lib/utils/stores.js

@@ -19,6 +19,7 @@ function createDarkModeStore() {
 export const darkMode = createDarkModeStore();
 
 export const authToken = writable(null);
+export const companyId = writable(null);
 
 if (browser) {
   // Inicializar com valor do localStorage
@@ -44,4 +45,11 @@ if (browser) {
   // Inicializar authToken a partir do cookie
   const m = document.cookie.match(/(?:^|; )auth_token=([^;]+)/);
   if (m) authToken.set(decodeURIComponent(m[1]));
+
+  // Inicializar companyId a partir do cookie
+  const m2 = document.cookie.match(/(?:^|; )company_id=([^;]+)/);
+  if (m2) {
+    const v = decodeURIComponent(m2[1]);
+    companyId.set(/^-?\d+$/.test(v) ? Number(v) : v);
+  }
 }

+ 1 - 1
src/routes/+layout.svelte

@@ -17,7 +17,7 @@
 	
 	const isNotFound = $derived(!$page.route || !$page.route.id); 
 	
-	const hideSidebar = $derived(isRoot || isNotFound);
+	const hideSidebar = $derived(isRoot || isNotFound || $page.url.pathname.startsWith('/register'));
   </script>
   
   <svelte:head>

+ 14 - 1
src/routes/+page.svelte

@@ -2,7 +2,7 @@
   import { goto } from '$app/navigation';
   import { browser } from '$app/environment';
   import { onMount, onDestroy } from 'svelte';
-  import { authToken } from '$lib/utils/stores';
+  import { authToken, companyId as companyIdStore } from '$lib/utils/stores';
 
   const apiUrl = import.meta.env.VITE_API_URL;
   let email = '';
@@ -64,10 +64,14 @@
           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;
       if (!token) {
         throw new Error('Resposta inválida do servidor.');
       }
@@ -79,6 +83,10 @@
         remember ? `Max-Age=${60 * 60 * 24 * 7}` : null
       ].filter(Boolean).join('; ');
       document.cookie = `auth_token=${encodeURIComponent(token)}; ${attrs}`;
+      if (companyIdFromApi != null) {
+        document.cookie = `company_id=${encodeURIComponent(companyIdFromApi)}; ${attrs}`;
+        companyIdStore.set(companyIdFromApi);
+      }
       authToken.set(token);
 
       await goto('/dashboard');
@@ -146,6 +154,11 @@
     <p class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
       Esqueceu a senha? <a href="#" class="text-blue-600 hover:text-blue-500">Fale com o suporte</a>
     </p>
+    <div class="mt-3">
+      <a href="/register" class="block w-full text-center rounded border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 font-medium px-4 py-2">
+        Ainda não tem conta? Crie agora
+      </a>
+    </div>
   </div>
   
 </div>

+ 427 - 0
src/routes/register/+page.svelte

@@ -0,0 +1,427 @@
+<script>
+  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;
+
+  let name = '';
+  let email = '';
+  let password = '';
+  let confirmPassword = '';
+  let loading = false;
+  let error = '';
+  let success = '';
+
+  let step = 1;
+
+  let companyName = '';
+  let companyId = '';
+  let companyFlag = 'ACTIVE';
+  let companyCnpj = '';
+
+  let phone = '';
+  let address = '';
+  let city = '';
+  let stateUF = '';
+  let zip = '';
+  let country = 'BR';
+  let kyc = 1;
+  let birthdate = '';
+  let cpf = '';
+  let roleId = 1;
+
+  const companyEndpoint = `${apiUrl}/companies`;
+  const ownerEndpoint = `${apiUrl}/users`;
+
+  function setDark(enabled) {
+    if (enabled) document.documentElement.classList.add('dark');
+    else document.documentElement.classList.remove('dark');
+  }
+
+  function applyDarkModeFromStorage() {
+    if (!browser) return;
+    try {
+      const saved = localStorage.getItem('darkmode');
+      if (saved === 'true') setDark(true);
+      else if (saved === 'false') setDark(false);
+      else setDark(window.matchMedia('(prefers-color-scheme: dark)').matches);
+    } catch (e) {}
+  }
+
+  onMount(() => {
+    applyDarkModeFromStorage();
+    const onStorage = (e) => {
+      if (e.key === 'darkmode') applyDarkModeFromStorage();
+    };
+    window.addEventListener('storage', onStorage);
+    onDestroy(() => {
+      window.removeEventListener('storage', onStorage);
+    });
+  });
+
+  async function onSubmit(e) {
+    e.preventDefault();
+    error = '';
+    success = '';
+    loading = true;
+    try {
+      if (step === 1) {
+        if (!companyName || !companyCnpj) {
+          throw new Error('Preencha os dados da empresa.');
+        }
+        step = 2;
+        loading = false;
+        return;
+      }
+
+      if (!name || !email || !password || !confirmPassword || !phone || !address || !city || !stateUF || !zip || !cpf || !birthdate) {
+        throw new Error('Preencha todos os campos.');
+      }
+      if (password !== confirmPassword) {
+        throw new Error('As senhas não coincidem.');
+      }
+      if (!browser) {
+        throw new Error('Ação disponível apenas no navegador.');
+      }
+
+      const companyPayload = {
+        name: companyName,
+        companyCnpj: companyCnpj,
+        flag: "ACTIVE"
+      };
+
+      function toBirthdateInt(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 ownerPayload = {
+        name,
+        email,
+        password,
+        phone,
+        address,
+        city,
+        state: stateUF,
+        zip,
+        country,
+        kyc: Number(kyc),
+        birthdate: toBirthdateInt(birthdate),
+        cpf,
+        companyId: Number($companyIdStore ?? companyId),
+        roleId: Number(roleId),
+        flag: "ACTIVE"
+      };
+
+      const resCompany = await fetch(companyEndpoint, {
+        method: 'POST',
+        headers: { 'content-type': 'application/json' },
+        body: JSON.stringify(companyPayload)
+      });
+
+      if (!resCompany.ok) {
+        let msg = 'Falha ao cadastrar empresa.';
+        try {
+          const err = await resCompany.json();
+          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.';
+        try {
+          const err = await resOwner.json();
+          if (err?.message) msg = err.message;
+        } catch {}
+        throw new Error(msg);
+      }
+
+      success = 'Conta criada com sucesso. Você já pode entrar.';
+      setTimeout(() => goto('/'), 800);
+    } catch (err) {
+      error = err?.message ?? 'Falha no cadastro, tente novamente.';
+    } finally {
+      loading = false;
+    }
+  }
+</script>
+
+<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
+  <div class="w-full max-w-md">
+    <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-8">
+      <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Criar conta</h1>
+      <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
+        {#if step === 1}
+          Etapa 1 de 2: Dados da empresa
+        {:else}
+          Etapa 2 de 2: Dados do proprietário
+        {/if}
+      </p>
+
+      {#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">
+        {#if step === 1}
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="companyName">Nome da empresa</label>
+            <input
+              id="companyName"
+              name="companyName"
+              type="text"
+              bind:value={companyName}
+              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"
+              placeholder="Minha Empresa"
+              required
+            />
+          </div>
+
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="companyCnpj">CNPJ</label>
+            <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"
+              required
+            />
+          </div>
+
+          <button
+            class="w-full inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
+            disabled={loading}
+            type="submit"
+          >
+            {#if loading}
+              Continuando...
+            {:else}
+              Continuar
+            {/if}
+          </button>
+        {:else}
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="ownerName">Nome do proprietário</label>
+            <input
+              id="ownerName"
+              name="ownerName"
+              type="text"
+              bind:value={name}
+              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"
+              placeholder="John Doe"
+              required
+            />
+          </div>
+
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="email">E-mail</label>
+            <input
+              id="email"
+              name="email"
+              type="email"
+              bind:value={email}
+              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"
+              placeholder="email@exemplo.com"
+              autocomplete="email"
+              required
+            />
+          </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 mb-1" for="password">Senha</label>
+              <input
+                id="password"
+                name="password"
+                type="password"
+                bind:value={password}
+                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"
+                placeholder="••••••••"
+                autocomplete="new-password"
+                required
+              />
+            </div>
+            <div>
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="confirm">Confirmar senha</label>
+              <input
+                id="confirm"
+                name="confirm"
+                type="password"
+                bind:value={confirmPassword}
+                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"
+                placeholder="••••••••"
+                autocomplete="new-password"
+                required
+              />
+            </div>
+          </div>
+
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="phone">Telefone</label>
+            <input
+              id="phone"
+              name="phone"
+              type="tel"
+              bind:value={phone}
+              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"
+              placeholder="+55 11 99999-9999"
+              required
+            />
+          </div>
+
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="address">Endereço</label>
+            <input
+              id="address"
+              name="address"
+              type="text"
+              bind:value={address}
+              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"
+              placeholder="Av. Paulista, 1000"
+              required
+            />
+          </div>
+
+          <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
+            <div class="sm:col-span-2">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="city">Cidade</label>
+              <input
+                id="city"
+                name="city"
+                type="text"
+                bind:value={city}
+                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"
+                placeholder="São Paulo"
+                required
+              />
+            </div>
+            <div>
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="stateUF">UF</label>
+              <input
+                id="stateUF"
+                name="stateUF"
+                type="text"
+                maxlength="2"
+                bind:value={stateUF}
+                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"
+                placeholder="SP"
+                required
+              />
+            </div>
+          </div>
+
+          <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
+            <div>
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="zip">CEP</label>
+              <input
+                id="zip"
+                name="zip"
+                type="text"
+                bind:value={zip}
+                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"
+                placeholder="01310-100"
+                required
+              />
+            </div>
+            <div class="sm:col-span-2">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="country">País</label>
+              <input
+                id="country"
+                name="country"
+                type="text"
+                bind:value={country}
+                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"
+                placeholder="BR"
+                required
+              />
+            </div>
+          </div>
+
+          <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
+            <div>
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="kyc">KYC</label>
+              <select
+                id="kyc"
+                name="kyc"
+                bind:value={kyc}
+                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"
+                required
+              >
+                <option value="1">1 (Sim)</option>
+                <option value="0">0 (Não)</option>
+              </select>
+            </div>
+            <div class="sm:col-span-2">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="birthdate">Data de nascimento</label>
+              <input
+                id="birthdate"
+                name="birthdate"
+                type="date"
+                bind:value={birthdate}
+                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"
+                required
+              />
+            </div>
+          </div>
+
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="cpf">CPF</label>
+            <input
+              id="cpf"
+              name="cpf"
+              type="text"
+              bind:value={cpf}
+              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"
+              placeholder="123.456.789-00"
+              required
+            />
+          </div>
+
+          <div class="flex gap-3">
+            <button
+              type="button"
+              class="w-1/3 inline-flex items-center justify-center rounded border border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
+              on:click={() => step = 1}
+              disabled={loading}
+            >Voltar</button>
+            <button
+              class="w-2/3 inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
+              disabled={loading}
+              type="submit"
+            >
+              {#if loading}
+                Enviando...
+              {:else}
+                Concluir cadastro
+              {/if}
+            </button>
+          </div>
+        {/if}
+      </form>
+    </div>
+
+    <p class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
+      Já tem conta? <a href="/" class="text-blue-600 hover:text-blue-500">Entrar</a>
+    </p>
+  </div>
+</div>

+ 92 - 5
src/routes/users/+page.svelte

@@ -3,7 +3,8 @@
   import Tables from '$lib/components/Tables.svelte';
   import UserCreateModal from '$lib/components/users/UserCreateModal.svelte';
   import ConfirmModal from '$lib/components/ui/PopUpDelete.svelte';
-  import { authToken } from '$lib/utils/stores';
+  import { authToken, companyId as companyIdStore } from '$lib/utils/stores';
+  import { onMount } from 'svelte';
   const apiUrl = import.meta.env.VITE_API_URL;
   const breadcrumb = [{ label: 'Início' }, { label: 'Usuários', active: true }];
 
@@ -13,10 +14,7 @@
     { key: 'email', label: 'E-mail' }
   ];
 
-  let data = [
-    { name: 'Lucas Lumps', email: 'lucas@empresa.com' },
-    { name: 'Jorginho', email: 'jorginho@empresa.com' }
-  ];
+  let data = [];
 
   // Modal criar usuário
   let showCreate = false;
@@ -25,10 +23,56 @@
   let showDeleteConfirm = false;
   let rowToDelete = null;
 
+  let selectedUser = null;
+  let showDetails = false;
+
   function handleAddTop() {
     showCreate = true;
   }
 
+  function getCookie(name) {
+    const m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]+)'));
+    return m ? decodeURIComponent(m[1]) : null;
+  }
+
+  function getCompanyIdValue() {
+    const fromStore = $companyIdStore;
+    if (fromStore != null && fromStore !== '') return fromStore;
+    const fromCookie = getCookie('company_id');
+    if (fromCookie == null || fromCookie === '') return null;
+    return /^-?\d+$/.test(fromCookie) ? Number(fromCookie) : fromCookie;
+  }
+
+  onMount(async () => {
+    try {
+      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}`;
+      console.log('fetch url:', url);
+      const res = await fetch(url, {
+        headers: {
+          ...( $authToken ? { Authorization: `Bearer ${$authToken}` } : {} )
+        }
+      });
+      console.log('response status:', res.status);
+      if (!res.ok) {
+        try { console.log('error body:', await res.text()); } catch {}
+        return;
+      }
+      const body = await res.json();
+      console.log('response body:', body);
+      const list = Array.isArray(body) ? body : (body?.users ?? []);
+      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 ?? '-'
+      }));
+    } catch (e) { console.log('fetch users error:', e); }
+  });
+
   async function handleCreateSubmit(e) {
     const payload = e?.detail;
     if (!payload) return;
@@ -71,6 +115,14 @@
     showDeleteConfirm = true;
   }
 
+  function handleEditRow(e) {
+    const { row } = e?.detail || {};
+    if (!row) return;
+    selectedUser = row.__raw ?? row;
+    console.log('clicked user details:', selectedUser);
+    showDetails = true;
+  }
+
   function confirmDelete() {
     data = data.filter((r) => r !== rowToDelete);
     showDeleteConfirm = false;
@@ -92,6 +144,8 @@
         {columns}
         {data}
         on:addTop={handleAddTop}
+        on:rowClick={handleEditRow}
+        on:editRow={handleEditRow}
         on:deleteRow={handleDeleteRow}
         showEdit={false}
       />
@@ -112,6 +166,39 @@
       >
         <p>Tem certeza que deseja excluir o usuário "{rowToDelete?.name}"?</p>
       </ConfirmModal>
+
+      {#if showDetails}
+        <div class="fixed inset-0 bg-black/40 flex items-center justify-center z-50" on:click={() => showDetails = false}>
+          <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg w-full max-w-lg p-6" on:click|stopPropagation>
+            <div class="flex items-center justify-between mb-4">
+              <h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Detalhes do usuário</h4>
+              <button class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" on:click={() => showDetails = false}>✕</button>
+            </div>
+            {#if selectedUser}
+              <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
+                <div><span class="text-gray-500">ID:</span> <span class="dark:text-gray-100">{selectedUser.userId}</span></div>
+                <div><span class="text-gray-500">Empresa:</span> <span class="dark:text-gray-100">{selectedUser.companyId}</span></div>
+                <div><span class="text-gray-500">Nome:</span> <span class="dark:text-gray-100">{selectedUser.userName}</span></div>
+                <div><span class="text-gray-500">E-mail:</span> <span class="dark:text-gray-100">{selectedUser.userEmail}</span></div>
+                <div><span class="text-gray-500">Telefone:</span> <span class="dark:text-gray-100">{selectedUser.userPhone}</span></div>
+                <div><span class="text-gray-500">CPF:</span> <span class="dark:text-gray-100">{selectedUser.userCpf}</span></div>
+                <div><span class="text-gray-500">Data Nasc.:</span> <span class="dark:text-gray-100">{selectedUser.userBirthdate}</span></div>
+                <div><span class="text-gray-500">KYC:</span> <span class="dark:text-gray-100">{selectedUser.userKyc}</span></div>
+                <div><span class="text-gray-500">Papel (roleId):</span> <span class="dark:text-gray-100">{selectedUser.roleId}</span></div>
+                <div><span class="text-gray-500">Status:</span> <span class="dark:text-gray-100">{selectedUser.userFlag}</span></div>
+                <div class="sm:col-span-2"><span class="text-gray-500">Endereço:</span> <span class="dark:text-gray-100">{selectedUser.userAddress}</span></div>
+                <div><span class="text-gray-500">Cidade:</span> <span class="dark:text-gray-100">{selectedUser.userCity}</span></div>
+                <div><span class="text-gray-500">Estado:</span> <span class="dark:text-gray-100">{selectedUser.userState}</span></div>
+                <div><span class="text-gray-500">CEP:</span> <span class="dark:text-gray-100">{selectedUser.userZip}</span></div>
+                <div><span class="text-gray-500">País:</span> <span class="dark:text-gray-100">{selectedUser.userCountry}</span></div>
+              </div>
+            {/if}
+            <div class="mt-5 flex justify-end">
+              <button class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white" on:click={() => showDetails = false}>Fechar</button>
+            </div>
+          </div>
+        </div>
+      {/if}
     </div>
   </div>
 </div>