Compare commits

..

10 Commits

4 changed files with 1357 additions and 861 deletions

View File

@@ -0,0 +1,275 @@
---
trigger: glob
globs: **/*.svelte, **/*.ts, **/*.svelte.ts
---
# Convex + Svelte Guidelines
## Overview
These guidelines describe how to write **Convex** backend code **and** consume it from a **Svelte** (SvelteKit) frontend. The syntax for Convex functions stays exactly the same, but the way you import and call them from the client differs from a React/Next.js project. Below you will find the adapted sections from the original Convex style guide with Sveltespecific notes.
---
## 1. Function Syntax (Backend)
> **No change** keep the new Convex function syntax.
```typescript
import {
query,
mutation,
action,
internalQuery,
internalMutation,
internalAction
} from './_generated/server';
import { v } from 'convex/values';
export const getUser = query({
args: { userId: v.id('users') },
returns: v.object({ name: v.string(), email: v.string() }),
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) throw new Error('User not found');
return { name: user.name, email: user.email };
}
});
```
---
## 2. HTTP Endpoints (Backend)
> **No change** keep the same `convex/http.ts` file.
```typescript
import { httpRouter } from 'convex/server';
import { httpAction } from './_generated/server';
const http = httpRouter();
http.route({
path: '/api/echo',
method: 'POST',
handler: httpAction(async (ctx, req) => {
const body = await req.bytes();
return new Response(body, { status: 200 });
})
});
```
---
## 3. Validators (Backend)
> **No change** keep the same validators (`v.string()`, `v.id()`, etc.).
---
## 4. Function Registration (Backend)
> **No change** use `query`, `mutation`, `action` for public functions and `internal*` for private ones.
---
## 5. Function Calling from **Svelte**
### 5.1 Install the Convex client
```bash
npm i convex @convex-dev/convex-svelte
```
> The `@convex-dev/convex-svelte` package provides a thin wrapper that works with Svelte stores.
### 5.2 Initialise the client (e.g. in `src/lib/convex.ts`)
```typescript
import { createConvexClient } from '@convex-dev/convex-svelte';
export const convex = createConvexClient({
url: import.meta.env.VITE_CONVEX_URL // set in .env
});
```
### 5.3 Using queries in a component
```svelte
<script lang="ts">
import { convex } from '$lib/convex';
import { onMount } from 'svelte';
import { api } from '../convex/_generated/api';
let user: { name: string; email: string } | null = null;
let loading = true;
let error: string | null = null;
onMount(async () => {
try {
user = await convex.query(api.users.getUser, { userId: 'some-id' });
} catch (e) {
error = (e as Error).message;
} finally {
loading = false;
}
});
</script>
{#if loading}
<p>Loading…</p>
{:else if error}
<p class="error">{error}</p>
{:else if user}
<h2>{user.name}</h2>
<p>{user.email}</p>
{/if}
```
### 5.4 Using mutations in a component
```svelte
<script lang="ts">
import { convex } from '$lib/convex';
import { api } from '../convex/_generated/api';
let name = '';
let creating = false;
let error: string | null = null;
async function createUser() {
creating = true;
error = null;
try {
const userId = await convex.mutation(api.users.createUser, { name });
console.log('Created user', userId);
} catch (e) {
error = (e as Error).message;
} finally {
creating = false;
}
}
</script>
<input bind:value={name} placeholder="Name" />
<button on:click={createUser} disabled={creating}>Create</button>
{#if error}<p class="error">{error}</p>{/if}
```
### 5.5 Using **actions** (Nodeonly) from Svelte
Actions run in a Node environment, so they cannot be called directly from the browser. Use a **mutation** that internally calls the action, or expose a HTTP endpoint that triggers the action.
---
## 6. Scheduler / Cron (Backend)
> Same as original guide define `crons.ts` and export the default `crons` object.
---
## 7. File Storage (Backend)
> Same as original guide use `ctx.storage.getUrl()` and query `_storage` for metadata.
---
## 8. TypeScript Helpers (Backend)
> Keep using `Id<'table'>` from `./_generated/dataModel`.
---
## 9. SvelteSpecific Tips
| Topic | Recommendation |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Storebased data** | If you need reactive data across many components, wrap `convex.query` in a Svelte store (`readable`, `writable`). |
| **Error handling** | Use `try / catch` around every client call; surface the error in the UI. |
| **SSR / SvelteKit** | Calls made in `load` functions run on the server; you can use `convex.query` there without worrying about the browser environment. |
| **Environment variables** | Prefix with `VITE_` for clientside access (`import.meta.env.VITE_CONVEX_URL`). |
| **Testing** | Use the Convex mock client (`createMockConvexClient`) provided by `@convex-dev/convex-svelte` for unit tests. |
---
## 10. Full Example (SvelteKit + Convex)
### 10.1 Backend (`convex/users.ts`)
```typescript
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';
export const createUser = mutation({
args: { name: v.string() },
returns: v.id('users'),
handler: async (ctx, args) => {
return await ctx.db.insert('users', { name: args.name });
}
});
export const getUser = query({
args: { userId: v.id('users') },
returns: v.object({ name: v.string() }),
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) throw new Error('Not found');
return { name: user.name };
}
});
```
### 10.2 Frontend (`src/routes/+page.svelte`)
```svelte
<script lang="ts">
import { convex } from '$lib/convex';
import { api } from '$lib/convex/_generated/api';
import { onMount } from 'svelte';
let name = '';
let createdId: string | null = null;
let loading = false;
let error: string | null = null;
async function create() {
loading = true;
error = null;
try {
createdId = await convex.mutation(api.users.createUser, { name });
} catch (e) {
error = (e as Error).message;
} finally {
loading = false;
}
}
</script>
<input bind:value={name} placeholder="Your name" />
<button on:click={create} disabled={loading}>Create user</button>
{#if createdId}<p>Created user id: {createdId}</p>{/if}
{#if error}<p class="error">{error}</p>{/if}
```
---
## 11. Checklist for New Files
- ✅ All Convex functions use the **new syntax** (`query({ … })`).
- ✅ Every public function has **argument** and **return** validators.
- ✅ Svelte components import the generated `api` object from `convex/_generated/api`.
- ✅ All client calls use the `convex` instance from `$lib/convex`.
- ✅ Environment variable `VITE_CONVEX_URL` is defined in `.env`.
- ✅ Errors are caught and displayed in the UI.
- ✅ Types are imported from `convex/_generated/dataModel` when needed.
---
## 12. References
- Convex Docs [Functions](https://docs.convex.dev/functions)
- Convex Svelte SDK [`@convex-dev/convex-svelte`](https://github.com/convex-dev/convex-svelte)
- SvelteKit Docs [Loading Data](https://kit.svelte.dev/docs/loading)
---
_Keep these guidelines alongside the existing `svelte-rules.md` so that contributors have a single source of truth for both frontend and backend conventions._

View File

@@ -0,0 +1,28 @@
---
trigger: glob
globs: **/*.svelte.ts,**/*.svelte
---
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.

View File

@@ -1,31 +1,40 @@
<script lang="ts"> <script lang="ts">
import { useConvexClient } from "convex-svelte"; import { useConvexClient } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import { onMount } from "svelte"; import { onMount } from 'svelte';
import { resolve } from "$app/paths"; import { resolve } from '$app/paths';
const client = useConvexClient(); const client = useConvexClient();
type Row = { _id: string; nome: string; valor: number; count: number }; type Row = { _id: string; nome: string; valor: number; count: number };
let rows: Array<Row> = $state<Array<Row>>([]); let rows: Array<Row> = $state<Array<Row>>([]);
let isLoading = $state(true); let isLoading = $state(true);
let notice = $state<{ kind: "error" | "success"; text: string } | null>(null); let notice = $state<{ kind: 'error' | 'success'; text: string } | null>(null);
let containerWidth = $state(1200); let containerWidth = $state(1200);
onMount(async () => { onMount(async () => {
try { try {
const simbolos = await client.query(api.simbolos.getAll, {} as any); const simbolos = await client.query(api.simbolos.getAll, {});
const funcionarios = await client.query(api.funcionarios.getAll, {} as any); const funcionarios = await client.query(api.funcionarios.getAll, {});
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
for (const f of funcionarios) counts[f.simboloId] = (counts[f.simboloId] ?? 0) + 1; for (const f of funcionarios) {
rows = simbolos.map((s: any) => ({ const sId = String(f.simboloId);
counts[sId] = (counts[sId] ?? 0) + 1;
}
rows = simbolos.map((s) => ({
_id: String(s._id), _id: String(s._id),
nome: s.nome as string, nome: s.nome,
valor: Number(s.valor || 0), valor: Number(s.valor || 0),
count: counts[String(s._id)] ?? 0, count: counts[String(s._id)] ?? 0
})); }));
} catch (e) { } catch (e) {
notice = { kind: "error", text: "Falha ao carregar dados de relatórios." }; if (e instanceof Error) {
notice = { kind: 'error', text: e.message };
} else {
notice = { kind: 'error', text: 'Falha ao carregar dados de relatórios.' };
}
} finally { } finally {
isLoading = false; isLoading = false;
} }
@@ -69,7 +78,7 @@
} }
function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string { function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string {
if (data.length === 0) return ""; if (data.length === 0) return '';
const n = data.length; const n = data.length;
let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`; let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`;
@@ -80,82 +89,166 @@
} }
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`; path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`;
path += " Z"; path += ' Z';
return path; return path;
} }
</script> </script>
<div class="container mx-auto px-4 py-6 max-w-7xl"> <div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4"> <div class="breadcrumbs mb-4 text-sm">
<ul> <ul>
<li><a href={resolve('/')} class="hover:text-primary">Dashboard</a></li> <li><a href={resolve('/')} class="hover:text-primary">Dashboard</a></li>
<li><a href={resolve('/recursos-humanos')} class="hover:text-primary">Recursos Humanos</a></li> <li>
<li><a href={resolve('/recursos-humanos/funcionarios')} class="hover:text-primary">Funcionários</a></li> <a href={resolve('/recursos-humanos')} class="hover:text-primary">Recursos Humanos</a>
<li class="font-semibold text-primary">Relatórios</li> </li>
<li>
<a href={resolve('/recursos-humanos/funcionarios')} class="hover:text-primary"
>Funcionários</a
>
</li>
<li class="text-primary font-semibold">Relatórios</li>
</ul> </ul>
</div> </div>
<!-- Header --> <!-- Header -->
<div class="flex items-center gap-4 mb-8"> <div class="mb-8 flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl"> <div class="bg-primary/10 rounded-xl p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> xmlns="http://www.w3.org/2000/svg"
class="text-primary h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg> </svg>
</div> </div>
<div> <div>
<h1 class="text-3xl font-bold text-base-content">Relatórios de Funcionários</h1> <h1 class="text-base-content text-3xl font-bold">Relatórios de Funcionários</h1>
<p class="text-base-content/60 mt-1">Análise de distribuição de salários e funcionários por símbolo</p> <p class="text-base-content/60 mt-1">
Análise de distribuição de salários e funcionários por símbolo
</p>
</div> </div>
</div> </div>
{#if notice} {#if notice}
<div class="alert mb-6" class:alert-error={notice.kind === "error"} class:alert-success={notice.kind === "success"}> <div
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"> class="alert mb-6"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> class:alert-error={notice.kind === 'error'}
class:alert-success={notice.kind === 'success'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg> </svg>
<span>{notice.text}</span> <span>{notice.text}</span>
</div> </div>
{/if} {/if}
{#if isLoading} {#if isLoading}
<div class="flex justify-center items-center py-20"> <div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span> <span class="loading loading-spinner loading-lg text-primary"></span>
</div> </div>
{:else} {:else}
<div class="space-y-6 chart-container"> <div class="chart-container space-y-6">
<!-- Gráfico 1: Símbolo x Salário (Valor) - Layer Chart --> <!-- Gráfico 1: Símbolo x Salário (Valor) - Layer Chart -->
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow border border-base-300"> <div
class="card bg-base-100 border-base-300 border shadow-lg transition-shadow hover:shadow-xl"
>
<div class="card-body p-6"> <div class="card-body p-6">
<div class="flex items-center gap-3 mb-6"> <div class="mb-6 flex items-center gap-3">
<div class="p-2.5 bg-primary/10 rounded-lg"> <div class="bg-primary/10 rounded-lg p-2.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-bold text-base-content">Distribuição de Salários por Símbolo</h3> <h3 class="text-base-content text-lg font-bold">
<p class="text-sm text-base-content/60 mt-0.5">Valores dos símbolos cadastrados no sistema</p> Distribuição de Salários por Símbolo
</h3>
<p class="text-base-content/60 mt-0.5 text-sm">
Valores dos símbolos cadastrados no sistema
</p>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4"> <div class="bg-base-200/30 w-full overflow-x-auto rounded-xl p-4">
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: salário por símbolo"> <svg
width={chartWidth}
height={chartHeight}
role="img"
aria-label="Gráfico de área: salário por símbolo"
>
{#if rows.length === 0} {#if rows.length === 0}
<text x="16" y="32" class="opacity-60">Sem dados</text> <text x="16" y="32" class="opacity-60">Sem dados</text>
{:else} {:else}
{@const max = getMax(rows, (r) => r.valor)} {@const max = getMax(rows, (r) => r.valor)}
<!-- Grid lines --> <!-- Grid lines -->
{#each [0,1,2,3,4,5] as t} {#each [0, 1, 2, 3, 4, 5] as t (t)}
{@const val = Math.round((max/5) * t)} {@const val = Math.round((max / 5) * t)}
{@const y = chartHeight - padding.bottom - scaleY(val, max)} {@const y = chartHeight - padding.bottom - scaleY(val, max)}
<line x1={padding.left} y1={y} x2={chartWidth - padding.right} y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-dasharray="4,4" /> <line
<text x={padding.left - 8} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{`R$ ${val.toLocaleString('pt-BR')}`}</text> x1={padding.left}
y1={y}
x2={chartWidth - padding.right}
y2={y}
stroke="currentColor"
stroke-opacity="0.1"
stroke-dasharray="4,4"
/>
<text
x={padding.left - 8}
y={y + 4}
text-anchor="end"
class="text-[10px] opacity-70">{`R$ ${val.toLocaleString('pt-BR')}`}</text
>
{/each} {/each}
<!-- Eixos --> <!-- Eixos -->
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" /> <line
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" /> x1={padding.left}
y1={chartHeight - padding.bottom}
x2={chartWidth - padding.right}
y2={chartHeight - padding.bottom}
stroke="currentColor"
stroke-opacity="0.3"
stroke-width="2"
/>
<line
x1={padding.left}
y1={padding.top}
x2={padding.left}
y2={chartHeight - padding.bottom}
stroke="currentColor"
stroke-opacity="0.3"
stroke-width="2"
/>
<!-- Area fill (camada) --> <!-- Area fill (camada) -->
<path <path
@@ -166,32 +259,54 @@
<!-- Line --> <!-- Line -->
<polyline <polyline
points={rows.map((r, i) => { points={rows
.map((r, i) => {
const x = getX(i, rows.length); const x = getX(i, rows.length);
const y = chartHeight - padding.bottom - scaleY(r.valor, max); const y = chartHeight - padding.bottom - scaleY(r.valor, max);
return `${x},${y}`; return `${x},${y}`;
}).join(' ')} })
.join(' ')}
fill="none" fill="none"
stroke="rgb(59, 130, 246)" stroke="rgb(59, 130, 246)"
stroke-width="3" stroke-width="3"
/> />
<!-- Data points --> <!-- Data points -->
{#each rows as r, i} {#each rows as r, i (r._id)}
{@const x = getX(i, rows.length)} {@const x = getX(i, rows.length)}
{@const y = chartHeight - padding.bottom - scaleY(r.valor, max)} {@const y = chartHeight - padding.bottom - scaleY(r.valor, max)}
<circle cx={x} cy={y} r="5" fill="rgb(59, 130, 246)" stroke="white" stroke-width="2" /> <circle
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-primary"> cx={x}
cy={y}
r="5"
fill="rgb(59, 130, 246)"
stroke="white"
stroke-width="2"
/>
<text
{x}
y={y - 12}
text-anchor="middle"
class="fill-primary text-[10px] font-semibold"
>
{`R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`} {`R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`}
</text> </text>
{/each} {/each}
<!-- Eixo X labels --> <!-- Eixo X labels -->
{#each rows as r, i} {#each rows as r, i (r._id)}
{@const x = getX(i, rows.length)} {@const x = getX(i, rows.length)}
<foreignObject x={x - 40} y={chartHeight - padding.bottom + 15} width="80" height="70"> <foreignObject
x={x - 40}
y={chartHeight - padding.bottom + 15}
width="80"
height="70"
>
<div class="flex items-center justify-center text-center"> <div class="flex items-center justify-center text-center">
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;"> <span
class="text-base-content/80 text-[11px] leading-tight font-medium"
style="word-wrap: break-word; hyphens: auto;"
>
{r.nome} {r.nome}
</span> </span>
</div> </div>
@@ -212,37 +327,88 @@
</div> </div>
<!-- Gráfico 2: Quantidade de Funcionários por Símbolo - Layer Chart --> <!-- Gráfico 2: Quantidade de Funcionários por Símbolo - Layer Chart -->
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow border border-base-300"> <div
class="card bg-base-100 border-base-300 border shadow-lg transition-shadow hover:shadow-xl"
>
<div class="card-body p-6"> <div class="card-body p-6">
<div class="flex items-center gap-3 mb-6"> <div class="mb-6 flex items-center gap-3">
<div class="p-2.5 bg-secondary/10 rounded-lg"> <div class="bg-secondary/10 rounded-lg p-2.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> xmlns="http://www.w3.org/2000/svg"
class="text-secondary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg> </svg>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3> <h3 class="text-base-content text-lg font-bold">
<p class="text-sm text-base-content/60 mt-0.5">Quantidade de funcionários alocados em cada símbolo</p> Distribuição de Funcionários por Símbolo
</h3>
<p class="text-base-content/60 mt-0.5 text-sm">
Quantidade de funcionários alocados em cada símbolo
</p>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4"> <div class="bg-base-200/30 w-full overflow-x-auto rounded-xl p-4">
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: quantidade por símbolo"> <svg
width={chartWidth}
height={chartHeight}
role="img"
aria-label="Gráfico de área: quantidade por símbolo"
>
{#if rows.length === 0} {#if rows.length === 0}
<text x="16" y="32" class="opacity-60">Sem dados</text> <text x="16" y="32" class="opacity-60">Sem dados</text>
{:else} {:else}
{@const maxC = getMax(rows, (r) => r.count)} {@const maxC = getMax(rows, (r) => r.count)}
<!-- Grid lines --> <!-- Grid lines -->
{#each [0,1,2,3,4,5] as t} {#each [0, 1, 2, 3, 4, 5] as t (t)}
{@const val = Math.round((maxC/5) * t)} {@const val = Math.round((maxC / 5) * t)}
{@const y = chartHeight - padding.bottom - scaleY(val, Math.max(1, maxC))} {@const y = chartHeight - padding.bottom - scaleY(val, Math.max(1, maxC))}
<line x1={padding.left} y1={y} x2={chartWidth - padding.right} y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-dasharray="4,4" /> <line
<text x={padding.left - 6} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{val}</text> x1={padding.left}
y1={y}
x2={chartWidth - padding.right}
y2={y}
stroke="currentColor"
stroke-opacity="0.1"
stroke-dasharray="4,4"
/>
<text
x={padding.left - 6}
y={y + 4}
text-anchor="end"
class="text-[10px] opacity-70">{val}</text
>
{/each} {/each}
<!-- Eixos --> <!-- Eixos -->
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" /> <line
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" /> x1={padding.left}
y1={chartHeight - padding.bottom}
x2={chartWidth - padding.right}
y2={chartHeight - padding.bottom}
stroke="currentColor"
stroke-opacity="0.3"
stroke-width="2"
/>
<line
x1={padding.left}
y1={padding.top}
x2={padding.left}
y2={chartHeight - padding.bottom}
stroke="currentColor"
stroke-opacity="0.3"
stroke-width="2"
/>
<!-- Area fill (camada) --> <!-- Area fill (camada) -->
<path <path
@@ -253,32 +419,54 @@
<!-- Line --> <!-- Line -->
<polyline <polyline
points={rows.map((r, i) => { points={rows
.map((r, i) => {
const x = getX(i, rows.length); const x = getX(i, rows.length);
const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC)); const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC));
return `${x},${y}`; return `${x},${y}`;
}).join(' ')} })
.join(' ')}
fill="none" fill="none"
stroke="rgb(236, 72, 153)" stroke="rgb(236, 72, 153)"
stroke-width="3" stroke-width="3"
/> />
<!-- Data points --> <!-- Data points -->
{#each rows as r, i} {#each rows as r, i (r._id)}
{@const x = getX(i, rows.length)} {@const x = getX(i, rows.length)}
{@const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC))} {@const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC))}
<circle cx={x} cy={y} r="5" fill="rgb(236, 72, 153)" stroke="white" stroke-width="2" /> <circle
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-secondary"> cx={x}
cy={y}
r="5"
fill="rgb(236, 72, 153)"
stroke="white"
stroke-width="2"
/>
<text
{x}
y={y - 12}
text-anchor="middle"
class="fill-secondary text-[10px] font-semibold"
>
{r.count} {r.count}
</text> </text>
{/each} {/each}
<!-- Eixo X labels --> <!-- Eixo X labels -->
{#each rows as r, i} {#each rows as r, i (r._id)}
{@const x = getX(i, rows.length)} {@const x = getX(i, rows.length)}
<foreignObject x={x - 40} y={chartHeight - padding.bottom + 15} width="80" height="70"> <foreignObject
x={x - 40}
y={chartHeight - padding.bottom + 15}
width="80"
height="70"
>
<div class="flex items-center justify-center text-center"> <div class="flex items-center justify-center text-center">
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;"> <span
class="text-base-content/80 text-[11px] leading-tight font-medium"
style="word-wrap: break-word; hyphens: auto;"
>
{r.nome} {r.nome}
</span> </span>
</div> </div>
@@ -299,22 +487,37 @@
</div> </div>
<!-- Tabela Resumo --> <!-- Tabela Resumo -->
<div class="card bg-base-100 shadow-lg border border-base-300"> <div class="card bg-base-100 border-base-300 border shadow-lg">
<div class="card-body p-6"> <div class="card-body p-6">
<div class="flex items-center gap-3 mb-6"> <div class="mb-6 flex items-center gap-3">
<div class="p-2.5 bg-accent/10 rounded-lg"> <div class="bg-accent/10 rounded-lg p-2.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" /> xmlns="http://www.w3.org/2000/svg"
class="text-accent h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg> </svg>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-bold text-base-content">Tabela Resumo - Símbolos e Funcionários</h3> <h3 class="text-base-content text-lg font-bold">
<p class="text-sm text-base-content/60 mt-0.5">Visão detalhada dos dados apresentados nos gráficos</p> Tabela Resumo - Símbolos e Funcionários
</h3>
<p class="text-base-content/60 mt-0.5 text-sm">
Visão detalhada dos dados apresentados nos gráficos
</p>
</div> </div>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra"> <table class="table-zebra table">
<thead> <thead>
<tr> <tr>
<th class="bg-base-200">Símbolo</th> <th class="bg-base-200">Símbolo</th>
@@ -326,34 +529,54 @@
<tbody> <tbody>
{#if rows.length === 0} {#if rows.length === 0}
<tr> <tr>
<td colspan="4" class="text-center text-base-content/60 py-8">Nenhum dado disponível</td> <td colspan="4" class="text-base-content/60 py-8 text-center"
>Nenhum dado disponível</td
>
</tr> </tr>
{:else} {:else}
{#each rows as row} {#each rows as row (row._id)}
<tr class="hover"> <tr class="hover">
<td class="font-semibold">{row.nome}</td> <td class="font-semibold">{row.nome}</td>
<td class="text-right font-mono"> <td class="text-right font-mono">
{row.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {row.valor.toLocaleString('pt-BR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</td> </td>
<td class="text-right"> <td class="text-right">
<span class="badge badge-primary badge-outline">{row.count}</span> <span class="badge badge-primary badge-outline">{row.count}</span>
</td> </td>
<td class="text-right font-mono font-semibold text-primary"> <td class="text-primary text-right font-mono font-semibold">
{(row.valor * row.count).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {(row.valor * row.count).toLocaleString('pt-BR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</td> </td>
</tr> </tr>
{/each} {/each}
<!-- Total Geral --> <!-- Total Geral -->
<tr class="font-bold bg-base-200 border-t-2 border-base-300"> <tr class="bg-base-200 border-base-300 border-t-2 font-bold">
<td>TOTAL GERAL</td> <td>TOTAL GERAL</td>
<td class="text-right font-mono"> <td class="text-right font-mono">
{rows.reduce((sum, r) => sum + r.valor, 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {rows
.reduce((sum, r) => sum + r.valor, 0)
.toLocaleString('pt-BR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</td> </td>
<td class="text-right"> <td class="text-right">
<span class="badge badge-primary">{rows.reduce((sum, r) => sum + r.count, 0)}</span> <span class="badge badge-primary"
>{rows.reduce((sum, r) => sum + r.count, 0)}</span
>
</td> </td>
<td class="text-right font-mono text-primary text-lg"> <td class="text-primary text-right font-mono text-lg">
{rows.reduce((sum, r) => sum + (r.valor * r.count), 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {rows
.reduce((sum, r) => sum + r.valor * r.count, 0)
.toLocaleString('pt-BR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</td> </td>
</tr> </tr>
{/if} {/if}

View File

@@ -1,51 +1,45 @@
<script lang="ts"> <script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte"; import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import type { FunctionReference } from 'convex/server';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
const configAtual = useQuery(api.configuracaoEmail.obterConfigEmail, {}); const configAtual = useQuery(api.configuracaoEmail.obterConfigEmail);
let servidor = $state(""); let servidor = $state('');
let porta = $state(587); let porta = $state(587);
let usuario = $state(""); let usuario = $state('');
let senha = $state(""); let senha = $state('');
let emailRemetente = $state(""); let emailRemetente = $state('');
let nomeRemetente = $state(""); let nomeRemetente = $state('');
let usarSSL = $state(false); let usarSSL = $state(false);
let usarTLS = $state(true); let usarTLS = $state(true);
let processando = $state(false); let processando = $state(false);
let testando = $state(false); let testando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>( let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
null,
);
function mostrarMensagem(tipo: "success" | "error", texto: string) { function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
mensagem = { tipo, texto }; mensagem = { tipo, texto };
setTimeout(() => { setTimeout(() => {
mensagem = null; mensagem = null;
}, 5000); }, 5000);
} }
// Carregar config existente let dataLoaded = $state(false);
// Carregar config existente apenas uma vez
$effect(() => { $effect(() => {
if (configAtual?.data) { if (configAtual?.data && !dataLoaded) {
servidor = configAtual.data.servidor || ""; servidor = configAtual.data.servidor || '';
porta = configAtual.data.porta || 587; porta = configAtual.data.porta || 587;
usuario = configAtual.data.usuario || ""; usuario = configAtual.data.usuario || '';
emailRemetente = configAtual.data.emailRemetente || ""; emailRemetente = configAtual.data.emailRemetente || '';
nomeRemetente = configAtual.data.nomeRemetente || ""; nomeRemetente = configAtual.data.nomeRemetente || '';
usarSSL = configAtual.data.usarSSL || false; usarSSL = configAtual.data.usarSSL || false;
usarTLS = configAtual.data.usarTLS || true; usarTLS = configAtual.data.usarTLS || true;
} dataLoaded = true;
});
// Tornar SSL e TLS mutuamente exclusivos
$effect(() => {
if (usarSSL && usarTLS) {
// Se ambos estão marcados, priorizar TLS por padrão
usarSSL = false;
} }
}); });
@@ -72,62 +66,61 @@
!emailRemetente?.trim() || !emailRemetente?.trim() ||
!nomeRemetente?.trim() !nomeRemetente?.trim()
) { ) {
mostrarMensagem("error", "Preencha todos os campos obrigatórios"); mostrarMensagem('error', 'Preencha todos os campos obrigatórios');
return; return;
} }
// Validação de porta (1-65535) // Validação de porta (1-65535)
const portaNum = Number(porta); const portaNum = Number(porta);
if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) { if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) {
mostrarMensagem("error", "Porta deve ser um número entre 1 e 65535"); mostrarMensagem('error', 'Porta deve ser um número entre 1 e 65535');
return; return;
} }
// Validação de formato de email // Validação de formato de email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailRemetente.trim())) { if (!emailRegex.test(emailRemetente.trim())) {
mostrarMensagem("error", "Email remetente inválido"); mostrarMensagem('error', 'Email remetente inválido');
return; return;
} }
// Validação de senha: obrigatória apenas se não houver configuração existente // Validação de senha: obrigatória apenas se não houver configuração existente
const temConfigExistente = configAtual?.data?.ativo; const temConfigExistente = configAtual?.data?.ativo;
if (!temConfigExistente && !senha) { if (!temConfigExistente && !senha) {
mostrarMensagem("error", "Senha é obrigatória para nova configuração"); mostrarMensagem('error', 'Senha é obrigatória para nova configuração');
return; return;
} }
if (!currentUser?.data) { if (!currentUser?.data) {
mostrarMensagem("error", "Usuário não autenticado"); mostrarMensagem('error', 'Usuário não autenticado');
return; return;
} }
processando = true; processando = true;
try { try {
const resultado = await client.mutation( const resultado = await client.mutation(api.configuracaoEmail.salvarConfigEmail, {
api.configuracaoEmail.salvarConfigEmail,
{
servidor: servidor.trim(), servidor: servidor.trim(),
porta: portaNum, porta: portaNum,
usuario: usuario.trim(), usuario: usuario.trim(),
senha: senha || "", // Senha vazia será tratada no backend senha: senha || '', // Senha vazia será tratada no backend
emailRemetente: emailRemetente.trim(), emailRemetente: emailRemetente.trim(),
nomeRemetente: nomeRemetente.trim(), nomeRemetente: nomeRemetente.trim(),
usarSSL, usarSSL,
usarTLS, usarTLS,
configuradoPorId: currentUser.data._id as Id<"usuarios">, configuradoPorId: currentUser.data._id as Id<'usuarios'>
}, });
);
if (resultado.sucesso) { if (resultado.sucesso) {
mostrarMensagem("success", "Configuração salva com sucesso!"); mostrarMensagem('success', 'Configuração salva com sucesso!');
senha = ""; // Limpar senha senha = ''; // Limpar senha
} else { } else {
mostrarMensagem("error", resultado.erro); mostrarMensagem('error', resultado.erro);
}
} catch (error) {
if (error instanceof Error) {
console.error('Erro ao salvar configuração:', error);
mostrarMensagem('error', error.message || 'Erro ao salvar configuração');
} }
} catch (error: any) {
console.error("Erro ao salvar configuração:", error);
mostrarMensagem("error", error.message || "Erro ao salvar configuração");
} finally { } finally {
processando = false; processando = false;
} }
@@ -135,66 +128,56 @@
async function testarConexao() { async function testarConexao() {
if (!servidor?.trim() || !porta || !usuario?.trim() || !senha) { if (!servidor?.trim() || !porta || !usuario?.trim() || !senha) {
mostrarMensagem("error", "Preencha os dados de conexão antes de testar"); mostrarMensagem('error', 'Preencha os dados de conexão antes de testar');
return; return;
} }
// Validação de porta // Validação de porta
const portaNum = Number(porta); const portaNum = Number(porta);
if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) { if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) {
mostrarMensagem("error", "Porta deve ser um número entre 1 e 65535"); mostrarMensagem('error', 'Porta deve ser um número entre 1 e 65535');
return; return;
} }
testando = true; testando = true;
try { try {
const resultado = await client.action( const resultado = await client.action(api.configuracaoEmail.testarConexaoSMTP, {
api.configuracaoEmail.testarConexaoSMTP,
{
servidor: servidor.trim(), servidor: servidor.trim(),
porta: portaNum, porta: portaNum,
usuario: usuario.trim(), usuario: usuario.trim(),
senha: senha, senha: senha,
usarSSL, usarSSL,
usarTLS, usarTLS
}, });
);
if (resultado.sucesso) { if (resultado.sucesso) {
mostrarMensagem( mostrarMensagem('success', 'Conexão testada com sucesso! Servidor SMTP está respondendo.');
"success",
"Conexão testada com sucesso! Servidor SMTP está respondendo.",
);
} else { } else {
mostrarMensagem("error", `Erro ao testar conexão: ${resultado.erro}`); mostrarMensagem('error', `Erro ao testar conexão: ${resultado.erro}`);
}
} catch (error) {
if (error instanceof Error) {
console.error('Erro ao testar conexão:', error);
mostrarMensagem('error', error.message || 'Erro ao conectar com o servidor SMTP');
} }
} catch (error: any) {
console.error("Erro ao testar conexão:", error);
mostrarMensagem(
"error",
error.message || "Erro ao conectar com o servidor SMTP",
);
} finally { } finally {
testando = false; testando = false;
} }
} }
const statusConfig = $derived( const statusConfig = $derived(configAtual?.data?.ativo ? 'Configurado' : 'Não configurado');
configAtual?.data?.ativo ? "Configurado" : "Não configurado",
);
const isLoading = $derived(configAtual === undefined); const isLoading = $derived(configAtual === undefined);
const hasError = $derived(configAtual === null && !isLoading);
</script> </script>
<div class="container mx-auto px-4 py-6 max-w-4xl"> <div class="container mx-auto max-w-4xl px-4 py-6">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between mb-6"> <div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="p-3 bg-secondary/10 rounded-xl"> <div class="bg-secondary/10 rounded-xl p-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-secondary" class="text-secondary h-8 w-8"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -208,9 +191,7 @@
</svg> </svg>
</div> </div>
<div> <div>
<h1 class="text-3xl font-bold text-base-content"> <h1 class="text-base-content text-3xl font-bold">Configurações de Email (SMTP)</h1>
Configurações de Email (SMTP)
</h1>
<p class="text-base-content/60 mt-1"> <p class="text-base-content/60 mt-1">
Configurar servidor de email para envio de notificações Configurar servidor de email para envio de notificações
</p> </p>
@@ -222,16 +203,16 @@
{#if mensagem} {#if mensagem}
<div <div
class="alert mb-6" class="alert mb-6"
class:alert-success={mensagem.tipo === "success"} class:alert-success={mensagem.tipo === 'success'}
class:alert-error={mensagem.tipo === "error"} class:alert-error={mensagem.tipo === 'error'}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
{#if mensagem.tipo === "success"} {#if mensagem.tipo === 'success'}
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@@ -261,16 +242,12 @@
<!-- Status --> <!-- Status -->
{#if !isLoading} {#if !isLoading}
<div <div class="alert {configAtual?.data?.ativo ? 'alert-success' : 'alert-warning'} mb-6">
class="alert {configAtual?.data?.ativo
? 'alert-success'
: 'alert-warning'} mb-6"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6" class="h-6 w-6 shrink-0 stroke-current"
> >
{#if configAtual?.data?.ativo} {#if configAtual?.data?.ativo}
<path <path
@@ -292,9 +269,7 @@
<strong>Status:</strong> <strong>Status:</strong>
{statusConfig} {statusConfig}
{#if configAtual?.data?.testadoEm} {#if configAtual?.data?.testadoEm}
- Última conexão testada em {new Date( - Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')}
configAtual.data.testadoEm,
).toLocaleString("pt-BR")}
{/if} {/if}
</span> </span>
</div> </div>
@@ -306,7 +281,7 @@
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-4">Dados do Servidor SMTP</h2> <h2 class="card-title mb-4">Dados do Servidor SMTP</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Servidor --> <!-- Servidor -->
<div class="form-control md:col-span-1"> <div class="form-control md:col-span-1">
<label class="label" for="smtp-servidor"> <label class="label" for="smtp-servidor">
@@ -320,9 +295,7 @@
class="input input-bordered" class="input input-bordered"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt">Ex: smtp.gmail.com, smtp.office365.com</span>
>Ex: smtp.gmail.com, smtp.office365.com</span
>
</div> </div>
</div> </div>
@@ -339,8 +312,7 @@
class="input input-bordered" class="input input-bordered"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt">Comum: 587 (TLS), 465 (SSL), 25</span <span class="label-text-alt">Comum: 587 (TLS), 465 (SSL), 25</span>
>
</div> </div>
</div> </div>
@@ -412,7 +384,7 @@
<!-- Opções de Segurança --> <!-- Opções de Segurança -->
<div class="divider"></div> <div class="divider"></div>
<h3 class="font-bold mb-2">Configurações de Segurança</h3> <h3 class="mb-2 font-bold">Configurações de Segurança</h3>
<div class="flex flex-wrap gap-6"> <div class="flex flex-wrap gap-6">
<div class="form-control"> <div class="form-control">
@@ -441,7 +413,7 @@
</div> </div>
<!-- Ações --> <!-- Ações -->
<div class="card-actions justify-end mt-6 gap-3"> <div class="card-actions mt-6 justify-end gap-3">
<button <button
class="btn btn-outline btn-info" class="btn btn-outline btn-info"
onclick={testarConexao} onclick={testarConexao}
@@ -499,12 +471,12 @@
{/if} {/if}
<!-- Exemplos Comuns --> <!-- Exemplos Comuns -->
<div class="card bg-base-100 shadow-xl mt-6"> <div class="card bg-base-100 mt-6 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-4">Exemplos de Configuração</h2> <h2 class="card-title mb-4">Exemplos de Configuração</h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-sm"> <table class="table-sm table">
<thead> <thead>
<tr> <tr>
<th>Provedor</th> <th>Provedor</th>
@@ -550,7 +522,7 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6" class="h-6 w-6 shrink-0 stroke-current"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@@ -561,13 +533,11 @@
</svg> </svg>
<div> <div>
<p> <p>
<strong>Dica de Segurança:</strong> Para Gmail e outros provedores, você <strong>Dica de Segurança:</strong> Para Gmail e outros provedores, você pode precisar gerar
pode precisar gerar uma "senha de app" específica em vez de usar sua senha uma "senha de app" específica em vez de usar sua senha principal.
principal.
</p> </p>
<p class="text-sm mt-1"> <p class="mt-1 text-sm">
Gmail: Conta Google → Segurança → Verificação em duas etapas → Senhas de Gmail: Conta Google → Segurança → Verificação em duas etapas → Senhas de app
app
</p> </p>
</div> </div>
</div> </div>