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,367 +1,590 @@
<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> = {};
for (const f of funcionarios) counts[f.simboloId] = (counts[f.simboloId] ?? 0) + 1;
rows = simbolos.map((s: any) => ({
_id: String(s._id),
nome: s.nome as string,
valor: Number(s.valor || 0),
count: counts[String(s._id)] ?? 0,
}));
} catch (e) {
notice = { kind: "error", text: "Falha ao carregar dados de relatórios." };
} finally {
isLoading = false;
}
});
// Dimensões responsivas const counts: Record<string, number> = {};
$effect(() => { for (const f of funcionarios) {
const updateSize = () => { const sId = String(f.simboloId);
const container = document.querySelector('.chart-container'); counts[sId] = (counts[sId] ?? 0) + 1;
if (container) { }
containerWidth = Math.min(container.clientWidth - 32, 1200);
}
};
updateSize(); rows = simbolos.map((s) => ({
window.addEventListener('resize', updateSize); _id: String(s._id),
nome: s.nome,
valor: Number(s.valor || 0),
count: counts[String(s._id)] ?? 0
}));
} catch (e) {
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;
}
});
return () => window.removeEventListener('resize', updateSize); // Dimensões responsivas
}); $effect(() => {
const updateSize = () => {
const container = document.querySelector('.chart-container');
if (container) {
containerWidth = Math.min(container.clientWidth - 32, 1200);
}
};
const chartHeight = 350; updateSize();
const padding = { top: 20, right: 20, bottom: 80, left: 70 }; window.addEventListener('resize', updateSize);
let chartWidth = $derived(containerWidth); return () => window.removeEventListener('resize', updateSize);
});
function getMax<T>(arr: Array<T>, sel: (t: T) => number): number { const chartHeight = 350;
let m = 0; const padding = { top: 20, right: 20, bottom: 80, left: 70 };
for (const a of arr) m = Math.max(m, sel(a));
return m;
}
function scaleY(v: number, max: number): number { let chartWidth = $derived(containerWidth);
if (max <= 0) return 0;
const innerH = chartHeight - padding.top - padding.bottom;
return (v / max) * innerH;
}
function getX(i: number, n: number): number { function getMax<T>(arr: Array<T>, sel: (t: T) => number): number {
const innerW = chartWidth - padding.left - padding.right; let m = 0;
return padding.left + (innerW / (n - 1)) * i; for (const a of arr) m = Math.max(m, sel(a));
} return m;
}
function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string { function scaleY(v: number, max: number): number {
if (data.length === 0) return ""; if (max <= 0) return 0;
const n = data.length; const innerH = chartHeight - padding.top - padding.bottom;
let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`; return (v / max) * innerH;
}
for (let i = 0; i < n; i++) { function getX(i: number, n: number): number {
const x = getX(i, n); const innerW = chartWidth - padding.left - padding.right;
const y = chartHeight - padding.bottom - scaleY(getValue(data[i]), max); return padding.left + (innerW / (n - 1)) * i;
path += ` L ${x} ${y}`; }
}
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`; function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string {
path += " Z"; if (data.length === 0) return '';
return path; const n = data.length;
} let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`;
for (let i = 0; i < n; i++) {
const x = getX(i, n);
const y = chartHeight - padding.bottom - scaleY(getValue(data[i]), max);
path += ` L ${x} ${y}`;
}
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`;
path += ' Z';
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>
</ul> <li>
</div> <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 --> <!-- 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"
</svg> class="text-primary h-8 w-8"
</div> fill="none"
<div> viewBox="0 0 24 24"
<h1 class="text-3xl font-bold text-base-content">Relatórios de Funcionários</h1> stroke="currentColor"
<p class="text-base-content/60 mt-1">Análise de distribuição de salários e funcionários por símbolo</p> >
</div> <path
</div> 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-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} {#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'}
</svg> class:alert-success={notice.kind === 'success'}
<span>{notice.text}</span> >
</div> <svg
{/if} 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} {#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
<div class="card-body p-6"> class="card bg-base-100 border-base-300 border shadow-lg transition-shadow hover:shadow-xl"
<div class="flex items-center gap-3 mb-6"> >
<div class="p-2.5 bg-primary/10 rounded-lg"> <div class="card-body p-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="mb-6 flex items-center gap-3">
<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="bg-primary/10 rounded-lg p-2.5">
</svg> <svg
</div> xmlns="http://www.w3.org/2000/svg"
<div class="flex-1"> class="text-primary h-6 w-6"
<h3 class="text-lg font-bold text-base-content">Distribuição de Salários por Símbolo</h3> fill="none"
<p class="text-sm text-base-content/60 mt-0.5">Valores dos símbolos cadastrados no sistema</p> viewBox="0 0 24 24"
</div> stroke="currentColor"
</div> >
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4"> <path
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: salário por símbolo"> stroke-linecap="round"
{#if rows.length === 0} stroke-linejoin="round"
<text x="16" y="32" class="opacity-60">Sem dados</text> stroke-width="2"
{:else} 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"
{@const max = getMax(rows, (r) => r.valor)} />
</svg>
</div>
<div class="flex-1">
<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="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 --> <!-- 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}
{/each} 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 --> <!-- 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
d={createAreaPath(rows, (r) => r.valor, max)} d={createAreaPath(rows, (r) => r.valor, max)}
fill="url(#gradient-salary)" fill="url(#gradient-salary)"
opacity="0.7" opacity="0.7"
/> />
<!-- Line --> <!-- Line -->
<polyline <polyline
points={rows.map((r, i) => { points={rows
const x = getX(i, rows.length); .map((r, i) => {
const y = chartHeight - padding.bottom - scaleY(r.valor, max); const x = getX(i, rows.length);
return `${x},${y}`; const y = chartHeight - padding.bottom - scaleY(r.valor, max);
}).join(' ')} return `${x},${y}`;
fill="none" })
stroke="rgb(59, 130, 246)" .join(' ')}
stroke-width="3" fill="none"
/> stroke="rgb(59, 130, 246)"
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}
{`R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`} cy={y}
</text> r="5"
{/each} 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 --> <!-- 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
<div class="flex items-center justify-center text-center"> x={x - 40}
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;"> y={chartHeight - padding.bottom + 15}
{r.nome} width="80"
</span> height="70"
</div> >
</foreignObject> <div class="flex items-center justify-center text-center">
{/each} <span
class="text-base-content/80 text-[11px] leading-tight font-medium"
style="word-wrap: break-word; hyphens: auto;"
>
{r.nome}
</span>
</div>
</foreignObject>
{/each}
<!-- Gradient definition --> <!-- Gradient definition -->
<defs> <defs>
<linearGradient id="gradient-salary" x1="0%" y1="0%" x2="0%" y2="100%"> <linearGradient id="gradient-salary" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.8" /> <stop offset="0%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.8" />
<stop offset="100%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.1" /> <stop offset="100%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.1" />
</linearGradient> </linearGradient>
</defs> </defs>
{/if} {/if}
</svg> </svg>
</div> </div>
</div> </div>
</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
<div class="card-body p-6"> class="card bg-base-100 border-base-300 border shadow-lg transition-shadow hover:shadow-xl"
<div class="flex items-center gap-3 mb-6"> >
<div class="p-2.5 bg-secondary/10 rounded-lg"> <div class="card-body p-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="mb-6 flex items-center gap-3">
<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="bg-secondary/10 rounded-lg p-2.5">
</svg> <svg
</div> xmlns="http://www.w3.org/2000/svg"
<div class="flex-1"> class="text-secondary h-6 w-6"
<h3 class="text-lg font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3> fill="none"
<p class="text-sm text-base-content/60 mt-0.5">Quantidade de funcionários alocados em cada símbolo</p> viewBox="0 0 24 24"
</div> stroke="currentColor"
</div> >
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4"> <path
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: quantidade por símbolo"> stroke-linecap="round"
{#if rows.length === 0} stroke-linejoin="round"
<text x="16" y="32" class="opacity-60">Sem dados</text> stroke-width="2"
{:else} 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"
{@const maxC = getMax(rows, (r) => r.count)} />
</svg>
</div>
<div class="flex-1">
<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="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 --> <!-- 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}
{/each} 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 --> <!-- 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
d={createAreaPath(rows, (r) => r.count, Math.max(1, maxC))} d={createAreaPath(rows, (r) => r.count, Math.max(1, maxC))}
fill="url(#gradient-count)" fill="url(#gradient-count)"
opacity="0.7" opacity="0.7"
/> />
<!-- Line --> <!-- Line -->
<polyline <polyline
points={rows.map((r, i) => { points={rows
const x = getX(i, rows.length); .map((r, i) => {
const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC)); const x = getX(i, rows.length);
return `${x},${y}`; const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC));
}).join(' ')} return `${x},${y}`;
fill="none" })
stroke="rgb(236, 72, 153)" .join(' ')}
stroke-width="3" fill="none"
/> stroke="rgb(236, 72, 153)"
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}
{r.count} cy={y}
</text> r="5"
{/each} 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 --> <!-- 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
<div class="flex items-center justify-center text-center"> x={x - 40}
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;"> y={chartHeight - padding.bottom + 15}
{r.nome} width="80"
</span> height="70"
</div> >
</foreignObject> <div class="flex items-center justify-center text-center">
{/each} <span
class="text-base-content/80 text-[11px] leading-tight font-medium"
style="word-wrap: break-word; hyphens: auto;"
>
{r.nome}
</span>
</div>
</foreignObject>
{/each}
<!-- Gradient definition --> <!-- Gradient definition -->
<defs> <defs>
<linearGradient id="gradient-count" x1="0%" y1="0%" x2="0%" y2="100%"> <linearGradient id="gradient-count" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.8" /> <stop offset="0%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.8" />
<stop offset="100%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.1" /> <stop offset="100%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.1" />
</linearGradient> </linearGradient>
</defs> </defs>
{/if} {/if}
</svg> </svg>
</div> </div>
</div> </div>
</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"
</svg> class="text-accent h-6 w-6"
</div> fill="none"
<div class="flex-1"> viewBox="0 0 24 24"
<h3 class="text-lg font-bold text-base-content">Tabela Resumo - Símbolos e Funcionários</h3> stroke="currentColor"
<p class="text-sm text-base-content/60 mt-0.5">Visão detalhada dos dados apresentados nos gráficos</p> >
</div> <path
</div> 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-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"> <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>
<th class="bg-base-200 text-right">Valor (R$)</th> <th class="bg-base-200 text-right">Valor (R$)</th>
<th class="bg-base-200 text-right">Funcionários</th> <th class="bg-base-200 text-right">Funcionários</th>
<th class="bg-base-200 text-right">Total (R$)</th> <th class="bg-base-200 text-right">Total (R$)</th>
</tr> </tr>
</thead> </thead>
<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"
</tr> >Nenhum dado disponível</td
{:else} >
{#each rows as row} </tr>
<tr class="hover"> {:else}
<td class="font-semibold">{row.nome}</td> {#each rows as row (row._id)}
<td class="text-right font-mono"> <tr class="hover">
{row.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <td class="font-semibold">{row.nome}</td>
</td> <td class="text-right font-mono">
<td class="text-right"> {row.valor.toLocaleString('pt-BR', {
<span class="badge badge-primary badge-outline">{row.count}</span> minimumFractionDigits: 2,
</td> maximumFractionDigits: 2
<td class="text-right font-mono font-semibold text-primary"> })}
{(row.valor * row.count).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} </td>
</td> <td class="text-right">
</tr> <span class="badge badge-primary badge-outline">{row.count}</span>
{/each} </td>
<!-- Total Geral --> <td class="text-primary text-right font-mono font-semibold">
<tr class="font-bold bg-base-200 border-t-2 border-base-300"> {(row.valor * row.count).toLocaleString('pt-BR', {
<td>TOTAL GERAL</td> minimumFractionDigits: 2,
<td class="text-right font-mono"> maximumFractionDigits: 2
{rows.reduce((sum, r) => sum + r.valor, 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} })}
</td> </td>
<td class="text-right"> </tr>
<span class="badge badge-primary">{rows.reduce((sum, r) => sum + r.count, 0)}</span> {/each}
</td> <!-- Total Geral -->
<td class="text-right font-mono text-primary text-lg"> <tr class="bg-base-200 border-base-300 border-t-2 font-bold">
{rows.reduce((sum, r) => sum + (r.valor * r.count), 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <td>TOTAL GERAL</td>
</td> <td class="text-right font-mono">
</tr> {rows
{/if} .reduce((sum, r) => sum + r.valor, 0)
</tbody> .toLocaleString('pt-BR', {
</table> minimumFractionDigits: 2,
</div> maximumFractionDigits: 2
</div> })}
</div> </td>
</div> <td class="text-right">
{/if} <span class="badge badge-primary"
>{rows.reduce((sum, r) => sum + r.count, 0)}</span
>
</td>
<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}
</tbody>
</table>
</div>
</div>
</div>
</div>
{/if}
</div> </div>