Compare commits
10 Commits
call-audio
...
refinament
| Author | SHA1 | Date | |
|---|---|---|---|
| 409872352c | |||
| d4a3214451 | |||
| 649b9b145c | |||
| ae4fc1c4d5 | |||
| 2d7761ee94 | |||
| 9dc816977d | |||
| 3cc35d3a1e | |||
|
|
7871b87bb9 | ||
| b8a2e67f3a | |||
| ce94eb53b3 |
275
.agent/rules/convex-svelte-guidelines.md
Normal file
275
.agent/rules/convex-svelte-guidelines.md
Normal 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 Svelte‑specific 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** (Node‑only) 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. Svelte‑Specific Tips
|
||||
|
||||
| Topic | Recommendation |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Store‑based 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 client‑side 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._
|
||||
28
.agent/rules/svelte-rules.md
Normal file
28
.agent/rules/svelte-rules.md
Normal 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.
|
||||
@@ -1,31 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { onMount } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { onMount } from 'svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
type Row = { _id: string; nome: string; valor: number; count: number };
|
||||
let rows: Array<Row> = $state<Array<Row>>([]);
|
||||
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);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const simbolos = await client.query(api.simbolos.getAll, {} as any);
|
||||
const funcionarios = await client.query(api.funcionarios.getAll, {} as any);
|
||||
const simbolos = await client.query(api.simbolos.getAll, {});
|
||||
const funcionarios = await client.query(api.funcionarios.getAll, {});
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
for (const f of funcionarios) counts[f.simboloId] = (counts[f.simboloId] ?? 0) + 1;
|
||||
rows = simbolos.map((s: any) => ({
|
||||
for (const f of funcionarios) {
|
||||
const sId = String(f.simboloId);
|
||||
counts[sId] = (counts[sId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
rows = simbolos.map((s) => ({
|
||||
_id: String(s._id),
|
||||
nome: s.nome as string,
|
||||
nome: s.nome,
|
||||
valor: Number(s.valor || 0),
|
||||
count: counts[String(s._id)] ?? 0,
|
||||
count: counts[String(s._id)] ?? 0
|
||||
}));
|
||||
} 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 {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -69,7 +78,7 @@
|
||||
}
|
||||
|
||||
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;
|
||||
let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`;
|
||||
|
||||
@@ -80,82 +89,166 @@
|
||||
}
|
||||
|
||||
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`;
|
||||
path += " Z";
|
||||
path += ' Z';
|
||||
return path;
|
||||
}
|
||||
</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 -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<div class="breadcrumbs mb-4 text-sm">
|
||||
<ul>
|
||||
<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><a href={resolve('/recursos-humanos/funcionarios')} class="hover:text-primary">Funcionários</a></li>
|
||||
<li class="font-semibold text-primary">Relatórios</li>
|
||||
<li>
|
||||
<a href={resolve('/recursos-humanos')} class="hover:text-primary">Recursos Humanos</a>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" 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" />
|
||||
<div class="mb-8 flex items-center gap-4">
|
||||
<div class="bg-primary/10 rounded-xl p-3">
|
||||
<svg
|
||||
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>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if notice}
|
||||
<div class="alert mb-6" 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="stroke-current shrink-0 w-6 h-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>
|
||||
<div
|
||||
class="alert mb-6"
|
||||
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>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#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>
|
||||
</div>
|
||||
{: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 -->
|
||||
<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="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-primary/10 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" 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" />
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<div class="bg-primary/10 rounded-lg p-2.5">
|
||||
<svg
|
||||
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>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-bold text-base-content">Distribuição de Salários por Símbolo</h3>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">Valores dos símbolos cadastrados no sistema</p>
|
||||
<h3 class="text-base-content text-lg font-bold">
|
||||
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 class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4">
|
||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: salário por símbolo">
|
||||
<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"
|
||||
>
|
||||
{#if rows.length === 0}
|
||||
<text x="16" y="32" class="opacity-60">Sem dados</text>
|
||||
{:else}
|
||||
{@const max = getMax(rows, (r) => r.valor)}
|
||||
|
||||
<!-- Grid lines -->
|
||||
{#each [0,1,2,3,4,5] as t}
|
||||
{@const val = Math.round((max/5) * t)}
|
||||
{#each [0, 1, 2, 3, 4, 5] as t (t)}
|
||||
{@const val = Math.round((max / 5) * t)}
|
||||
{@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" />
|
||||
<text x={padding.left - 8} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{`R$ ${val.toLocaleString('pt-BR')}`}</text>
|
||||
<line
|
||||
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}
|
||||
|
||||
<!-- 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 x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
||||
<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
|
||||
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) -->
|
||||
<path
|
||||
@@ -166,32 +259,54 @@
|
||||
|
||||
<!-- Line -->
|
||||
<polyline
|
||||
points={rows.map((r, i) => {
|
||||
points={rows
|
||||
.map((r, i) => {
|
||||
const x = getX(i, rows.length);
|
||||
const y = chartHeight - padding.bottom - scaleY(r.valor, max);
|
||||
return `${x},${y}`;
|
||||
}).join(' ')}
|
||||
})
|
||||
.join(' ')}
|
||||
fill="none"
|
||||
stroke="rgb(59, 130, 246)"
|
||||
stroke-width="3"
|
||||
/>
|
||||
|
||||
<!-- Data points -->
|
||||
{#each rows as r, i}
|
||||
{#each rows as r, i (r._id)}
|
||||
{@const x = getX(i, rows.length)}
|
||||
{@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" />
|
||||
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-primary">
|
||||
<circle
|
||||
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 })}`}
|
||||
</text>
|
||||
{/each}
|
||||
|
||||
<!-- Eixo X labels -->
|
||||
{#each rows as r, i}
|
||||
{#each rows as r, i (r._id)}
|
||||
{@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">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -212,37 +327,88 @@
|
||||
</div>
|
||||
|
||||
<!-- 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="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-secondary/10 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-secondary" 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" />
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<div class="bg-secondary/10 rounded-lg p-2.5">
|
||||
<svg
|
||||
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>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">Quantidade de funcionários alocados em cada símbolo</p>
|
||||
<h3 class="text-base-content text-lg font-bold">
|
||||
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 class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4">
|
||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: quantidade por símbolo">
|
||||
<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"
|
||||
>
|
||||
{#if rows.length === 0}
|
||||
<text x="16" y="32" class="opacity-60">Sem dados</text>
|
||||
{:else}
|
||||
{@const maxC = getMax(rows, (r) => r.count)}
|
||||
|
||||
<!-- Grid lines -->
|
||||
{#each [0,1,2,3,4,5] as t}
|
||||
{@const val = Math.round((maxC/5) * t)}
|
||||
{#each [0, 1, 2, 3, 4, 5] as t (t)}
|
||||
{@const val = Math.round((maxC / 5) * t)}
|
||||
{@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" />
|
||||
<text x={padding.left - 6} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{val}</text>
|
||||
<line
|
||||
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}
|
||||
|
||||
<!-- 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 x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
||||
<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
|
||||
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) -->
|
||||
<path
|
||||
@@ -253,32 +419,54 @@
|
||||
|
||||
<!-- Line -->
|
||||
<polyline
|
||||
points={rows.map((r, i) => {
|
||||
points={rows
|
||||
.map((r, i) => {
|
||||
const x = getX(i, rows.length);
|
||||
const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC));
|
||||
return `${x},${y}`;
|
||||
}).join(' ')}
|
||||
})
|
||||
.join(' ')}
|
||||
fill="none"
|
||||
stroke="rgb(236, 72, 153)"
|
||||
stroke-width="3"
|
||||
/>
|
||||
|
||||
<!-- Data points -->
|
||||
{#each rows as r, i}
|
||||
{#each rows as r, i (r._id)}
|
||||
{@const x = getX(i, rows.length)}
|
||||
{@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" />
|
||||
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-secondary">
|
||||
<circle
|
||||
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}
|
||||
</text>
|
||||
{/each}
|
||||
|
||||
<!-- Eixo X labels -->
|
||||
{#each rows as r, i}
|
||||
{#each rows as r, i (r._id)}
|
||||
{@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">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -299,22 +487,37 @@
|
||||
</div>
|
||||
|
||||
<!-- 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="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-accent/10 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-accent" 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" />
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<div class="bg-accent/10 rounded-lg p-2.5">
|
||||
<svg
|
||||
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>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-bold text-base-content">Tabela Resumo - Símbolos e Funcionários</h3>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">Visão detalhada dos dados apresentados nos gráficos</p>
|
||||
<h3 class="text-base-content text-lg font-bold">
|
||||
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 class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bg-base-200">Símbolo</th>
|
||||
@@ -326,34 +529,54 @@
|
||||
<tbody>
|
||||
{#if rows.length === 0}
|
||||
<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>
|
||||
{:else}
|
||||
{#each rows as row}
|
||||
{#each rows as row (row._id)}
|
||||
<tr class="hover">
|
||||
<td class="font-semibold">{row.nome}</td>
|
||||
<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 class="text-right">
|
||||
<span class="badge badge-primary badge-outline">{row.count}</span>
|
||||
</td>
|
||||
<td class="text-right font-mono font-semibold text-primary">
|
||||
{(row.valor * row.count).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
<td class="text-primary text-right font-mono font-semibold">
|
||||
{(row.valor * row.count).toLocaleString('pt-BR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
<!-- 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 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 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 class="text-right font-mono text-primary text-lg">
|
||||
{rows.reduce((sum, r) => sum + (r.valor * r.count), 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
<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
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import type { FunctionReference } from 'convex/server';
|
||||
|
||||
const client = useConvexClient();
|
||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||
const configAtual = useQuery(api.configuracaoEmail.obterConfigEmail, {});
|
||||
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
|
||||
const configAtual = useQuery(api.configuracaoEmail.obterConfigEmail);
|
||||
|
||||
let servidor = $state("");
|
||||
let servidor = $state('');
|
||||
let porta = $state(587);
|
||||
let usuario = $state("");
|
||||
let senha = $state("");
|
||||
let emailRemetente = $state("");
|
||||
let nomeRemetente = $state("");
|
||||
let usuario = $state('');
|
||||
let senha = $state('');
|
||||
let emailRemetente = $state('');
|
||||
let nomeRemetente = $state('');
|
||||
let usarSSL = $state(false);
|
||||
let usarTLS = $state(true);
|
||||
let processando = $state(false);
|
||||
let testando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
|
||||
null,
|
||||
);
|
||||
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||
function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Carregar config existente
|
||||
let dataLoaded = $state(false);
|
||||
|
||||
// Carregar config existente apenas uma vez
|
||||
$effect(() => {
|
||||
if (configAtual?.data) {
|
||||
servidor = configAtual.data.servidor || "";
|
||||
if (configAtual?.data && !dataLoaded) {
|
||||
servidor = configAtual.data.servidor || '';
|
||||
porta = configAtual.data.porta || 587;
|
||||
usuario = configAtual.data.usuario || "";
|
||||
emailRemetente = configAtual.data.emailRemetente || "";
|
||||
nomeRemetente = configAtual.data.nomeRemetente || "";
|
||||
usuario = configAtual.data.usuario || '';
|
||||
emailRemetente = configAtual.data.emailRemetente || '';
|
||||
nomeRemetente = configAtual.data.nomeRemetente || '';
|
||||
usarSSL = configAtual.data.usarSSL || false;
|
||||
usarTLS = configAtual.data.usarTLS || true;
|
||||
}
|
||||
});
|
||||
|
||||
// Tornar SSL e TLS mutuamente exclusivos
|
||||
$effect(() => {
|
||||
if (usarSSL && usarTLS) {
|
||||
// Se ambos estão marcados, priorizar TLS por padrão
|
||||
usarSSL = false;
|
||||
dataLoaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,62 +66,61 @@
|
||||
!emailRemetente?.trim() ||
|
||||
!nomeRemetente?.trim()
|
||||
) {
|
||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||
mostrarMensagem('error', 'Preencha todos os campos obrigatórios');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validação de porta (1-65535)
|
||||
const portaNum = Number(porta);
|
||||
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;
|
||||
}
|
||||
|
||||
// Validação de formato de email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailRemetente.trim())) {
|
||||
mostrarMensagem("error", "Email remetente inválido");
|
||||
mostrarMensagem('error', 'Email remetente inválido');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validação de senha: obrigatória apenas se não houver configuração existente
|
||||
const temConfigExistente = configAtual?.data?.ativo;
|
||||
if (!temConfigExistente && !senha) {
|
||||
mostrarMensagem("error", "Senha é obrigatória para nova configuração");
|
||||
mostrarMensagem('error', 'Senha é obrigatória para nova configuração');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUser?.data) {
|
||||
mostrarMensagem("error", "Usuário não autenticado");
|
||||
mostrarMensagem('error', 'Usuário não autenticado');
|
||||
return;
|
||||
}
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
const resultado = await client.mutation(
|
||||
api.configuracaoEmail.salvarConfigEmail,
|
||||
{
|
||||
const resultado = await client.mutation(api.configuracaoEmail.salvarConfigEmail, {
|
||||
servidor: servidor.trim(),
|
||||
porta: portaNum,
|
||||
usuario: usuario.trim(),
|
||||
senha: senha || "", // Senha vazia será tratada no backend
|
||||
senha: senha || '', // Senha vazia será tratada no backend
|
||||
emailRemetente: emailRemetente.trim(),
|
||||
nomeRemetente: nomeRemetente.trim(),
|
||||
usarSSL,
|
||||
usarTLS,
|
||||
configuradoPorId: currentUser.data._id as Id<"usuarios">,
|
||||
},
|
||||
);
|
||||
configuradoPorId: currentUser.data._id as Id<'usuarios'>
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem("success", "Configuração salva com sucesso!");
|
||||
senha = ""; // Limpar senha
|
||||
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
||||
senha = ''; // Limpar senha
|
||||
} 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 {
|
||||
processando = false;
|
||||
}
|
||||
@@ -135,66 +128,56 @@
|
||||
|
||||
async function testarConexao() {
|
||||
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;
|
||||
}
|
||||
|
||||
// Validação de porta
|
||||
const portaNum = Number(porta);
|
||||
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;
|
||||
}
|
||||
|
||||
testando = true;
|
||||
try {
|
||||
const resultado = await client.action(
|
||||
api.configuracaoEmail.testarConexaoSMTP,
|
||||
{
|
||||
const resultado = await client.action(api.configuracaoEmail.testarConexaoSMTP, {
|
||||
servidor: servidor.trim(),
|
||||
porta: portaNum,
|
||||
usuario: usuario.trim(),
|
||||
senha: senha,
|
||||
usarSSL,
|
||||
usarTLS,
|
||||
},
|
||||
);
|
||||
usarTLS
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem(
|
||||
"success",
|
||||
"Conexão testada com sucesso! Servidor SMTP está respondendo.",
|
||||
);
|
||||
mostrarMensagem('success', 'Conexão testada com sucesso! Servidor SMTP está respondendo.');
|
||||
} 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 {
|
||||
testando = false;
|
||||
}
|
||||
}
|
||||
|
||||
const statusConfig = $derived(
|
||||
configAtual?.data?.ativo ? "Configurado" : "Não configurado",
|
||||
);
|
||||
const statusConfig = $derived(configAtual?.data?.ativo ? 'Configurado' : 'Não configurado');
|
||||
|
||||
const isLoading = $derived(configAtual === undefined);
|
||||
const hasError = $derived(configAtual === null && !isLoading);
|
||||
</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 -->
|
||||
<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="p-3 bg-secondary/10 rounded-xl">
|
||||
<div class="bg-secondary/10 rounded-xl p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-secondary"
|
||||
class="text-secondary h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -208,9 +191,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Configurações de Email (SMTP)
|
||||
</h1>
|
||||
<h1 class="text-base-content text-3xl font-bold">Configurações de Email (SMTP)</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Configurar servidor de email para envio de notificações
|
||||
</p>
|
||||
@@ -222,16 +203,16 @@
|
||||
{#if mensagem}
|
||||
<div
|
||||
class="alert mb-6"
|
||||
class:alert-success={mensagem.tipo === "success"}
|
||||
class:alert-error={mensagem.tipo === "error"}
|
||||
class:alert-success={mensagem.tipo === 'success'}
|
||||
class:alert-error={mensagem.tipo === 'error'}
|
||||
>
|
||||
<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"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{#if mensagem.tipo === "success"}
|
||||
{#if mensagem.tipo === 'success'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -261,16 +242,12 @@
|
||||
|
||||
<!-- Status -->
|
||||
{#if !isLoading}
|
||||
<div
|
||||
class="alert {configAtual?.data?.ativo
|
||||
? 'alert-success'
|
||||
: 'alert-warning'} mb-6"
|
||||
>
|
||||
<div class="alert {configAtual?.data?.ativo ? 'alert-success' : 'alert-warning'} mb-6">
|
||||
<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="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
{#if configAtual?.data?.ativo}
|
||||
<path
|
||||
@@ -292,9 +269,7 @@
|
||||
<strong>Status:</strong>
|
||||
{statusConfig}
|
||||
{#if configAtual?.data?.testadoEm}
|
||||
- Última conexão testada em {new Date(
|
||||
configAtual.data.testadoEm,
|
||||
).toLocaleString("pt-BR")}
|
||||
- Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -306,7 +281,7 @@
|
||||
<div class="card-body">
|
||||
<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 -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label" for="smtp-servidor">
|
||||
@@ -320,9 +295,7 @@
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Ex: smtp.gmail.com, smtp.office365.com</span
|
||||
>
|
||||
<span class="label-text-alt">Ex: smtp.gmail.com, smtp.office365.com</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -339,8 +312,7 @@
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -412,7 +384,7 @@
|
||||
|
||||
<!-- Opções de Segurança -->
|
||||
<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="form-control">
|
||||
@@ -441,7 +413,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="card-actions justify-end mt-6 gap-3">
|
||||
<div class="card-actions mt-6 justify-end gap-3">
|
||||
<button
|
||||
class="btn btn-outline btn-info"
|
||||
onclick={testarConexao}
|
||||
@@ -499,12 +471,12 @@
|
||||
{/if}
|
||||
|
||||
<!-- 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">
|
||||
<h2 class="card-title mb-4">Exemplos de Configuração</h2>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<table class="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provedor</th>
|
||||
@@ -550,7 +522,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -561,13 +533,11 @@
|
||||
</svg>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Dica de Segurança:</strong> Para Gmail e outros provedores, você
|
||||
pode precisar gerar uma "senha de app" específica em vez de usar sua senha
|
||||
principal.
|
||||
<strong>Dica de Segurança:</strong> Para Gmail e outros provedores, você pode precisar gerar
|
||||
uma "senha de app" específica em vez de usar sua senha principal.
|
||||
</p>
|
||||
<p class="text-sm mt-1">
|
||||
Gmail: Conta Google → Segurança → Verificação em duas etapas → Senhas de
|
||||
app
|
||||
<p class="mt-1 text-sm">
|
||||
Gmail: Conta Google → Segurança → Verificação em duas etapas → Senhas de app
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user