Merge branch 'master' into feat-controle-ponto

This commit is contained in:
2025-11-21 12:45:31 -03:00
committed by GitHub
53 changed files with 2962 additions and 2240 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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)

View File

@@ -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}

View File

@@ -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>