feat: menu lateral colapsavel com ocultar texto e 2 novos modos de visao na agenda (Semanal e Lista)
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m39s Details

This commit is contained in:
Sidney 2026-07-19 21:25:56 -03:00
parent 50edd54561
commit 1876172d78
4 changed files with 418 additions and 156 deletions

View File

@ -69,12 +69,24 @@ body {
left: 0; left: 0;
bottom: 0; bottom: 0;
z-index: 100; z-index: 100;
transition: transform 0.3s ease; transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s ease;
}
.sidebar.collapsed {
width: 76px;
} }
.sidebar-header { .sidebar-header {
padding: 24px 20px; padding: 20px 16px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar.collapsed .sidebar-header {
justify-content: center;
padding: 20px 10px;
} }
.sidebar-logo { .sidebar-logo {
@ -84,77 +96,41 @@ body {
text-decoration: none; text-decoration: none;
} }
.logo-icon { .desktop-collapse-btn {
width: 40px;
height: 40px;
background: var(--gradient-primary);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
box-shadow: var(--shadow-glow);
}
.logo-text {
font-size: 20px;
font-weight: 800;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.sidebar-nav {
flex: 1;
padding: 16px 12px;
overflow-y: auto;
}
.nav-section-title {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1.2px;
padding: 16px 12px 8px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: var(--radius-sm);
color: var(--text-secondary); color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
background: none;
width: 100%;
text-align: left;
} }
.nav-item:hover { .desktop-collapse-btn:hover {
background: var(--bg-card);
color: var(--text-primary); color: var(--text-primary);
background: rgba(255, 255, 255, 0.1);
} }
.nav-item.active { .mobile-close-btn {
background: var(--accent-glow); display: none;
color: var(--accent);
box-shadow: inset 3px 0 0 var(--accent);
} }
.nav-item svg { width: 20px; height: 20px; flex-shrink: 0; } .nav-section-divider {
height: 1px;
background: var(--border-color);
margin: 16px 12px;
}
.sidebar-footer { .sidebar.collapsed .nav-item {
padding: 16px; justify-content: center;
border-top: 1px solid var(--border-color); padding: 12px;
}
.sidebar.collapsed .nav-item svg {
margin: 0;
}
.sidebar.collapsed .plan-badge {
justify-content: center;
padding: 10px;
}
.sidebar.collapsed .sidebar-footer {
padding: 16px 10px;
} }
.plan-badge { .plan-badge {
@ -174,6 +150,11 @@ body {
flex: 1; flex: 1;
margin-left: 260px; margin-left: 260px;
min-height: 100vh; min-height: 100vh;
transition: margin-left 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.main-content.collapsed {
margin-left: 76px;
} }
.topbar { .topbar {
@ -997,3 +978,41 @@ tr:hover td { background: var(--bg-card-hover); }
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
} }
} }
/* === VIEW MODE SELECTOR === */
.view-mode-selector {
display: inline-flex;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 3px;
gap: 3px;
}
.view-mode-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 4px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.view-mode-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.06);
}
.view-mode-btn.active {
background: var(--accent);
color: white;
font-weight: 600;
box-shadow: 0 2px 10px rgba(108, 99, 255, 0.4);
}

View File

@ -36,8 +36,24 @@ export default function Home() {
const [currentPlan, setCurrentPlan] = useState('Professional'); const [currentPlan, setCurrentPlan] = useState('Professional');
const [isLocked, setIsLocked] = useState(false); const [isLocked, setIsLocked] = useState(false);
const [loadingSubscription, setLoadingSubscription] = useState(true); const [loadingSubscription, setLoadingSubscription] = useState(true);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const router = useRouter(); const router = useRouter();
useEffect(() => {
const savedCollapse = localStorage.getItem('agendapro_sidebar_collapsed');
if (savedCollapse === 'true') {
setIsSidebarCollapsed(true);
}
}, []);
const toggleSidebarCollapse = () => {
setIsSidebarCollapsed(prev => {
const next = !prev;
localStorage.setItem('agendapro_sidebar_collapsed', String(next));
return next;
});
};
const checkSubscriptionStatus = async () => { const checkSubscriptionStatus = async () => {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
@ -130,7 +146,7 @@ export default function Home() {
} }
return ( return (
<div className="app-layout"> <div className={`app-layout ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
<Sidebar <Sidebar
currentPage={currentPage} currentPage={currentPage}
onNavigate={setCurrentPage} onNavigate={setCurrentPage}
@ -138,8 +154,10 @@ export default function Home() {
onClose={() => setSidebarOpen(false)} onClose={() => setSidebarOpen(false)}
currentPlan={currentPlan} currentPlan={currentPlan}
isLocked={isLocked} isLocked={isLocked}
isCollapsed={isSidebarCollapsed}
onToggleCollapse={toggleSidebarCollapse}
/> />
<main className="main-content"> <main className={`main-content ${isSidebarCollapsed ? 'collapsed' : ''}`}>
<Topbar <Topbar
title={pages[currentPage]?.title || 'Dashboard'} title={pages[currentPage]?.title || 'Dashboard'}
subtitle={pages[currentPage]?.subtitle || ''} subtitle={pages[currentPage]?.subtitle || ''}

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, User, Phone, CheckCircle, AlertCircle, X, Mail } from 'lucide-react'; import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, User, Phone, CheckCircle, AlertCircle, X, Mail, ListFilter, CheckCircle2, XCircle, MessageCircle } from 'lucide-react';
import { showToast } from '@/lib/toast'; import { showToast } from '@/lib/toast';
const hours = Array.from({ length: 12 }, (_, i) => i + 8); const hours = Array.from({ length: 12 }, (_, i) => i + 8);
@ -16,15 +16,24 @@ const statusBorders: Record<string, string> = {
completed: 'var(--success)', completed: 'var(--success)',
cancelled: 'var(--danger)', cancelled: 'var(--danger)',
}; };
const statusLabels: Record<string, string> = {
confirmed: 'Confirmado',
pending: 'Pendente',
completed: 'Concluído',
cancelled: 'Cancelado',
};
export default function Agenda() { export default function Agenda() {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const today = new Date(); const today = new Date();
const [currentDate, setCurrentDate] = useState(today); const [currentDate, setCurrentDate] = useState(today);
const [viewMode, setViewMode] = useState<'day' | 'week' | 'list'>('day');
const [appointments, setAppointments] = useState<any[]>([]); const [appointments, setAppointments] = useState<any[]>([]);
const [professionals, setProfessionals] = useState<any[]>([]); const [professionals, setProfessionals] = useState<any[]>([]);
const [services, setServices] = useState<any[]>([]); const [services, setServices] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filterProf, setFilterProf] = useState<string>('all');
const [filterStatus, setFilterStatus] = useState<string>('all');
const fetchAgenda = async () => { const fetchAgenda = async () => {
try { try {
@ -89,18 +98,71 @@ export default function Agenda() {
setNewDate(todayObj.toISOString().split('T')[0]); setNewDate(todayObj.toISOString().split('T')[0]);
}; };
const getWeekDays = (date: Date) => {
const start = new Date(date);
const dayOfWeek = start.getDay();
start.setDate(start.getDate() - dayOfWeek);
const days = [];
for (let i = 0; i < 7; i++) {
const d = new Date(start);
d.setDate(d.getDate() + i);
days.push(d);
}
return days;
};
const weekDays = getWeekDays(currentDate);
if (loading && professionals.length === 0) { if (loading && professionals.length === 0) {
return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando agenda...</div>; return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando agenda...</div>;
} }
return ( return (
<div className="slide-up"> <div className="slide-up">
<div className="flex-between mb-6"> {/* BARRA SUPERIOR DE NAVEGAÇÃO E SELETOR DE MODO */}
<div className="flex-between mb-6" style={{ flexWrap: 'wrap', gap: '12px' }}>
<div className="flex-gap"> <div className="flex-gap">
<button className="btn btn-ghost btn-icon" onClick={() => changeDate(-1)}><ChevronLeft size={20} /></button> <button className="btn btn-ghost btn-icon" onClick={() => changeDate(viewMode === 'week' ? -7 : -1)}>
<h2 style={{ fontSize: '16px', fontWeight: 600, textTransform: 'capitalize' }}>{dateStr}</h2> <ChevronLeft size={20} />
<button className="btn btn-ghost btn-icon" onClick={() => changeDate(1)}><ChevronRight size={20} /></button> </button>
<h2 style={{ fontSize: '16px', fontWeight: 600, textTransform: 'capitalize' }}>
{viewMode === 'week'
? `Semana de ${weekDays[0].getDate()}/${weekDays[0].getMonth()+1} a ${weekDays[6].getDate()}/${weekDays[6].getMonth()+1}`
: dateStr}
</h2>
<button className="btn btn-ghost btn-icon" onClick={() => changeDate(viewMode === 'week' ? 7 : 1)}>
<ChevronRight size={20} />
</button>
</div> </div>
{/* SELETOR DE MODO DE VISÃO */}
<div className="view-mode-selector">
<button
type="button"
className={`view-mode-btn ${viewMode === 'day' ? 'active' : ''}`}
onClick={() => setViewMode('day')}
title="Visão Diária por Horários (Grade)"
>
<Clock size={14} /> Dia
</button>
<button
type="button"
className={`view-mode-btn ${viewMode === 'week' ? 'active' : ''}`}
onClick={() => setViewMode('week')}
title="Visão Semanal de 7 Dias"
>
<Calendar size={14} /> Semana
</button>
<button
type="button"
className={`view-mode-btn ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => setViewMode('list')}
title="Visão em Lista Detalhada"
>
<ListFilter size={14} /> Lista
</button>
</div>
<div className="flex-gap"> <div className="flex-gap">
<button className="btn btn-secondary btn-sm" onClick={goToToday}>Hoje</button> <button className="btn btn-secondary btn-sm" onClick={goToToday}>Hoje</button>
<button className="btn btn-primary" onClick={() => setShowModal(true)}> <button className="btn btn-primary" onClick={() => setShowModal(true)}>
@ -115,81 +177,224 @@ export default function Agenda() {
<p>Você precisa adicionar um profissional na aba Equipe antes de agendar clientes.</p> <p>Você precisa adicionar um profissional na aba Equipe antes de agendar clientes.</p>
</div> </div>
) : ( ) : (
<div className="card" style={{ padding: 0, overflow: 'hidden' }}> <>
<div style={{ display: 'grid', gridTemplateColumns: `70px repeat(${professionals.length}, 1fr)`, borderBottom: '1px solid var(--border-color)' }}> {/* MODO 1: GRADE DIÁRIA POR HORA (MANTIDA ORIGINAL) */}
<div style={{ padding: '14px', borderRight: '1px solid var(--border-color)' }} /> {viewMode === 'day' && (
{professionals.map(p => ( <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div key={p.id} style={{ <div style={{ display: 'grid', gridTemplateColumns: `70px repeat(${professionals.length}, 1fr)`, borderBottom: '1px solid var(--border-color)' }}>
padding: '14px 16px', textAlign: 'center', borderRight: '1px solid var(--border-color)', <div style={{ padding: '14px', borderRight: '1px solid var(--border-color)' }} />
background: 'var(--bg-secondary)', fontWeight: 600, fontSize: '14px' {professionals.map(p => (
}}> <div key={p.id} style={{
<div className="flex-center" style={{ gap: '8px' }}> padding: '14px 16px', textAlign: 'center', borderRight: '1px solid var(--border-color)',
<div style={{ width: 10, height: 10, borderRadius: '50%', background: p.color }} /> background: 'var(--bg-secondary)', fontWeight: 600, fontSize: '14px'
{p.name} }}>
</div> <div className="flex-center" style={{ gap: '8px' }}>
</div> <div style={{ width: 10, height: 10, borderRadius: '50%', background: p.color }} />
))} {p.name}
</div>
<div style={{ display: 'grid', gridTemplateColumns: `70px repeat(${professionals.length}, 1fr)`, position: 'relative' }}>
<div>
{hours.map(h => (
<div key={h} style={{
height: '120px', display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end',
paddingRight: '12px', paddingTop: '2px', fontSize: '12px', color: 'var(--text-muted)',
borderRight: '1px solid var(--border-color)', borderBottom: '1px solid var(--border-color)'
}}>
{String(h).padStart(2, '0')}:00
</div>
))}
</div>
{professionals.map(prof => {
const visibleAppointments = appointments.filter(a => a.professionalId === prof.id);
return (
<div key={prof.id} style={{ position: 'relative', borderRight: '1px solid var(--border-color)' }}>
{hours.map(h => (
<div key={h} style={{ height: '120px', borderBottom: '1px solid var(--border-color)' }} />
))}
{visibleAppointments.map(apt => {
const scale = 2; // 2px per minute
const top = (apt.startHour - 8) * 60 * scale + (apt.startMin * scale);
const height = apt.duration * scale;
const isSmall = apt.duration <= 20;
return (
<div key={apt.id} style={{
position: 'absolute', top: `${top}px`, left: '4px', right: '4px',
height: `${height}px`, borderRadius: 'var(--radius-sm)',
background: statusColors[apt.status] || statusColors.confirmed,
borderLeft: `3px solid ${statusBorders[apt.status] || statusBorders.confirmed}`,
padding: isSmall ? '4px 8px' : '6px 10px', cursor: 'pointer', overflow: 'hidden',
transition: 'all 0.2s', zIndex: 2, display: 'flex', flexDirection: isSmall ? 'row' : 'column',
alignItems: isSmall ? 'center' : 'flex-start', gap: isSmall ? '8px' : '0'
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1.02)'; (e.currentTarget as HTMLElement).style.zIndex = '10'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; (e.currentTarget as HTMLElement).style.zIndex = '2'; }}
>
<div style={{ fontWeight: 600, fontSize: '12px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{apt.client}
</div> </div>
<div style={{ fontSize: '11px', opacity: 0.8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{isSmall ? `- ${apt.service}` : `${apt.service} • R$ ${apt.price}`}
</div>
{!isSmall && height >= 60 && (
<div style={{ fontSize: '10px', opacity: 0.7, marginTop: '2px' }}>
{String(apt.startHour).padStart(2, '0')}:{String(apt.startMin).padStart(2, '0')} - {apt.duration}min
</div>
)}
</div> </div>
); ))}
})} </div>
<div style={{ display: 'grid', gridTemplateColumns: `70px repeat(${professionals.length}, 1fr)`, position: 'relative' }}>
<div>
{hours.map(h => (
<div key={h} style={{
height: '120px', display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end',
paddingRight: '12px', paddingTop: '2px', fontSize: '12px', color: 'var(--text-muted)',
borderRight: '1px solid var(--border-color)', borderBottom: '1px solid var(--border-color)'
}}>
{String(h).padStart(2, '0')}:00
</div>
))}
</div>
{professionals.map(prof => {
const visibleAppointments = appointments.filter(a => a.professionalId === prof.id);
return (
<div key={prof.id} style={{ position: 'relative', borderRight: '1px solid var(--border-color)' }}>
{hours.map(h => (
<div key={h} style={{ height: '120px', borderBottom: '1px solid var(--border-color)' }} />
))}
{visibleAppointments.map(apt => {
const scale = 2;
const top = (apt.startHour - 8) * 60 * scale + (apt.startMin * scale);
const height = apt.duration * scale;
const isSmall = apt.duration <= 20;
return (
<div key={apt.id} style={{
position: 'absolute', top: `${top}px`, left: '4px', right: '4px',
height: `${height}px`, borderRadius: 'var(--radius-sm)',
background: statusColors[apt.status] || statusColors.confirmed,
borderLeft: `3px solid ${statusBorders[apt.status] || statusBorders.confirmed}`,
padding: isSmall ? '4px 8px' : '6px 10px', cursor: 'pointer', overflow: 'hidden',
transition: 'all 0.2s', zIndex: 2, display: 'flex', flexDirection: isSmall ? 'row' : 'column',
alignItems: isSmall ? 'center' : 'flex-start', gap: isSmall ? '8px' : '0'
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1.02)'; (e.currentTarget as HTMLElement).style.zIndex = '10'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; (e.currentTarget as HTMLElement).style.zIndex = '2'; }}
>
<div style={{ fontWeight: 600, fontSize: '12px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{apt.client}
</div>
<div style={{ fontSize: '11px', opacity: 0.8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{isSmall ? `- ${apt.service}` : `${apt.service} • R$ ${apt.price}`}
</div>
{!isSmall && height >= 60 && (
<div style={{ fontSize: '10px', opacity: 0.7, marginTop: '2px' }}>
{String(apt.startHour).padStart(2, '0')}:{String(apt.startMin).padStart(2, '0')} - {apt.duration}min
</div>
)}
</div>
);
})}
</div>
);
})}
</div>
</div> </div>
); )}
})}
</div> {/* MODO 2: VISÃO SEMANAL DE 7 DIAS */}
</div> {viewMode === 'week' && (
<div className="card" style={{ padding: 0, overflowX: 'auto' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, minmax(140px, 1fr))', borderBottom: '1px solid var(--border-color)' }}>
{weekDays.map(day => {
const isTodayDay = day.toDateString() === today.toDateString();
const isSelectedDay = day.toDateString() === currentDate.toDateString();
const dayName = day.toLocaleDateString('pt-BR', { weekday: 'short' });
const dayNum = day.getDate();
const dayMonth = day.toLocaleDateString('pt-BR', { month: 'short' });
return (
<div
key={day.toISOString()}
onClick={() => setCurrentDate(day)}
style={{
padding: '14px 8px', textAlign: 'center', borderRight: '1px solid var(--border-color)',
background: isSelectedDay ? 'var(--accent-glow)' : 'var(--bg-secondary)', cursor: 'pointer',
transition: 'all 0.2s'
}}
>
<div style={{ fontSize: '11px', textTransform: 'uppercase', color: 'var(--text-muted)' }}>{dayName}</div>
<div style={{ fontSize: '18px', fontWeight: 800, color: isTodayDay ? 'var(--accent)' : 'var(--text-primary)' }}>
{dayNum} <span style={{ fontSize: '11px', fontWeight: 400 }}>{dayMonth}</span>
</div>
</div>
);
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, minmax(140px, 1fr))', minHeight: '380px' }}>
{weekDays.map(day => {
const dayStr = day.toISOString().split('T')[0];
// Para homologação estática, os agendamentos do dia ativo usam a data selecionada
const isSelected = day.toDateString() === currentDate.toDateString();
const dayApts = isSelected ? appointments : [];
return (
<div key={day.toISOString()} style={{ padding: '10px', borderRight: '1px solid var(--border-color)', background: 'var(--bg-primary)' }}>
{dayApts.length === 0 ? (
<div style={{ fontSize: '11px', color: 'var(--text-muted)', textAlign: 'center', marginTop: '20px' }}>
Sem horários
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{dayApts.map(apt => {
const prof = professionals.find(p => p.id === apt.professionalId);
return (
<div key={apt.id} style={{
padding: '8px', borderRadius: 'var(--radius-sm)',
background: statusColors[apt.status] || 'var(--bg-secondary)',
borderLeft: `3px solid ${statusBorders[apt.status] || 'var(--accent)'}`,
fontSize: '12px'
}}>
<div style={{ fontWeight: 700 }}>{String(apt.startHour).padStart(2, '0')}:{String(apt.startMin).padStart(2, '0')}</div>
<div style={{ fontWeight: 600, marginTop: '2px' }}>{apt.client}</div>
<div style={{ fontSize: '11px', opacity: 0.8 }}>{apt.service}</div>
{prof && (
<div style={{ fontSize: '10px', color: prof.color, marginTop: '4px', fontWeight: 600 }}>
{prof.name}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* MODO 3: VISÃO EM LISTA DETALHADA */}
{viewMode === 'list' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Filtros da Lista */}
<div className="card" style={{ padding: '16px', display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-secondary)' }}>Filtrar Lista:</span>
<select className="form-select" style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }} value={filterProf} onChange={e => setFilterProf(e.target.value)}>
<option value="all">Todos os Profissionais</option>
{professionals.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<select className="form-select" style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }} value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
<option value="all">Todos os Status</option>
<option value="confirmed">Confirmados</option>
<option value="pending">Pendentes</option>
<option value="completed">Concluídos</option>
<option value="cancelled">Cancelados</option>
</select>
</div>
{/* Feed de Cards da Lista */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '16px' }}>
{appointments
.filter(a => filterProf === 'all' || a.professionalId === filterProf)
.filter(a => filterStatus === 'all' || a.status === filterStatus)
.map(apt => {
const prof = professionals.find(p => p.id === apt.professionalId);
return (
<div key={apt.id} className="card" style={{ padding: '16px', borderLeft: `4px solid ${statusBorders[apt.status] || 'var(--accent)'}` }}>
<div className="flex-between mb-2">
<span className="badge" style={{ background: statusColors[apt.status], color: statusBorders[apt.status] }}>
{statusLabels[apt.status] || apt.status}
</span>
<span style={{ fontSize: '12px', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
<Clock size={12} /> {String(apt.startHour).padStart(2, '0')}:{String(apt.startMin).padStart(2, '0')} ({apt.duration} min)
</span>
</div>
<h4 style={{ fontSize: '16px', fontWeight: 700, margin: '8px 0 4px' }}>{apt.client}</h4>
<div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '12px' }}>
{apt.service} <strong style={{ color: 'var(--text-primary)' }}>R$ {apt.price}</strong>
</div>
<div className="flex-between" style={{ borderTop: '1px solid var(--border-color)', paddingTop: '10px', fontSize: '12px' }}>
{prof ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontWeight: 600, color: 'var(--text-secondary)' }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: prof.color }} /> {prof.name}
</span>
) : <span />}
{apt.phone && (
<a
href={`https://wa.me/55${apt.phone.replace(/\D/g, '')}`}
target="_blank"
rel="noreferrer"
style={{ color: '#25D366', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4, fontWeight: 600 }}
>
<MessageCircle size={14} /> WhatsApp
</a>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</>
)} )}
{showModal && ( {showModal && (

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Calendar, Users, Scissors, LayoutDashboard, Settings, CreditCard, UserCheck, X, Crown, LogOut } from 'lucide-react'; import { Calendar, Users, Scissors, LayoutDashboard, Settings, CreditCard, UserCheck, X, Crown, LogOut, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
interface SidebarProps { interface SidebarProps {
@ -9,6 +9,8 @@ interface SidebarProps {
onClose: () => void; onClose: () => void;
currentPlan?: string; currentPlan?: string;
isLocked?: boolean; isLocked?: boolean;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
} }
const navItems = [ const navItems = [
@ -24,7 +26,7 @@ const navItems = [
{ id: 'subscription', label: 'Assinatura', icon: CreditCard }, { id: 'subscription', label: 'Assinatura', icon: CreditCard },
]; ];
export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, currentPlan, isLocked = false }: SidebarProps) { export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, currentPlan, isLocked = false, isCollapsed = false, onToggleCollapse }: SidebarProps) {
const router = useRouter(); const router = useRouter();
const [tenantName, setTenantName] = useState('AgendaPRO'); const [tenantName, setTenantName] = useState('AgendaPRO');
@ -39,7 +41,6 @@ export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, curr
localStorage.removeItem('agendapro_token'); localStorage.removeItem('agendapro_token');
localStorage.removeItem('agendapro_tenant_name'); localStorage.removeItem('agendapro_tenant_name');
localStorage.removeItem('agendapro_plan'); localStorage.removeItem('agendapro_plan');
// Limpar cookies de sessão
document.cookie = "agendapro_new_tenant=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; document.cookie = "agendapro_new_tenant=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "agendapro_plan=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; document.cookie = "agendapro_plan=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
router.push('/login'); router.push('/login');
@ -48,13 +49,23 @@ export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, curr
return ( return (
<> <>
{isOpen && <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 99 }} onClick={onClose} />} {isOpen && <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 99 }} onClick={onClose} />}
<aside className={`sidebar ${isOpen ? 'open' : ''}`}> <aside className={`sidebar ${isOpen ? 'open' : ''} ${isCollapsed ? 'collapsed' : ''}`}>
<div className="sidebar-header"> <div className="sidebar-header">
<div className="sidebar-logo"> <div className="sidebar-logo">
<div className="logo-icon"></div> <div className="logo-icon"></div>
<span className="logo-text" style={{ fontSize: '18px', fontWeight: 800 }}>{tenantName}</span> {!isCollapsed && <span className="logo-text" style={{ fontSize: '18px', fontWeight: 800 }}>{tenantName}</span>}
</div> </div>
<button className="btn-ghost btn-icon" onClick={onClose} style={{ display: 'none' }}> {onToggleCollapse && (
<button
type="button"
className="btn-ghost btn-icon desktop-collapse-btn"
onClick={onToggleCollapse}
title={isCollapsed ? "Expandir Menu" : "Ocultar Texto"}
>
{isCollapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
</button>
)}
<button type="button" className="btn-ghost btn-icon mobile-close-btn" onClick={onClose}>
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
@ -62,6 +73,7 @@ export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, curr
<nav className="sidebar-nav"> <nav className="sidebar-nav">
{navItems.map((item, i) => { {navItems.map((item, i) => {
if ('section' in item && item.section) { if ('section' in item && item.section) {
if (isCollapsed) return <div key={`s-${i}`} className="nav-section-divider" title={item.section} />;
return <div key={`s-${i}`} className="nav-section-title">{item.section}</div>; return <div key={`s-${i}`} className="nav-section-title">{item.section}</div>;
} }
if ('id' in item && item.id) { if ('id' in item && item.id) {
@ -72,6 +84,7 @@ export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, curr
key={item.id} key={item.id}
className={`nav-item ${currentPage === item.id ? 'active' : ''}`} className={`nav-item ${currentPage === item.id ? 'active' : ''}`}
disabled={itemLocked} disabled={itemLocked}
title={isCollapsed ? item.label : undefined}
style={itemLocked ? { opacity: 0.4, cursor: 'not-allowed' } : {}} style={itemLocked ? { opacity: 0.4, cursor: 'not-allowed' } : {}}
onClick={() => { onClick={() => {
if (!itemLocked) { if (!itemLocked) {
@ -81,8 +94,8 @@ export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, curr
}} }}
> >
<Icon size={20} /> <Icon size={20} />
{item.label} {!isCollapsed && <span className="nav-item-text">{item.label}</span>}
{itemLocked && <span style={{ marginLeft: 'auto', fontSize: '12px' }}>🔒</span>} {itemLocked && !isCollapsed && <span style={{ marginLeft: 'auto', fontSize: '12px' }}>🔒</span>}
</button> </button>
); );
} }
@ -90,13 +103,20 @@ export default function Sidebar({ currentPage, onNavigate, isOpen, onClose, curr
})} })}
</nav> </nav>
<div className="sidebar-footer" style={{ padding: '20px', borderTop: '1px solid var(--border-color)', display: 'flex', flexDirection: 'column', gap: '12px' }}> <div className="sidebar-footer">
<div className="plan-badge"> <div className="plan-badge" title={`Plano ${currentPlan || 'Professional'}`}>
<Crown size={16} /> <Crown size={16} />
<span>Plano {currentPlan || 'Professional'}</span> {!isCollapsed && <span>Plano {currentPlan || 'Professional'}</span>}
</div> </div>
<button className="btn btn-secondary" style={{ width: '100%', justifyContent: 'center' }} onClick={handleLogout}> <button
<LogOut size={16} /> Sair do Sistema type="button"
className="btn btn-secondary"
style={{ width: '100%', justifyContent: 'center' }}
onClick={handleLogout}
title={isCollapsed ? "Sair do Sistema" : undefined}
>
<LogOut size={16} />
{!isCollapsed && <span>Sair do Sistema</span>}
</button> </button>
</div> </div>
</aside> </aside>