Merge branch 'master' into feat-controle-ponto
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
|
||||
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
|
||||
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
|
||||
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||
|
||||
import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from 'lucide-svelte';
|
||||
import { authClient } from '$lib/auth';
|
||||
import { resolve } from '$app/paths';
|
||||
@@ -33,8 +33,8 @@
|
||||
return currentUser.data.avatar;
|
||||
}
|
||||
|
||||
// Fallback: gerar avatar baseado no nome
|
||||
return getAvatarUrl(currentUser.data.nome);
|
||||
// Fallback: retornar null para usar o ícone User do Lucide
|
||||
return null;
|
||||
});
|
||||
|
||||
// Função para gerar classes do menu ativo
|
||||
@@ -328,8 +328,9 @@
|
||||
>Contato</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href={resolve('/abrir-chamado')} class="link link-hover hover:text-primary transition-colors"
|
||||
>Suporte</a
|
||||
<a
|
||||
href={resolve('/abrir-chamado')}
|
||||
class="link link-hover hover:text-primary transition-colors">Suporte</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
||||
@@ -365,7 +366,7 @@
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
{#each setores as s}
|
||||
{#each setores as s (s.link)}
|
||||
{@const isActive = currentPath.startsWith(s.link)}
|
||||
<li class="rounded-xl">
|
||||
<a
|
||||
@@ -400,7 +401,7 @@
|
||||
<!-- Botão de fechar moderno -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-4 right-4 z-10 hover:bg-error/20 hover:text-error transition-all duration-200"
|
||||
class="btn btn-sm btn-circle btn-ghost hover:bg-error/20 hover:text-error absolute top-4 right-4 z-10 transition-all duration-200"
|
||||
onclick={closeLoginModal}
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
@@ -408,28 +409,20 @@
|
||||
</button>
|
||||
|
||||
<!-- Decoração de fundo -->
|
||||
<div
|
||||
class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-primary/10 blur-3xl"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -bottom-20 -left-20 h-40 w-40 rounded-full bg-primary/5 blur-3xl"
|
||||
></div>
|
||||
<div class="bg-primary/10 absolute -top-20 -right-20 h-40 w-40 rounded-full blur-3xl"></div>
|
||||
<div class="bg-primary/5 absolute -bottom-20 -left-20 h-40 w-40 rounded-full blur-3xl"></div>
|
||||
|
||||
<div class="relative z-10 p-8">
|
||||
<!-- Header com logo e título -->
|
||||
<div class="mb-8 text-center">
|
||||
<div class="avatar mb-5 mx-auto">
|
||||
<div class="avatar mx-auto mb-5">
|
||||
<div
|
||||
class="group relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 ring-primary/20 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
class="group ring-primary/20 relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
class="from-primary/10 absolute inset-0 bg-gradient-to-br to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
></div>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo SGSE"
|
||||
class="relative z-10 h-full w-full object-contain"
|
||||
/>
|
||||
<img src={logo} alt="Logo SGSE" class="relative z-10 h-full w-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-primary mb-2 text-4xl font-bold tracking-tight">Login</h3>
|
||||
@@ -441,7 +434,7 @@
|
||||
<!-- Mensagem de erro -->
|
||||
{#if erroLogin}
|
||||
<div
|
||||
class="alert alert-error mb-6 border-error/30 bg-error/10 shadow-lg backdrop-blur-sm"
|
||||
class="alert alert-error border-error/30 bg-error/10 mb-6 shadow-lg backdrop-blur-sm"
|
||||
>
|
||||
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2.5} />
|
||||
<span class="font-medium">{erroLogin}</span>
|
||||
@@ -453,16 +446,14 @@
|
||||
<!-- Campo Matrícula/E-mail -->
|
||||
<div class="form-control">
|
||||
<label class="label pb-2" for="login-matricula">
|
||||
<span class="text-primary label-text text-sm font-semibold"
|
||||
>Matrícula ou E-mail</span
|
||||
>
|
||||
<span class="text-primary label-text text-sm font-semibold">Matrícula ou E-mail</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="login-matricula"
|
||||
type="text"
|
||||
placeholder="Digite sua matrícula ou e-mail"
|
||||
class="input input-bordered input-primary w-full border-2 transition-all duration-200 focus:border-primary focus:shadow-lg focus:shadow-primary/20 disabled:opacity-50"
|
||||
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
|
||||
bind:value={matricula}
|
||||
required
|
||||
disabled={carregandoLogin}
|
||||
@@ -481,7 +472,7 @@
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
class="input input-bordered input-primary w-full border-2 transition-all duration-200 focus:border-primary focus:shadow-lg focus:shadow-primary/20 disabled:opacity-50"
|
||||
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
|
||||
bind:value={senha}
|
||||
required
|
||||
disabled={carregandoLogin}
|
||||
@@ -494,7 +485,7 @@
|
||||
<div class="form-control pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg group relative w-full overflow-hidden border-0 bg-gradient-to-r from-primary via-primary to-primary/90 shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
|
||||
class="btn btn-primary btn-lg group from-primary via-primary to-primary/90 relative w-full overflow-hidden border-0 bg-gradient-to-r shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
|
||||
disabled={carregandoLogin}
|
||||
>
|
||||
<!-- Efeito de brilho animado -->
|
||||
@@ -506,14 +497,17 @@
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span class="font-semibold">Entrando...</span>
|
||||
{:else}
|
||||
<LogIn class="h-5 w-5 transition-transform duration-300 group-hover:scale-110" strokeWidth={2.5} />
|
||||
<LogIn
|
||||
class="h-5 w-5 transition-transform duration-300 group-hover:scale-110"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span class="font-semibold">Entrar</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Links auxiliares -->
|
||||
<div class="pt-4 space-y-3 text-center">
|
||||
<div class="space-y-3 pt-4 text-center">
|
||||
<a
|
||||
href={resolve('/abrir-chamado')}
|
||||
class="link link-primary block text-sm font-medium transition-all duration-200 hover:scale-105"
|
||||
@@ -548,7 +542,7 @@
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2 z-10 hover:bg-base-300"
|
||||
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 absolute top-2 right-2 z-10"
|
||||
onclick={closeAboutModal}
|
||||
>
|
||||
✕
|
||||
@@ -558,7 +552,7 @@
|
||||
<!-- Logo e Título -->
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="avatar">
|
||||
<div class="w-20 rounded-xl bg-white p-3 shadow-lg ring-2 ring-primary/20">
|
||||
<div class="ring-primary/20 w-20 rounded-xl bg-white p-3 shadow-lg ring-2">
|
||||
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -574,10 +568,12 @@
|
||||
<div class="divider my-1"></div>
|
||||
|
||||
<!-- Informações de Versão -->
|
||||
<div class="bg-gradient-to-br from-primary/10 to-primary/5 space-y-2 rounded-xl border border-primary/10 p-4 shadow-sm">
|
||||
<div
|
||||
class="from-primary/10 to-primary/5 border-primary/10 space-y-2 rounded-xl border bg-gradient-to-br p-4 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Tag class="text-primary h-4 w-4" strokeWidth={2} />
|
||||
<p class="text-base-content/60 text-xs font-medium uppercase tracking-wide">Versão</p>
|
||||
<p class="text-base-content/60 text-xs font-medium tracking-wide uppercase">Versão</p>
|
||||
</div>
|
||||
<p class="text-primary text-2xl font-bold tracking-tight">1.0 11_2025</p>
|
||||
<div class="badge badge-warning badge-sm gap-1.5 px-3 py-1.5 text-xs">
|
||||
@@ -588,7 +584,9 @@
|
||||
|
||||
<!-- Desenvolvido por -->
|
||||
<div class="space-y-1.5">
|
||||
<p class="text-base-content/50 text-xs font-medium uppercase tracking-wide">Desenvolvido por</p>
|
||||
<p class="text-base-content/50 text-xs font-medium tracking-wide uppercase">
|
||||
Desenvolvido por
|
||||
</p>
|
||||
<p class="text-primary text-sm font-semibold">Secretaria de Esportes de Pernambuco</p>
|
||||
</div>
|
||||
|
||||
@@ -597,12 +595,16 @@
|
||||
|
||||
<!-- Informações Adicionais -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-base-200/60 rounded-lg border border-base-300/50 p-3 shadow-sm transition-all hover:shadow-md">
|
||||
<p class="text-primary mb-1 text-xs font-semibold uppercase tracking-wide">Governo</p>
|
||||
<div
|
||||
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Governo</p>
|
||||
<p class="text-base-content/60 text-xs font-medium">Estado de Pernambuco</p>
|
||||
</div>
|
||||
<div class="bg-base-200/60 rounded-lg border border-base-300/50 p-3 shadow-sm transition-all hover:shadow-md">
|
||||
<p class="text-primary mb-1 text-xs font-semibold uppercase tracking-wide">Ano</p>
|
||||
<div
|
||||
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Ano</p>
|
||||
<p class="text-base-content/60 text-xs font-medium">2025</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import multiMonthPlugin from "@fullcalendar/multimonth";
|
||||
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
|
||||
import { SvelteDate } from "svelte/reactivity";
|
||||
|
||||
interface Props {
|
||||
dataInicio?: string;
|
||||
@@ -34,18 +35,6 @@
|
||||
let calendarEl: HTMLDivElement;
|
||||
let calendar: Calendar | null = null;
|
||||
let selecionando = $state(false); // Flag para evitar atualizações durante seleção
|
||||
let eventos: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
start: string;
|
||||
end: string;
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
textColor: string;
|
||||
extendedProps: {
|
||||
status: string;
|
||||
};
|
||||
}> = $state([]);
|
||||
|
||||
// Cores por status
|
||||
const coresStatus: Record<
|
||||
@@ -58,7 +47,7 @@
|
||||
};
|
||||
|
||||
// Converter ausências existentes em eventos
|
||||
function atualizarEventos() {
|
||||
let eventos = $derived.by(() => {
|
||||
const novosEventos: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -103,8 +92,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
eventos = novosEventos;
|
||||
}
|
||||
return novosEventos;
|
||||
});
|
||||
|
||||
function getStatusTexto(status: string): string {
|
||||
const textos: Record<string, string> = {
|
||||
@@ -117,15 +106,15 @@
|
||||
|
||||
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
|
||||
function calcularDataFim(dataFim: string): string {
|
||||
const data = new Date(dataFim);
|
||||
const data = new SvelteDate(dataFim);
|
||||
data.setDate(data.getDate() + 1);
|
||||
return data.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
// Helper: Calcular dias entre datas (inclusivo)
|
||||
function calcularDias(inicio: string, fim: string): number {
|
||||
const dInicio = new Date(inicio);
|
||||
const dFim = new Date(fim);
|
||||
const dInicio = new SvelteDate(inicio);
|
||||
const dFim = new SvelteDate(fim);
|
||||
const diffTime = Math.abs(dFim.getTime() - dInicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
return diffDays;
|
||||
@@ -133,20 +122,23 @@
|
||||
|
||||
// Helper: Verificar se há sobreposição de datas
|
||||
function verificarSobreposicao(
|
||||
inicio1: Date,
|
||||
fim1: Date,
|
||||
inicio1: SvelteDate,
|
||||
fim1: SvelteDate,
|
||||
inicio2: string,
|
||||
fim2: string,
|
||||
): boolean {
|
||||
const d2Inicio = new Date(inicio2);
|
||||
const d2Fim = new Date(fim2);
|
||||
const d2Inicio = new SvelteDate(inicio2);
|
||||
const d2Fim = new SvelteDate(fim2);
|
||||
|
||||
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1
|
||||
return inicio1 <= d2Fim && d2Inicio <= fim1;
|
||||
}
|
||||
|
||||
// Helper: Verificar se período selecionado sobrepõe com ausências existentes
|
||||
function verificarSobreposicaoComAusencias(inicio: Date, fim: Date): boolean {
|
||||
function verificarSobreposicaoComAusencias(
|
||||
inicio: SvelteDate,
|
||||
fim: SvelteDate,
|
||||
): boolean {
|
||||
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
|
||||
|
||||
// Verificar apenas ausências aprovadas ou aguardando aprovação
|
||||
@@ -159,12 +151,17 @@
|
||||
);
|
||||
}
|
||||
|
||||
interface FullCalendarDayCellInfo {
|
||||
el: HTMLElement;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
// Helper: Atualizar classe de seleção em uma célula
|
||||
function atualizarClasseSelecionado(info: any) {
|
||||
function atualizarClasseSelecionado(info: FullCalendarDayCellInfo) {
|
||||
if (dataInicio && dataFim && !readonly) {
|
||||
const cellDate = new Date(info.date);
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
const cellDate = new SvelteDate(info.date);
|
||||
const inicio = new SvelteDate(dataInicio);
|
||||
const fim = new SvelteDate(dataFim);
|
||||
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
@@ -181,13 +178,13 @@
|
||||
}
|
||||
|
||||
// Helper: Atualizar classe de bloqueio para dias com ausências existentes
|
||||
function atualizarClasseBloqueado(info: any) {
|
||||
function atualizarClasseBloqueado(info: FullCalendarDayCellInfo) {
|
||||
if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
|
||||
info.el.classList.remove("fc-day-blocked");
|
||||
return;
|
||||
}
|
||||
|
||||
const cellDate = new Date(info.date);
|
||||
const cellDate = new SvelteDate(info.date);
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
|
||||
@@ -196,8 +193,8 @@
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
)
|
||||
.some((ausencia) => {
|
||||
const inicio = new Date(ausencia.dataInicio);
|
||||
const fim = new Date(ausencia.dataFim);
|
||||
const inicio = new SvelteDate(ausencia.dataInicio);
|
||||
const fim = new SvelteDate(ausencia.dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
return cellDate >= inicio && cellDate <= fim;
|
||||
@@ -218,8 +215,8 @@
|
||||
const view = calendar.view;
|
||||
if (!view) return;
|
||||
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
const inicio = new SvelteDate(dataInicio);
|
||||
const fim = new SvelteDate(dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
|
||||
@@ -235,14 +232,14 @@
|
||||
if (ariaLabel) {
|
||||
// Formato: "dia mês ano" ou similar
|
||||
try {
|
||||
const cellDate = new Date(ariaLabel);
|
||||
const cellDate = new SvelteDate(ariaLabel);
|
||||
if (!isNaN(cellDate.getTime())) {
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
if (cellDate >= inicio && cellDate <= fim) {
|
||||
cell.classList.add("fc-day-selected");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignorar erros de parsing
|
||||
}
|
||||
}
|
||||
@@ -266,6 +263,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const calendarInstance = calendar;
|
||||
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
@@ -280,17 +278,17 @@
|
||||
cell.classList.remove("fc-day-blocked");
|
||||
|
||||
// Tentar obter a data de diferentes formas
|
||||
let cellDate: Date | null = null;
|
||||
let cellDate: SvelteDate | null = null;
|
||||
|
||||
// Método 1: aria-label
|
||||
const ariaLabel = cell.getAttribute("aria-label");
|
||||
if (ariaLabel) {
|
||||
try {
|
||||
const parsed = new Date(ariaLabel);
|
||||
const parsed = new SvelteDate(ariaLabel);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
cellDate = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignorar
|
||||
}
|
||||
}
|
||||
@@ -300,27 +298,27 @@
|
||||
const dataDate = cell.getAttribute("data-date");
|
||||
if (dataDate) {
|
||||
try {
|
||||
const parsed = new Date(dataDate);
|
||||
const parsed = new SvelteDate(dataDate);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
cellDate = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignorar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Método 3: Tentar obter do número do dia e contexto do calendário
|
||||
if (!cellDate && calendar.view) {
|
||||
if (!cellDate && calendarInstance.view) {
|
||||
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
|
||||
if (dayNumberEl) {
|
||||
const dayNumber = parseInt(dayNumberEl.textContent || "0");
|
||||
if (dayNumber > 0 && dayNumber <= 31) {
|
||||
// Usar a data da view atual e o número do dia
|
||||
const viewStart = new Date(calendar.view.activeStart);
|
||||
const viewStart = new SvelteDate(calendarInstance.view.activeStart);
|
||||
const cellIndex = Array.from(cells).indexOf(cell);
|
||||
if (cellIndex >= 0) {
|
||||
const possibleDate = new Date(viewStart);
|
||||
const possibleDate = new SvelteDate(viewStart);
|
||||
possibleDate.setDate(viewStart.getDate() + cellIndex);
|
||||
// Verificar se o número do dia corresponde
|
||||
if (possibleDate.getDate() === dayNumber) {
|
||||
@@ -335,11 +333,11 @@
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||
const inicio = new Date(ausencia.dataInicio);
|
||||
const fim = new Date(ausencia.dataFim);
|
||||
const inicio = new SvelteDate(ausencia.dataInicio);
|
||||
const fim = new SvelteDate(ausencia.dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
return cellDate >= inicio && cellDate <= fim;
|
||||
return cellDate! >= inicio && cellDate! <= fim;
|
||||
});
|
||||
|
||||
if (estaBloqueado) {
|
||||
@@ -354,9 +352,7 @@
|
||||
if (!calendar || selecionando) return; // Não atualizar durante seleção
|
||||
|
||||
// Garantir que temos as ausências antes de atualizar
|
||||
const ausencias = ausenciasExistentes;
|
||||
|
||||
atualizarEventos();
|
||||
void ausenciasExistentes;
|
||||
|
||||
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
|
||||
requestAnimationFrame(() => {
|
||||
@@ -398,8 +394,6 @@
|
||||
onMount(() => {
|
||||
if (!calendarEl) return;
|
||||
|
||||
atualizarEventos();
|
||||
|
||||
calendar = new Calendar(calendarEl, {
|
||||
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
|
||||
initialView:
|
||||
@@ -416,9 +410,9 @@
|
||||
selectMirror: true,
|
||||
unselectAuto: false,
|
||||
selectOverlap: false,
|
||||
selectConstraint: null, // Permite seleção entre meses diferentes
|
||||
selectConstraint: undefined, // Permite seleção entre meses diferentes
|
||||
validRange: {
|
||||
start: new Date().toISOString().split("T")[0], // Não permite selecionar datas passadas
|
||||
start: new SvelteDate().toISOString().split("T")[0], // Não permite selecionar datas passadas
|
||||
},
|
||||
events: eventos,
|
||||
|
||||
@@ -437,12 +431,12 @@
|
||||
|
||||
// Usar setTimeout para evitar conflito com atualizações de estado
|
||||
setTimeout(() => {
|
||||
const inicio = new Date(info.startStr);
|
||||
const fim = new Date(info.endStr);
|
||||
const inicio = new SvelteDate(info.startStr);
|
||||
const fim = new SvelteDate(info.endStr);
|
||||
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
|
||||
|
||||
// Validar que não é no passado
|
||||
const hoje = new Date();
|
||||
const hoje = new SvelteDate();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
if (inicio < hoje) {
|
||||
alert("A data de início não pode ser no passado");
|
||||
@@ -511,11 +505,11 @@
|
||||
|
||||
// Desabilitar datas passadas e períodos que sobrepõem com ausências existentes
|
||||
selectAllow: (selectInfo) => {
|
||||
const hoje = new Date();
|
||||
const hoje = new SvelteDate();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
// Bloquear datas passadas
|
||||
if (new Date(selectInfo.start) < hoje) {
|
||||
if (new SvelteDate(selectInfo.start) < hoje) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -525,8 +519,8 @@
|
||||
ausenciasExistentes &&
|
||||
ausenciasExistentes.length > 0
|
||||
) {
|
||||
const inicioSelecao = new Date(selectInfo.start);
|
||||
const fimSelecao = new Date(selectInfo.end);
|
||||
const inicioSelecao = new SvelteDate(selectInfo.start);
|
||||
const fimSelecao = new SvelteDate(selectInfo.end);
|
||||
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
|
||||
|
||||
inicioSelecao.setHours(0, 0, 0, 0);
|
||||
@@ -578,7 +572,7 @@
|
||||
ausenciasExistentes &&
|
||||
ausenciasExistentes.length > 0
|
||||
) {
|
||||
const cellDate = new Date(arg.date);
|
||||
const cellDate = new SvelteDate(arg.date);
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
@@ -587,8 +581,8 @@
|
||||
);
|
||||
|
||||
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||
const inicio = new Date(ausencia.dataInicio);
|
||||
const fim = new Date(ausencia.dataFim);
|
||||
const inicio = new SvelteDate(ausencia.dataInicio);
|
||||
const fim = new SvelteDate(ausencia.dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
return cellDate >= inicio && cellDate <= fim;
|
||||
@@ -646,9 +640,6 @@
|
||||
|
||||
<!-- Alerta sobre dias bloqueados -->
|
||||
{#if ausenciasExistentes && ausenciasExistentes.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
{@const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
)}
|
||||
<div class="alert alert-warning shadow-lg border-2 border-warning/50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -744,10 +735,10 @@
|
||||
<!-- Informação do período selecionado -->
|
||||
{#if dataInicio && dataFim && !readonly}
|
||||
<div
|
||||
class="mt-6 card bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 shadow-lg border-2 border-orange-500/30"
|
||||
class="mt-6 card shadow-lg border border-orange-400"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-orange-700 dark:text-orange-400">
|
||||
<h3 class="card-title">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import ErrorModal from '../ErrorModal.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
funcionarioId: Id<'funcionarios'>;
|
||||
@@ -67,7 +68,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const hoje = new Date();
|
||||
const hoje = new SvelteDate();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
const inicio = new Date(dataInicio);
|
||||
|
||||
@@ -266,7 +267,7 @@
|
||||
<div>
|
||||
<h4 class="font-bold">Período selecionado!</h4>
|
||||
<p>
|
||||
De {new Date(dataInicio).toLocaleDateString('pt-BR')} até{' '}
|
||||
De {new Date(dataInicio).toLocaleDateString('pt-BR')} até
|
||||
{new Date(dataFim).toLocaleDateString('pt-BR')} ({totalDias} dias)
|
||||
</p>
|
||||
</div>
|
||||
@@ -286,7 +287,7 @@
|
||||
<!-- Resumo do período -->
|
||||
{#if dataInicio && dataFim}
|
||||
<div
|
||||
class="card border-2 border-orange-500/30 bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950"
|
||||
class="card border-2 border-base-content/20"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-orange-700 dark:text-orange-400">
|
||||
@@ -345,7 +346,7 @@
|
||||
bind:value={motivo}
|
||||
maxlength={500}
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<label class="label" for="motivo">
|
||||
<span class="label-text-alt text-base-content/70">
|
||||
Mínimo 10 caracteres. Seja claro e objetivo.
|
||||
</span>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import ChatList from './ChatList.svelte';
|
||||
import ChatWindow from './ChatWindow.svelte';
|
||||
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
|
||||
@@ -38,11 +38,8 @@
|
||||
return usuario.fotoPerfilUrl;
|
||||
}
|
||||
|
||||
if (usuario.avatar) {
|
||||
return getAvatarUrl(usuario.avatar);
|
||||
}
|
||||
// Fallback: gerar avatar baseado no nome
|
||||
return getAvatarUrl(usuario.nome);
|
||||
// Fallback: retornar null para usar o ícone User do Lucide
|
||||
return null;
|
||||
});
|
||||
|
||||
// Posição do widget (arrastável)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import UserAvatar from './UserAvatar.svelte';
|
||||
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
|
||||
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
|
||||
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||
|
||||
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -59,10 +59,7 @@
|
||||
const c = conversa();
|
||||
if (!c) return '💬';
|
||||
if (c.tipo === 'grupo') {
|
||||
return c.avatar || '👥';
|
||||
}
|
||||
if (c.outroUsuario?.avatar) {
|
||||
return c.outroUsuario.avatar;
|
||||
return '👥';
|
||||
}
|
||||
return '👤';
|
||||
}
|
||||
@@ -138,7 +135,6 @@
|
||||
<div class="relative shrink-0">
|
||||
{#if conversa() && conversa()?.tipo === 'individual' && conversa()?.outroUsuario}
|
||||
<UserAvatar
|
||||
avatar={conversa()?.outroUsuario?.avatar}
|
||||
fotoPerfilUrl={conversa()?.outroUsuario?.fotoPerfilUrl}
|
||||
nome={conversa()?.outroUsuario?.nome || 'Usuário'}
|
||||
size="md"
|
||||
@@ -195,18 +191,12 @@
|
||||
alt={participante.nome}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else if participante.avatar}
|
||||
<img
|
||||
src={getAvatarUrl(participante.avatar)}
|
||||
alt={participante.nome}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={getAvatarUrl(participante.nome)}
|
||||
alt={participante.nome}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
<div
|
||||
class="bg-base-200 flex h-full w-full items-center justify-center text-xs font-semibold"
|
||||
>
|
||||
{participante.nome.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { getAvatarUrl as generateAvatarUrl } from "$lib/utils/avatarGenerator";
|
||||
|
||||
interface Props {
|
||||
avatar?: string;
|
||||
fotoPerfilUrl?: string | null;
|
||||
nome: string;
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
}
|
||||
import { User } from 'lucide-svelte';
|
||||
|
||||
let { avatar, fotoPerfilUrl, nome, size = "md" }: Props = $props();
|
||||
interface Props {
|
||||
fotoPerfilUrl?: string | null;
|
||||
nome: string;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xs: "w-8 h-8",
|
||||
sm: "w-10 h-10",
|
||||
md: "w-12 h-12",
|
||||
lg: "w-16 h-16",
|
||||
};
|
||||
let { fotoPerfilUrl, nome, size = 'md' }: Props = $props();
|
||||
|
||||
function getAvatarUrl(avatarId: string): string {
|
||||
// Usar gerador local ao invés da API externa
|
||||
return generateAvatarUrl(avatarId);
|
||||
}
|
||||
const sizeClasses = {
|
||||
xs: 'w-8 h-8',
|
||||
sm: 'w-10 h-10',
|
||||
md: 'w-12 h-12',
|
||||
lg: 'w-16 h-16',
|
||||
xl: 'w-32 h-32'
|
||||
};
|
||||
|
||||
const avatarUrlToShow = $derived(() => {
|
||||
if (fotoPerfilUrl) return fotoPerfilUrl;
|
||||
if (avatar) return getAvatarUrl(avatar);
|
||||
return getAvatarUrl(nome); // Fallback usando o nome
|
||||
});
|
||||
const iconSizes = {
|
||||
xs: 16,
|
||||
sm: 20,
|
||||
md: 24,
|
||||
lg: 32,
|
||||
xl: 64
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="avatar">
|
||||
<div class={`${sizeClasses[size]} rounded-full bg-base-200 overflow-hidden`}>
|
||||
<img
|
||||
src={avatarUrlToShow()}
|
||||
alt={`Avatar de ${nome}`}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="avatar placeholder">
|
||||
<div
|
||||
class={`${sizeClasses[size]} bg-base-200 text-base-content/50 flex items-center justify-center overflow-hidden rounded-full`}
|
||||
>
|
||||
{#if fotoPerfilUrl}
|
||||
<img
|
||||
src={fotoPerfilUrl}
|
||||
alt={`Foto de perfil de ${nome}`}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<User size={iconSizes[size]} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Mapa de seeds para os 32 avatares
|
||||
const avatarSeeds: Record<string, string> = {
|
||||
// Masculinos (16)
|
||||
"avatar-m-1": "John",
|
||||
"avatar-m-2": "Peter",
|
||||
"avatar-m-3": "Michael",
|
||||
"avatar-m-4": "David",
|
||||
"avatar-m-5": "James",
|
||||
"avatar-m-6": "Robert",
|
||||
"avatar-m-7": "William",
|
||||
"avatar-m-8": "Joseph",
|
||||
"avatar-m-9": "Thomas",
|
||||
"avatar-m-10": "Charles",
|
||||
"avatar-m-11": "Daniel",
|
||||
"avatar-m-12": "Matthew",
|
||||
"avatar-m-13": "Anthony",
|
||||
"avatar-m-14": "Mark",
|
||||
"avatar-m-15": "Donald",
|
||||
"avatar-m-16": "Steven",
|
||||
// Femininos (16)
|
||||
"avatar-f-1": "Maria",
|
||||
"avatar-f-2": "Ana",
|
||||
"avatar-f-3": "Patricia",
|
||||
"avatar-f-4": "Jennifer",
|
||||
"avatar-f-5": "Linda",
|
||||
"avatar-f-6": "Barbara",
|
||||
"avatar-f-7": "Elizabeth",
|
||||
"avatar-f-8": "Jessica",
|
||||
"avatar-f-9": "Sarah",
|
||||
"avatar-f-10": "Karen",
|
||||
"avatar-f-11": "Nancy",
|
||||
"avatar-f-12": "Betty",
|
||||
"avatar-f-13": "Helen",
|
||||
"avatar-f-14": "Sandra",
|
||||
"avatar-f-15": "Ashley",
|
||||
"avatar-f-16": "Kimberly",
|
||||
};
|
||||
|
||||
/**
|
||||
* Gera URL do avatar usando API DiceBear com parâmetros simples
|
||||
*/
|
||||
export function getAvatarUrl(avatarId: string): string {
|
||||
const seed = avatarSeeds[avatarId] || avatarId || "default";
|
||||
|
||||
// Usar avataarstyle do DiceBear com parâmetros mínimos
|
||||
// API v7 suporta apenas parâmetros específicos
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todos os IDs de avatares disponíveis
|
||||
*/
|
||||
export function getAllAvatarIds(): string[] {
|
||||
return Object.keys(avatarSeeds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um avatarId é válido
|
||||
*/
|
||||
export function isValidAvatarId(avatarId: string): boolean {
|
||||
return avatarId in avatarSeeds;
|
||||
}
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
// Galeria de avatares inspirados em artistas do cinema
|
||||
// Usando DiceBear API com estilos variados para aparência cinematográfica
|
||||
|
||||
export interface Avatar {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
seed: string;
|
||||
style: string;
|
||||
}
|
||||
|
||||
// Avatares inspirados em artistas do cinema (30 avatares estilizados)
|
||||
const cinemaArtistsAvatars = [
|
||||
// 15 Masculinos - Inspirados em grandes atores
|
||||
{
|
||||
id: 'avatar-male-1',
|
||||
name: 'Leonardo DiCaprio',
|
||||
seed: 'Leonardo',
|
||||
style: 'adventurer',
|
||||
bgColor: 'C5CAE9',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-2',
|
||||
name: 'Brad Pitt',
|
||||
seed: 'Bradley',
|
||||
style: 'adventurer',
|
||||
bgColor: 'B2DFDB',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-3',
|
||||
name: 'Tom Hanks',
|
||||
seed: 'Thomas',
|
||||
style: 'adventurer-neutral',
|
||||
bgColor: 'DCEDC8',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-4',
|
||||
name: 'Morgan Freeman',
|
||||
seed: 'Morgan',
|
||||
style: 'adventurer',
|
||||
bgColor: 'F0F4C3',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-5',
|
||||
name: 'Robert De Niro',
|
||||
seed: 'Robert',
|
||||
style: 'adventurer-neutral',
|
||||
bgColor: 'E0E0E0',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-6',
|
||||
name: 'Al Pacino',
|
||||
seed: 'Alfredo',
|
||||
style: 'adventurer',
|
||||
bgColor: 'FFCCBC',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-7',
|
||||
name: 'Johnny Depp',
|
||||
seed: 'John',
|
||||
style: 'adventurer',
|
||||
bgColor: 'D1C4E9',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-8',
|
||||
name: 'Denzel Washington',
|
||||
seed: 'Denzel',
|
||||
style: 'adventurer-neutral',
|
||||
bgColor: 'B3E5FC',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-9',
|
||||
name: 'Will Smith',
|
||||
seed: 'Willard',
|
||||
style: 'adventurer',
|
||||
bgColor: 'FFF9C4',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-10',
|
||||
name: 'Tom Cruise',
|
||||
seed: 'TomC',
|
||||
style: 'adventurer-neutral',
|
||||
bgColor: 'CFD8DC',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-11',
|
||||
name: 'Samuel L Jackson',
|
||||
seed: 'Samuel',
|
||||
style: 'adventurer',
|
||||
bgColor: 'F8BBD0',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-12',
|
||||
name: 'Harrison Ford',
|
||||
seed: 'Harrison',
|
||||
style: 'adventurer-neutral',
|
||||
bgColor: 'C8E6C9',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-13',
|
||||
name: 'Keanu Reeves',
|
||||
seed: 'Keanu',
|
||||
style: 'adventurer',
|
||||
bgColor: 'BBDEFB',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-14',
|
||||
name: 'Matt Damon',
|
||||
seed: 'Matthew',
|
||||
style: 'adventurer-neutral',
|
||||
bgColor: 'FFE0B2',
|
||||
},
|
||||
{
|
||||
id: 'avatar-male-15',
|
||||
name: 'Christian Bale',
|
||||
seed: 'Christian',
|
||||
style: 'adventurer',
|
||||
bgColor: 'E1BEE7',
|
||||
},
|
||||
// 15 Femininos - Inspiradas em grandes atrizes
|
||||
{
|
||||
id: 'avatar-female-1',
|
||||
name: 'Meryl Streep',
|
||||
seed: 'Meryl',
|
||||
style: 'lorelei',
|
||||
bgColor: 'F8BBD0',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-2',
|
||||
name: 'Scarlett Johansson',
|
||||
seed: 'Scarlett',
|
||||
style: 'lorelei',
|
||||
bgColor: 'FFCCBC',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-3',
|
||||
name: 'Jennifer Lawrence',
|
||||
seed: 'Jennifer',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'E1BEE7',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-4',
|
||||
name: 'Angelina Jolie',
|
||||
seed: 'Angelina',
|
||||
style: 'lorelei',
|
||||
bgColor: 'C5CAE9',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-5',
|
||||
name: 'Cate Blanchett',
|
||||
seed: 'Catherine',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'B2DFDB',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-6',
|
||||
name: 'Nicole Kidman',
|
||||
seed: 'Nicole',
|
||||
style: 'lorelei',
|
||||
bgColor: 'DCEDC8',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-7',
|
||||
name: 'Julia Roberts',
|
||||
seed: 'Julia',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'FFF9C4',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-8',
|
||||
name: 'Emma Stone',
|
||||
seed: 'Emma',
|
||||
style: 'lorelei',
|
||||
bgColor: 'CFD8DC',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-9',
|
||||
name: 'Natalie Portman',
|
||||
seed: 'Natalie',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'F0F4C3',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-10',
|
||||
name: 'Charlize Theron',
|
||||
seed: 'Charlize',
|
||||
style: 'lorelei',
|
||||
bgColor: 'E0E0E0',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-11',
|
||||
name: 'Kate Winslet',
|
||||
seed: 'Kate',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'D1C4E9',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-12',
|
||||
name: 'Sandra Bullock',
|
||||
seed: 'Sandra',
|
||||
style: 'lorelei',
|
||||
bgColor: 'B3E5FC',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-13',
|
||||
name: 'Halle Berry',
|
||||
seed: 'Halle',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'C8E6C9',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-14',
|
||||
name: 'Anne Hathaway',
|
||||
seed: 'Anne',
|
||||
style: 'lorelei',
|
||||
bgColor: 'BBDEFB',
|
||||
},
|
||||
{
|
||||
id: 'avatar-female-15',
|
||||
name: 'Amy Adams',
|
||||
seed: 'Amy',
|
||||
style: 'lorelei-neutral',
|
||||
bgColor: 'FFE0B2',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Gera uma galeria de avatares inspirados em artistas do cinema
|
||||
* Usa DiceBear API com estilos cinematográficos
|
||||
* @param count Número de avatares a gerar (padrão: 30)
|
||||
* @returns Array de objetos com id, name, url, seed e style
|
||||
*/
|
||||
export function generateAvatarGallery(count: number = 30): Avatar[] {
|
||||
const avatars: Avatar[] = [];
|
||||
|
||||
for (let i = 0; i < Math.min(count, cinemaArtistsAvatars.length); i++) {
|
||||
const avatar = cinemaArtistsAvatars[i];
|
||||
|
||||
// URL do DiceBear com estilo cinematográfico
|
||||
const url = `https://api.dicebear.com/7.x/${avatar.style}/svg?seed=${encodeURIComponent(avatar.seed)}&backgroundColor=${avatar.bgColor}&radius=50&size=200`;
|
||||
|
||||
avatars.push({
|
||||
id: avatar.id,
|
||||
name: avatar.name,
|
||||
url,
|
||||
seed: avatar.seed,
|
||||
style: avatar.style,
|
||||
});
|
||||
}
|
||||
|
||||
return avatars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter URL do avatar por ID
|
||||
* @param avatarId ID do avatar (ex: "avatar-male-1")
|
||||
* @returns URL do avatar ou string vazia se não encontrado
|
||||
*/
|
||||
export function getAvatarUrl(avatarId: string): string {
|
||||
const gallery = generateAvatarGallery();
|
||||
const avatar = gallery.find(a => a.id === avatarId);
|
||||
return avatar?.url || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar avatar aleatório da galeria
|
||||
* @returns Avatar aleatório
|
||||
*/
|
||||
export function getRandomAvatar(): Avatar {
|
||||
const gallery = generateAvatarGallery();
|
||||
const randomIndex = Math.floor(Math.random() * gallery.length);
|
||||
return gallery[randomIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Salvar avatar selecionado (retorna o ID para salvar no backend)
|
||||
* @param avatarId ID do avatar selecionado
|
||||
* @returns ID do avatar
|
||||
*/
|
||||
export function saveAvatarSelection(avatarId: string): string {
|
||||
return avatarId;
|
||||
}
|
||||
@@ -43,6 +43,16 @@ export const maskCEP = (value: string): string => {
|
||||
return digits.replace(/(\d{5})(\d{1,3})$/, "$1-$2");
|
||||
};
|
||||
|
||||
/** Format CNPJ: 00.000.000/0000-00 */
|
||||
export const maskCNPJ = (value: string): string => {
|
||||
const digits = onlyDigits(value).slice(0, 14);
|
||||
return digits
|
||||
.replace(/(\d{2})(\d)/, "$1.$2")
|
||||
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||
.replace(/(\d{3})(\d)/, "$1/$2")
|
||||
.replace(/(\d{4})(\d{1,2})$/, "$1-$2");
|
||||
};
|
||||
|
||||
/** Format phone: (00) 0000-0000 or (00) 00000-0000 */
|
||||
export const maskPhone = (value: string): string => {
|
||||
const digits = onlyDigits(value).slice(0, 11);
|
||||
|
||||
Reference in New Issue
Block a user