feat: mudancas de layout

This commit is contained in:
yagostn 2025-12-05 10:43:48 -03:00
parent 4e9b743928
commit f73095e3d4
9 changed files with 1297 additions and 466 deletions

View file

@ -4,7 +4,6 @@ import { Home } from "./pages/Home";
import { Dashboard } from "./pages/Dashboard"; import { Dashboard } from "./pages/Dashboard";
import { Login } from "./pages/Login"; import { Login } from "./pages/Login";
import { Register } from "./pages/Register"; import { Register } from "./pages/Register";
import { CalendarPage } from "./pages/Calendar";
import { TeamPage } from "./pages/Team"; import { TeamPage } from "./pages/Team";
import { FinancePage } from "./pages/Finance"; import { FinancePage } from "./pages/Finance";
import { SettingsPage } from "./pages/Settings"; import { SettingsPage } from "./pages/Settings";
@ -54,9 +53,6 @@ const AppContent: React.FC = () => {
case "inspiration": case "inspiration":
return <InspirationPage />; return <InspirationPage />;
case "calendar":
return <CalendarPage />;
case "team": case "team":
return <TeamPage />; return <TeamPage />;

View file

@ -0,0 +1,225 @@
import React from 'react';
import { Calendar, MapPin, Clock, Filter, X } from 'lucide-react';
export interface EventFilters {
date: string;
city: string;
state: string;
timeRange: string;
type: string;
}
interface EventFiltersBarProps {
filters: EventFilters;
onFilterChange: (filters: EventFilters) => void;
availableCities: string[];
availableStates: string[];
availableTypes: string[];
}
export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
filters,
onFilterChange,
availableCities,
availableStates,
availableTypes,
}) => {
const handleReset = () => {
onFilterChange({
date: '',
city: '',
state: '',
timeRange: '',
type: '',
});
};
const hasActiveFilters = Object.values(filters).some(value => value !== '');
const timeRanges = [
{ value: '', label: 'Todos os horários' },
{ value: 'morning', label: 'Manhã (06:00 - 12:00)' },
{ value: 'afternoon', label: 'Tarde (12:00 - 18:00)' },
{ value: 'evening', label: 'Noite (18:00 - 23:59)' },
];
return (
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Filter size={18} className="text-brand-gold" />
<h3 className="font-semibold text-gray-800">Filtros Avançados</h3>
</div>
{hasActiveFilters && (
<button
onClick={handleReset}
className="flex items-center gap-1 text-sm text-gray-600 hover:text-brand-gold transition-colors"
>
<X size={16} />
Limpar filtros
</button>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3">
{/* Filtro por Data */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<Calendar size={14} className="text-brand-gold" />
Data
</label>
<input
type="date"
value={filters.date}
onChange={(e) => onFilterChange({ ...filters, date: e.target.value })}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors"
/>
</div>
{/* Filtro por Estado */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<MapPin size={14} className="text-brand-gold" />
Estado
</label>
<select
value={filters.state}
onChange={(e) => onFilterChange({ ...filters, state: e.target.value, city: '' })}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white"
>
<option value="">Todos os estados</option>
{availableStates.map((state) => (
<option key={state} value={state}>
{state}
</option>
))}
</select>
</div>
{/* Filtro por Cidade */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<MapPin size={14} className="text-brand-gold" />
Cidade
</label>
<select
value={filters.city}
onChange={(e) => onFilterChange({ ...filters, city: e.target.value })}
disabled={!filters.state}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Todas as cidades</option>
{availableCities.map((city) => (
<option key={city} value={city}>
{city}
</option>
))}
</select>
</div>
{/* Filtro por Horário */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<Clock size={14} className="text-brand-gold" />
Horário
</label>
<select
value={filters.timeRange}
onChange={(e) => onFilterChange({ ...filters, timeRange: e.target.value })}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white"
>
{timeRanges.map((range) => (
<option key={range.value} value={range.value}>
{range.label}
</option>
))}
</select>
</div>
{/* Filtro por Tipo */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<Filter size={14} className="text-brand-gold" />
Tipo de Evento
</label>
<select
value={filters.type}
onChange={(e) => onFilterChange({ ...filters, type: e.target.value })}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white"
>
<option value="">Todos os tipos</option>
{availableTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
</div>
{/* Active Filters Display */}
{hasActiveFilters && (
<div className="mt-3 pt-3 border-t border-gray-100">
<div className="flex flex-wrap gap-2">
<span className="text-xs text-gray-500">Filtros ativos:</span>
{filters.date && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
Data: {new Date(filters.date + 'T00:00:00').toLocaleDateString('pt-BR')}
<button
onClick={() => onFilterChange({ ...filters, date: '' })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
{filters.state && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
Estado: {filters.state}
<button
onClick={() => onFilterChange({ ...filters, state: '', city: '' })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
{filters.city && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
Cidade: {filters.city}
<button
onClick={() => onFilterChange({ ...filters, city: '' })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
{filters.timeRange && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
{timeRanges.find(r => r.value === filters.timeRange)?.label}
<button
onClick={() => onFilterChange({ ...filters, timeRange: '' })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
{filters.type && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
Tipo: {filters.type}
<button
onClick={() => onFilterChange({ ...filters, type: '' })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
</div>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,174 @@
import React from 'react';
import { EventData, EventStatus, UserRole } from '../types';
import { Calendar, MapPin, Users, CheckCircle, Clock } from 'lucide-react';
import { STATUS_COLORS } from '../constants';
interface EventTableProps {
events: EventData[];
onEventClick: (event: EventData) => void;
onApprove?: (e: React.MouseEvent, eventId: string) => void;
userRole: UserRole;
}
export const EventTable: React.FC<EventTableProps> = ({
events,
onEventClick,
onApprove,
userRole
}) => {
const canApprove = (userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN);
const formatDate = (date: string) => {
const eventDate = new Date(date + 'T00:00:00');
return eventDate.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const getStatusDisplay = (status: EventStatus) => {
const statusLabels: Record<EventStatus, string> = {
[EventStatus.PENDING_APPROVAL]: 'Pendente',
[EventStatus.CONFIRMED]: 'Confirmado',
[EventStatus.PLANNING]: 'Planejamento',
[EventStatus.IN_PROGRESS]: 'Em Andamento',
[EventStatus.COMPLETED]: 'Concluído',
[EventStatus.ARCHIVED]: 'Arquivado',
};
return statusLabels[status] || status;
};
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
{canApprove && (
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-20">
Ações
</th>
)}
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Nome do Evento
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Tipo
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Data
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Horário
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Local
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Contatos
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Equipe
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{events.map((event) => (
<tr
key={event.id}
onClick={() => onEventClick(event)}
className="hover:bg-gray-50 cursor-pointer transition-colors"
>
{canApprove && (
<td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
{event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => onApprove?.(e, event.id)}
className="bg-green-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-green-600 transition-colors flex items-center gap-1 whitespace-nowrap"
title="Aprovar evento"
>
<CheckCircle size={12} />
Aprovar
</button>
)}
</td>
)}
<td className="px-4 py-3">
<div className="font-medium text-gray-900 text-sm">{event.name}</div>
</td>
<td className="px-4 py-3">
<span className="text-xs text-gray-600 uppercase tracking-wide">{event.type}</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<Calendar size={14} className="mr-1.5 text-brand-gold flex-shrink-0" />
{formatDate(event.date)}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<Clock size={14} className="mr-1.5 text-gray-400 flex-shrink-0" />
{event.time}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<MapPin size={14} className="mr-1.5 text-brand-gold flex-shrink-0" />
<span className="truncate max-w-[200px]" title={`${event.address.street}, ${event.address.number} - ${event.address.city}/${event.address.state}`}>
{event.address.city}, {event.address.state}
</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<Users size={14} className="mr-1.5 text-gray-400 flex-shrink-0" />
{event.contacts.length}
</div>
</td>
<td className="px-4 py-3">
{event.photographerIds.length > 0 ? (
<div className="flex -space-x-1">
{event.photographerIds.slice(0, 3).map((id, idx) => (
<div
key={id}
className="w-6 h-6 rounded-full border-2 border-white bg-gray-300"
style={{
backgroundImage: `url(https://i.pravatar.cc/100?u=${id})`,
backgroundSize: 'cover',
}}
title={id}
/>
))}
{event.photographerIds.length > 3 && (
<div className="w-6 h-6 rounded-full border-2 border-white bg-gray-100 flex items-center justify-center text-[10px] text-gray-600 font-medium">
+{event.photographerIds.length - 3}
</div>
)}
</div>
) : (
<span className="text-xs text-gray-400">-</span>
)}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-semibold ${STATUS_COLORS[event.status]}`}>
{getStatusDisplay(event.status)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{events.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>Nenhum evento encontrado.</p>
</div>
)}
</div>
);
};

View file

@ -63,7 +63,6 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
case UserRole.PHOTOGRAPHER: case UserRole.PHOTOGRAPHER:
return [ return [
{ name: "Eventos Designados", path: "dashboard" }, { name: "Eventos Designados", path: "dashboard" },
{ name: "Agenda", path: "calendar" },
]; ];
default: default:
return []; return [];

View file

@ -25,10 +25,10 @@ const INITIAL_INSTITUTIONS: Institution[] = [
const INITIAL_EVENTS: EventData[] = [ const INITIAL_EVENTS: EventData[] = [
{ {
id: "1", id: "1",
name: "Casamento Juliana & Marcos", name: "Formatura Engenharia Civil",
date: "2024-10-15", date: "2025-12-05",
time: "16:00", time: "19:00",
type: EventType.WEDDING, type: EventType.GRADUATION,
status: EventStatus.CONFIRMED, status: EventStatus.CONFIRMED,
address: { address: {
street: "Av. das Hortênsias", street: "Av. das Hortênsias",
@ -37,30 +37,29 @@ const INITIAL_EVENTS: EventData[] = [
state: "RS", state: "RS",
zip: "95670-000", zip: "95670-000",
}, },
briefing: briefing: "Cerimônia de formatura com 120 formandos. Foco em fotos individuais e da turma.",
"Cerimônia ao pôr do sol. Foco em fotos espontâneas dos noivos e pais.",
coverImage: "https://picsum.photos/id/1059/800/400", coverImage: "https://picsum.photos/id/1059/800/400",
contacts: [ contacts: [
{ {
id: "c1", id: "c1",
name: "Cerimonial Silva", name: "Comissão de Formatura",
role: "Cerimonialista", role: "Organizador",
phone: "9999-9999", phone: "51 99999-1111",
email: "c@teste.com", email: "formatura@email.com",
}, },
], ],
checklist: [], checklist: [],
ownerId: "client-1", ownerId: "client-1",
photographerIds: ["photographer-1"], photographerIds: ["photographer-1", "photographer-2"],
institutionId: "inst-1", institutionId: "inst-1",
}, },
{ {
id: "2", id: "2",
name: "Conferência Tech Innovators", name: "Colação de Grau Medicina",
date: "2024-11-05", date: "2025-12-05",
time: "08:00", time: "10:00",
type: EventType.CORPORATE, type: EventType.COLATION,
status: EventStatus.PENDING_APPROVAL, status: EventStatus.CONFIRMED,
address: { address: {
street: "Rua Olimpíadas", street: "Rua Olimpíadas",
number: "205", number: "205",
@ -68,13 +67,463 @@ const INITIAL_EVENTS: EventData[] = [
state: "SP", state: "SP",
zip: "04551-000", zip: "04551-000",
}, },
briefing: "Cobrir palestras principais e networking no coffee break.", briefing: "Colação de grau solene. Capturar juramento e entrega de diplomas.",
coverImage: "https://picsum.photos/id/3/800/400", coverImage: "https://picsum.photos/id/3/800/400",
contacts: [
{
id: "c2",
name: "Secretaria Acadêmica",
role: "Coordenador",
phone: "11 98888-2222",
email: "academico@med.br",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-1"],
},
{
id: "3",
name: "Semana Acadêmica Direito",
date: "2025-12-05",
time: "14:00",
type: EventType.ACADEMIC_WEEK,
status: EventStatus.IN_PROGRESS,
address: {
street: "Av. Paulista",
number: "1500",
city: "São Paulo",
state: "SP",
zip: "01310-100",
},
briefing: "Palestras e painéis durante toda a semana. Cobertura de 3 dias.",
coverImage: "https://picsum.photos/id/10/800/400",
contacts: [], contacts: [],
checklist: [], checklist: [],
ownerId: "client-2", // Other client ownerId: "client-2",
photographerIds: ["photographer-2"],
},
{
id: "4",
name: "Defesa de Doutorado - Maria Silva",
date: "2025-12-05",
time: "15:30",
type: EventType.DEFENSE,
status: EventStatus.CONFIRMED,
address: {
street: "Rua Ramiro Barcelos",
number: "2600",
city: "Porto Alegre",
state: "RS",
zip: "90035-003",
},
briefing: "Defesa de tese em sala fechada. Fotos discretas da apresentação e banca.",
coverImage: "https://picsum.photos/id/20/800/400",
contacts: [
{
id: "c3",
name: "Prof. João Santos",
role: "Orientador",
phone: "51 97777-3333",
email: "joao@univ.br",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-1"],
},
{
id: "5",
name: "Semana de Calouros 2026",
date: "2025-12-06",
time: "09:00",
type: EventType.FRESHMAN_WEEK,
status: EventStatus.PENDING_APPROVAL,
address: {
street: "Campus Universitário",
number: "s/n",
city: "Curitiba",
state: "PR",
zip: "80060-000",
},
briefing: "Recepção dos calouros com atividades de integração e gincanas.",
coverImage: "https://picsum.photos/id/30/800/400",
contacts: [],
checklist: [],
ownerId: "client-2",
photographerIds: [], photographerIds: [],
}, },
{
id: "6",
name: "Formatura Administração",
date: "2025-12-06",
time: "20:00",
type: EventType.GRADUATION,
status: EventStatus.CONFIRMED,
address: {
street: "Av. Ipiranga",
number: "6681",
city: "Porto Alegre",
state: "RS",
zip: "90619-900",
},
briefing: "Formatura noturna com jantar. Fotos da cerimônia e festa.",
coverImage: "https://picsum.photos/id/40/800/400",
contacts: [
{
id: "c4",
name: "Lucas Oliveira",
role: "Presidente da Comissão",
phone: "51 96666-4444",
email: "lucas@formatura.com",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-2"],
},
{
id: "7",
name: "Congresso de Tecnologia",
date: "2025-12-06",
time: "08:30",
type: EventType.SYMPOSIUM,
status: EventStatus.CONFIRMED,
address: {
street: "Av. das Nações Unidas",
number: "12901",
city: "São Paulo",
state: "SP",
zip: "04578-000",
},
briefing: "Congresso com múltiplas salas. Cobrir palestrantes principais e stands.",
coverImage: "https://picsum.photos/id/50/800/400",
contacts: [
{
id: "c5",
name: "Eventos Tech",
role: "Organizadora",
phone: "11 95555-5555",
email: "contato@eventostech.com",
},
],
checklist: [],
ownerId: "client-2",
photographerIds: ["photographer-1", "photographer-3"],
},
{
id: "8",
name: "Campeonato Universitário de Futsal",
date: "2025-12-06",
time: "16:00",
type: EventType.SPORTS_EVENT,
status: EventStatus.CONFIRMED,
address: {
street: "Rua dos Esportes",
number: "500",
city: "Gramado",
state: "RS",
zip: "95670-100",
},
briefing: "Final do campeonato. Fotos dinâmicas da partida e premiação.",
coverImage: "https://picsum.photos/id/60/800/400",
contacts: [],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-3"],
},
{
id: "9",
name: "Colação de Grau Odontologia",
date: "2025-12-07",
time: "11:00",
type: EventType.COLATION,
status: EventStatus.PLANNING,
address: {
street: "Rua Voluntários da Pátria",
number: "89",
city: "Porto Alegre",
state: "RS",
zip: "90230-010",
},
briefing: "Cerimônia formal de colação. Fotos individuais e em grupo.",
coverImage: "https://picsum.photos/id/70/800/400",
contacts: [
{
id: "c6",
name: "Direção da Faculdade",
role: "Coordenador",
phone: "51 94444-6666",
email: "direcao@odonto.edu",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-2"],
},
{
id: "10",
name: "Festival Cultural Universitário",
date: "2025-12-07",
time: "18:00",
type: EventType.CULTURAL_EVENT,
status: EventStatus.CONFIRMED,
address: {
street: "Praça da República",
number: "s/n",
city: "São Paulo",
state: "SP",
zip: "01045-000",
},
briefing: "Festival com apresentações musicais e teatrais. Cobertura completa.",
coverImage: "https://picsum.photos/id/80/800/400",
contacts: [],
checklist: [],
ownerId: "client-2",
photographerIds: ["photographer-1"],
},
{
id: "11",
name: "Defesa de Mestrado - Pedro Costa",
date: "2025-12-07",
time: "14:00",
type: EventType.DEFENSE,
status: EventStatus.CONFIRMED,
address: {
street: "Av. Bento Gonçalves",
number: "9500",
city: "Porto Alegre",
state: "RS",
zip: "91509-900",
},
briefing: "Defesa de dissertação. Registro da apresentação e momento da aprovação.",
coverImage: "https://picsum.photos/id/90/800/400",
contacts: [],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-3"],
},
{
id: "12",
name: "Formatura Psicologia",
date: "2025-12-08",
time: "19:30",
type: EventType.GRADUATION,
status: EventStatus.CONFIRMED,
address: {
street: "Av. Protásio Alves",
number: "7000",
city: "Porto Alegre",
state: "RS",
zip: "91310-000",
},
briefing: "Formatura emotiva com homenagens. Foco em momentos especiais.",
coverImage: "https://picsum.photos/id/100/800/400",
contacts: [
{
id: "c7",
name: "Ana Paula",
role: "Formanda",
phone: "51 93333-7777",
email: "ana@email.com",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-1", "photographer-2"],
},
{
id: "13",
name: "Simpósio de Engenharia",
date: "2025-12-08",
time: "09:00",
type: EventType.SYMPOSIUM,
status: EventStatus.CONFIRMED,
address: {
street: "Av. Sertório",
number: "6600",
city: "Porto Alegre",
state: "RS",
zip: "91040-000",
},
briefing: "Apresentações técnicas e workshops. Cobrir painéis principais.",
coverImage: "https://picsum.photos/id/110/800/400",
contacts: [],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-2"],
},
{
id: "14",
name: "Torneio de Vôlei Universitário",
date: "2025-12-08",
time: "15:00",
type: EventType.SPORTS_EVENT,
status: EventStatus.IN_PROGRESS,
address: {
street: "Rua Faria Santos",
number: "100",
city: "Curitiba",
state: "PR",
zip: "80060-150",
},
briefing: "Semifinais e final. Fotos de ação e torcida.",
coverImage: "https://picsum.photos/id/120/800/400",
contacts: [],
checklist: [],
ownerId: "client-2",
photographerIds: ["photographer-3"],
},
{
id: "15",
name: "Colação de Grau Enfermagem",
date: "2025-12-09",
time: "10:30",
type: EventType.COLATION,
status: EventStatus.CONFIRMED,
address: {
street: "Rua São Manoel",
number: "963",
city: "São Paulo",
state: "SP",
zip: "01330-001",
},
briefing: "Colação com juramento de Florence Nightingale. Momento solene.",
coverImage: "https://picsum.photos/id/130/800/400",
contacts: [
{
id: "c8",
name: "Coordenação de Enfermagem",
role: "Coordenador",
phone: "11 92222-8888",
email: "coord@enf.br",
},
],
checklist: [],
ownerId: "client-2",
photographerIds: ["photographer-1"],
},
{
id: "16",
name: "Semana Acadêmica Biomedicina",
date: "2025-12-09",
time: "13:00",
type: EventType.ACADEMIC_WEEK,
status: EventStatus.PLANNING,
address: {
street: "Av. Independência",
number: "2293",
city: "Porto Alegre",
state: "RS",
zip: "90035-075",
},
briefing: "Palestras e atividades práticas. Cobertura de 2 dias.",
coverImage: "https://picsum.photos/id/140/800/400",
contacts: [],
checklist: [],
ownerId: "client-1",
photographerIds: [],
},
{
id: "17",
name: "Formatura Ciências Contábeis",
date: "2025-12-09",
time: "20:30",
type: EventType.GRADUATION,
status: EventStatus.CONFIRMED,
address: {
street: "Av. das Américas",
number: "3500",
city: "Gramado",
state: "RS",
zip: "95670-200",
},
briefing: "Formatura elegante em hotel. Cobertura completa da cerimônia e recepção.",
coverImage: "https://picsum.photos/id/150/800/400",
contacts: [
{
id: "c9",
name: "Rodrigo Almeida",
role: "Tesoureiro",
phone: "51 91111-9999",
email: "rodrigo@turma.com",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-2", "photographer-3"],
},
{
id: "18",
name: "Defesa de TCC - Turma 2025",
date: "2025-12-09",
time: "16:30",
type: EventType.DEFENSE,
status: EventStatus.CONFIRMED,
address: {
street: "Rua Marquês do Pombal",
number: "2000",
city: "Porto Alegre",
state: "RS",
zip: "90540-000",
},
briefing: "Múltiplas defesas sequenciais. Fotos rápidas de cada apresentação.",
coverImage: "https://picsum.photos/id/160/800/400",
contacts: [],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-1"],
},
{
id: "19",
name: "Festival de Música Universitária",
date: "2025-12-10",
time: "19:00",
type: EventType.CULTURAL_EVENT,
status: EventStatus.PENDING_APPROVAL,
address: {
street: "Parque da Redenção",
number: "s/n",
city: "Porto Alegre",
state: "RS",
zip: "90040-000",
},
briefing: "Festival ao ar livre com várias bandas. Fotos de palco e público.",
coverImage: "https://picsum.photos/id/170/800/400",
contacts: [],
checklist: [],
ownerId: "client-2",
photographerIds: [],
},
{
id: "20",
name: "Colação de Grau Arquitetura",
date: "2025-12-10",
time: "11:30",
type: EventType.COLATION,
status: EventStatus.CONFIRMED,
address: {
street: "Av. Borges de Medeiros",
number: "1501",
city: "Gramado",
state: "RS",
zip: "95670-300",
},
briefing: "Cerimônia especial com exposição de projetos. Fotos criativas.",
coverImage: "https://picsum.photos/id/180/800/400",
contacts: [
{
id: "c10",
name: "Atelier Arquitetura",
role: "Escritório Parceiro",
phone: "51 90000-1010",
email: "contato@atelier.arq",
},
],
checklist: [],
ownerId: "client-1",
photographerIds: ["photographer-3"],
},
]; ];
interface DataContextType { interface DataContextType {

View file

@ -157,221 +157,207 @@ export const CalendarPage: React.FC = () => {
}); });
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 pt-20 sm:pt-24 md:pt-32 pb-8 sm:pb-12"> <div className="min-h-screen bg-white pt-24 pb-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-6 lg:px-8"> <div className="max-w-7xl mx-auto">
{/* Header */} {/* Header */}
<div className="mb-4 sm:mb-6 md:mb-8 flex flex-col gap-3 sm:gap-4"> <div className="mb-8 fade-in">
<div> <h1 className="text-3xl font-serif font-bold text-brand-black">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#B8D033] mb-1 sm:mb-2"> Minha Agenda
Minha Agenda </h1>
</h1> <p className="text-gray-500 mt-1">
<p className="text-xs sm:text-sm md:text-base text-gray-600"> Gerencie seus eventos e compromissos fotográficos
Gerencie seus eventos e compromissos fotográficos </p>
</p>
</div>
<div className="flex gap-2">
</div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6"> <div className="space-y-6 fade-in">
{/* Calendar Section */} {/* Calendar Card */}
<div className="lg:col-span-2 space-y-4 sm:space-y-6"> <div className="bg-white rounded-xl border border-gray-200 shadow-lg overflow-hidden">
{/* Calendar Card */} {/* Header */}
<div className="bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden"> <div className="bg-gradient-to-r from-brand-black to-brand-black/90 px-6 py-5">
{/* Calendar Header */} <div className="flex items-center justify-between">
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-4 sm:p-6"> <button
<div className="flex items-center justify-between mb-3 sm:mb-4"> onClick={prevMonth}
<button className="p-2 hover:bg-white/10 rounded-lg transition-all"
onClick={prevMonth} >
className="p-1.5 sm:p-2 hover:bg-white/20 rounded-lg transition-colors" <ChevronLeft className="w-5 h-5 text-white" />
> </button>
<ChevronLeft className="w-5 h-5 sm:w-6 sm:h-6 text-white" /> <h2 className="text-2xl font-serif font-bold text-white capitalize">
</button> {currentMonthName}
<h2 className="text-lg sm:text-xl md:text-2xl font-bold text-white capitalize"> </h2>
{currentMonthName} <button
</h2> onClick={nextMonth}
<button className="p-2 hover:bg-white/10 rounded-lg transition-all"
onClick={nextMonth} >
className="p-1.5 sm:p-2 hover:bg-white/20 rounded-lg transition-colors" <ChevronRight className="w-5 h-5 text-white" />
> </button>
<ChevronRight className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
</button>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-2 sm:gap-4">
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-2 sm:p-3 text-center">
<p className="text-white/80 text-[10px] sm:text-xs mb-0.5 sm:mb-1">Total</p>
<p className="text-xl sm:text-2xl font-bold text-white">{monthEvents.length}</p>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-2 sm:p-3 text-center">
<p className="text-white/80 text-[10px] sm:text-xs mb-0.5 sm:mb-1">Confirmados</p>
<p className="text-xl sm:text-2xl font-bold text-green-300">
{monthEvents.filter(e => e.status === 'confirmed').length}
</p>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-2 sm:p-3 text-center">
<p className="text-white/80 text-[10px] sm:text-xs mb-0.5 sm:mb-1">Pendentes</p>
<p className="text-xl sm:text-2xl font-bold text-yellow-300">
{monthEvents.filter(e => e.status === 'pending').length}
</p>
</div>
</div>
</div> </div>
</div>
{/* Calendar Grid */} {/* Calendar Grid */}
<div className="p-3 sm:p-4 md:p-6"> <div className="p-4">
{/* Week Days Header */} {/* Week Days */}
<div className="grid grid-cols-7 gap-1 sm:gap-2 mb-1 sm:mb-2"> <div className="grid grid-cols-7 gap-2 mb-2">
{['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'].map((day) => ( {['D', 'S', 'T', 'Q', 'Q', 'S', 'S'].map((day, idx) => (
<div <div key={idx} className="text-center">
key={day} <span className="text-xs font-semibold text-gray-400">
className="text-center text-[10px] sm:text-xs md:text-sm font-bold text-gray-600 py-1 sm:py-2"
>
{day} {day}
</div> </span>
))}
</div>
{/* Calendar Days */}
<div className="grid grid-cols-7 gap-1 sm:gap-2">
{calendarDays.map((day, index) => {
if (!day) {
return <div key={`empty-${index}`} className="aspect-square" />;
}
const dayEvents = getEventsForDate(day);
const today = isToday(day);
return (
<div
key={index}
className={`aspect-square rounded-lg sm:rounded-xl border-2 p-1 sm:p-2 transition-all cursor-pointer hover:shadow-lg ${today
? 'border-[#492E61] bg-[#492E61]/5'
: dayEvents.length > 0
? 'border-[#B9CF32] bg-[#B9CF32]/5 hover:bg-[#B9CF32]/10'
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
}`}
>
<div className="h-full flex flex-col">
<span
className={`text-xs sm:text-sm font-semibold mb-0.5 sm:mb-1 ${today
? 'text-[#492E61]'
: dayEvents.length > 0
? 'text-gray-900'
: 'text-gray-600'
}`}
>
{day.getDate()}
</span>
{dayEvents.length > 0 && (
<div className="flex-1 flex flex-col gap-0.5 sm:gap-1">
{dayEvents.slice(0, 1).map((event) => (
<div
key={event.id}
className={`text-[6px] sm:text-[8px] px-0.5 sm:px-1 py-0.5 rounded ${getTypeColor(event.type)} truncate leading-tight`}
>
{event.title}
</div>
))}
{dayEvents.length > 1 && (
<span className="text-[6px] sm:text-[8px] text-gray-500 font-medium">
+{dayEvents.length - 1}
</span>
)}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Legend */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-bold text-gray-900 mb-3 sm:mb-4">Legenda</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
<div className="flex items-center gap-2">
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-green-500"></div>
<span className="text-xs sm:text-sm text-gray-700">Confirmado</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-yellow-500"></div>
<span className="text-xs sm:text-sm text-gray-700">Pendente</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-gray-400"></div>
<span className="text-xs sm:text-sm text-gray-700">Concluído</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-[#492E61]"></div>
<span className="text-xs sm:text-sm text-gray-700">Formatura</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-pink-500"></div>
<span className="text-xs sm:text-sm text-gray-700">Casamento</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-blue-500"></div>
<span className="text-xs sm:text-sm text-gray-700">Evento</span>
</div>
</div>
</div>
</div>
{/* Events List Sidebar */}
<div className="space-y-4 sm:space-y-6">
{/* Search */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-3 sm:p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Buscar eventos..."
className="w-full pl-9 sm:pl-10 pr-3 sm:pr-4 py-2 sm:py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#492E61] focus:border-transparent text-sm"
/>
</div>
</div>
{/* Upcoming Events */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-bold text-gray-900 mb-3 sm:mb-4 flex items-center gap-2">
<CalendarIcon size={18} className="sm:w-5 sm:h-5 text-[#492E61]" />
Próximos Eventos
</h3>
<div className="space-y-2 sm:space-y-3 max-h-[400px] sm:max-h-[600px] overflow-y-auto">
{MOCK_EVENTS.slice(0, 5).map((event) => (
<div
key={event.id}
className="p-3 sm:p-4 border-l-4 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer"
style={{ borderLeftColor: event.type === 'formatura' ? '#492E61' : event.type === 'casamento' ? '#ec4899' : '#3b82f6' }}
>
<div className="flex items-start justify-between mb-2 gap-2">
<h4 className="font-semibold text-gray-900 text-xs sm:text-sm flex-1">{event.title}</h4>
<span className={`text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-full border whitespace-nowrap ${getStatusColor(event.status)}`}>
{getStatusLabel(event.status)}
</span>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs text-gray-600">
<Clock size={12} className="sm:w-3.5 sm:h-3.5 flex-shrink-0" />
<span>{new Date(event.date).toLocaleDateString('pt-BR')} às {event.time}</span>
</div>
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs text-gray-600">
<MapPin size={12} className="sm:w-3.5 sm:h-3.5 flex-shrink-0" />
<span className="truncate">{event.location}</span>
</div>
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs text-gray-600">
<User size={12} className="sm:w-3.5 sm:h-3.5 flex-shrink-0" />
<span>{event.client}</span>
</div>
</div>
</div> </div>
))} ))}
</div> </div>
{/* Days */}
<div className="grid grid-cols-7 gap-2">
{calendarDays.map((day, index) => {
if (!day) {
return <div key={`empty-${index}`} />;
}
const dayEvents = getEventsForDate(day);
const today = isToday(day);
return (
<div
key={index}
className={`relative w-10 h-10 rounded-lg border-2 flex items-center justify-center cursor-pointer transition-all hover:scale-105 ${
today
? 'border-brand-gold bg-brand-gold text-white shadow-md font-bold'
: dayEvents.length > 0
? 'border-brand-black/20 bg-brand-black text-white hover:border-brand-gold'
: 'border-gray-200 text-gray-700 hover:border-gray-300 hover:bg-gray-50'
}`}
>
<span className={`text-sm ${today ? 'font-bold' : 'font-medium'}`}>
{day.getDate()}
</span>
{dayEvents.length > 0 && !today && (
<div className="absolute bottom-0.5 flex gap-0.5">
{dayEvents.slice(0, 3).map((_, i) => (
<div key={i} className="w-0.5 h-0.5 rounded-full bg-brand-gold" />
))}
</div>
)}
</div>
);
})}
</div>
</div> </div>
{/* Stats Footer */}
<div className="border-t border-gray-200 bg-gray-50 px-6 py-4">
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Total</p>
<p className="text-2xl font-bold text-gray-900">{monthEvents.length}</p>
</div>
<div className="text-center border-x border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Confirmados</p>
<p className="text-2xl font-bold text-green-600">
{monthEvents.filter(e => e.status === 'confirmed').length}
</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Pendentes</p>
<p className="text-2xl font-bold text-yellow-600">
{monthEvents.filter(e => e.status === 'pending').length}
</p>
</div>
</div>
</div>
</div>
{/* Search Bar */}
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Buscar eventos..."
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-sm focus:outline-none focus:border-brand-gold text-sm bg-white"
/>
</div>
</div>
{/* Events List - Table Format */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Evento
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Data
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Horário
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Local
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Cliente
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{monthEvents.map((event) => (
<tr
key={event.id}
className="hover:bg-gray-50 cursor-pointer transition-colors"
>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<div
className={`w-1 h-8 rounded-full ${getTypeColor(event.type)}`}
/>
<span className="font-medium text-gray-900 text-sm">{event.title}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<CalendarIcon size={14} className="mr-1.5 text-brand-gold flex-shrink-0" />
{new Date(event.date + 'T00:00:00').toLocaleDateString('pt-BR')}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<Clock size={14} className="mr-1.5 text-gray-400 flex-shrink-0" />
{event.time}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<MapPin size={14} className="mr-1.5 text-brand-gold flex-shrink-0" />
<span className="truncate max-w-[200px]">{event.location}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center text-sm text-gray-700">
<User size={14} className="mr-1.5 text-gray-400 flex-shrink-0" />
{event.client}
</div>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-semibold border ${getStatusColor(event.status)}`}>
{getStatusLabel(event.status)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{monthEvents.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>Nenhum evento encontrado neste mês.</p>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useMemo } from "react";
import { UserRole, EventData, EventStatus, EventType } from "../types"; import { UserRole, EventData, EventStatus, EventType } from "../types";
import { EventCard } from "../components/EventCard"; import { EventTable } from "../components/EventTable";
import { EventFiltersBar, EventFilters } from "../components/EventFiltersBar";
import { EventForm } from "../components/EventForm"; import { EventForm } from "../components/EventForm";
import { Button } from "../components/Button"; import { Button } from "../components/Button";
import { import {
@ -12,6 +13,8 @@ import {
Users, Users,
Map, Map,
Building2, Building2,
Calendar,
MapPin,
} from "lucide-react"; } from "lucide-react";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useData } from "../contexts/DataContext"; import { useData } from "../contexts/DataContext";
@ -39,6 +42,13 @@ export const Dashboard: React.FC<DashboardProps> = ({
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null); const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
const [activeFilter, setActiveFilter] = useState<string>("all"); const [activeFilter, setActiveFilter] = useState<string>("all");
const [advancedFilters, setAdvancedFilters] = useState<EventFilters>({
date: '',
city: '',
state: '',
timeRange: '',
type: '',
});
// Reset view when initialView prop changes // Reset view when initialView prop changes
useEffect(() => { useEffect(() => {
@ -54,6 +64,31 @@ export const Dashboard: React.FC<DashboardProps> = ({
const myEvents = getEventsByRole(user.id, user.role); const myEvents = getEventsByRole(user.id, user.role);
// Extract unique values for filters
const { availableStates, availableCities, availableTypes } = useMemo(() => {
const states = [...new Set(myEvents.map(e => e.address.state))].sort();
const cities = advancedFilters.state
? [...new Set(myEvents
.filter(e => e.address.state === advancedFilters.state)
.map(e => e.address.city))].sort()
: [];
const types = [...new Set(myEvents.map(e => e.type))].sort();
return { availableStates: states, availableCities: cities, availableTypes: types };
}, [myEvents, advancedFilters.state]);
// Helper function to check time range
const isInTimeRange = (time: string, range: string): boolean => {
if (!range) return true;
const [hours] = time.split(':').map(Number);
switch (range) {
case 'morning': return hours >= 6 && hours < 12;
case 'afternoon': return hours >= 12 && hours < 18;
case 'evening': return hours >= 18 || hours < 6;
default: return true;
}
};
// Filter Logic // Filter Logic
const filteredEvents = myEvents.filter((e) => { const filteredEvents = myEvents.filter((e) => {
const matchesSearch = e.name const matchesSearch = e.name
@ -66,7 +101,16 @@ export const Dashboard: React.FC<DashboardProps> = ({
(activeFilter === "active" && (activeFilter === "active" &&
e.status !== EventStatus.ARCHIVED && e.status !== EventStatus.ARCHIVED &&
e.status !== EventStatus.PENDING_APPROVAL); e.status !== EventStatus.PENDING_APPROVAL);
return matchesSearch && matchesStatus;
// Advanced filters
const matchesDate = !advancedFilters.date || e.date === advancedFilters.date;
const matchesState = !advancedFilters.state || e.address.state === advancedFilters.state;
const matchesCity = !advancedFilters.city || e.address.city === advancedFilters.city;
const matchesTimeRange = isInTimeRange(e.time, advancedFilters.timeRange);
const matchesType = !advancedFilters.type || e.type === advancedFilters.type;
return matchesSearch && matchesStatus && matchesDate && matchesState &&
matchesCity && matchesTimeRange && matchesType;
}); });
const handleSaveEvent = (data: any) => { const handleSaveEvent = (data: any) => {
@ -183,27 +227,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
); );
}; };
const renderAdminActions = (event: EventData) => {
if (
user.role !== UserRole.BUSINESS_OWNER &&
user.role !== UserRole.SUPERADMIN
)
return null;
if (event.status === EventStatus.PENDING_APPROVAL) {
return (
<div className="absolute top-3 left-3 flex space-x-2 z-10">
<button
onClick={(e) => handleApprove(e, event.id)}
className="bg-green-500 text-white px-3 py-1 rounded-sm text-xs font-bold shadow hover:bg-green-600 transition-colors flex items-center"
>
<CheckCircle size={12} className="mr-1" /> APROVAR
</button>
</div>
);
}
return null;
};
// --- MAIN RENDER --- // --- MAIN RENDER ---
@ -221,7 +245,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
{/* Content Switcher */} {/* Content Switcher */}
{view === "list" && ( {view === "list" && (
<div className="space-y-6 fade-in"> <div className="space-y-6 fade-in">
{/* Filters Bar */} {/* Search Bar */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-100"> <div className="flex flex-col sm:flex-row gap-4 items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-100">
<div className="relative flex-1 w-full"> <div className="relative flex-1 w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
@ -259,29 +283,32 @@ export const Dashboard: React.FC<DashboardProps> = ({
)} )}
</div> </div>
{/* Grid */} {/* Advanced Filters */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6 md:gap-8"> <EventFiltersBar
{filteredEvents.map((event) => ( filters={advancedFilters}
<div key={event.id} className="relative group"> onFilterChange={setAdvancedFilters}
{renderAdminActions(event)} availableCities={availableCities}
<EventCard availableStates={availableStates}
event={event} availableTypes={availableTypes}
onClick={() => { />
setSelectedEvent(event);
setView("details"); {/* Results Count */}
}} <div className="flex items-center justify-between text-sm text-gray-600">
/> <span>
</div> Exibindo <strong className="text-brand-gold">{filteredEvents.length}</strong> de <strong>{myEvents.length}</strong> eventos
))} </span>
</div> </div>
{filteredEvents.length === 0 && ( {/* Event Table */}
<div className="text-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200"> <EventTable
<p className="text-gray-500 mb-4"> events={filteredEvents}
Nenhum evento encontrado com os filtros atuais. onEventClick={(event) => {
</p> setSelectedEvent(event);
</div> setView("details");
)} }}
onApprove={handleApprove}
userRole={user.role}
/>
</div> </div>
)} )}
@ -319,51 +346,85 @@ export const Dashboard: React.FC<DashboardProps> = ({
)} )}
<div className="bg-white border rounded-lg overflow-hidden shadow-sm"> <div className="bg-white border rounded-lg overflow-hidden shadow-sm">
<div className="h-64 w-full relative"> {/* Header Section - Sem foto */}
<img <div className="bg-gradient-to-r from-brand-gold/5 to-brand-black/5 border-b-2 border-brand-gold p-6">
src={selectedEvent.coverImage} <div className="flex items-start justify-between">
className="w-full h-full object-cover" <div>
alt="Cover" <h1 className="text-3xl font-serif font-bold text-brand-black mb-2">
/> {selectedEvent.name}
<div className="absolute inset-0 bg-black/40 flex items-center justify-center"> </h1>
<h1 className="text-4xl font-serif text-white font-bold text-center px-4 drop-shadow-lg"> <div className="flex flex-wrap gap-3 text-sm text-gray-600">
{selectedEvent.name} <span className="flex items-center gap-1">
</h1> <Calendar size={16} className="text-brand-gold" />
{new Date(selectedEvent.date + 'T00:00:00').toLocaleDateString('pt-BR')} às {selectedEvent.time}
</span>
<span className="flex items-center gap-1">
<MapPin size={16} className="text-brand-gold" />
{selectedEvent.address.city}, {selectedEvent.address.state}
</span>
</div>
</div>
<div className={`px-4 py-2 rounded text-sm font-semibold ${STATUS_COLORS[selectedEvent.status]}`}>
{selectedEvent.status}
</div>
</div> </div>
</div> </div>
<div className="p-4 sm:p-6 md:p-8"> <div className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8"> {/* Actions Toolbar */}
<div className="lg:col-span-2 space-y-6 md:space-y-8"> <div className="flex flex-wrap gap-2 mb-6 pb-4 border-b">
{/* Actions Toolbar */} {(user.role === UserRole.BUSINESS_OWNER ||
<div className="flex flex-wrap gap-2 sm:gap-3 border-b pb-4"> user.role === UserRole.SUPERADMIN) && (
{(user.role === UserRole.BUSINESS_OWNER || <>
user.role === UserRole.SUPERADMIN) && ( <Button
<> variant="outline"
<Button onClick={() => setView("edit")}
variant="outline" className="text-sm"
onClick={() => setView("edit")} >
className="text-sm" <Edit size={16} className="mr-2" /> Editar Detalhes
> </Button>
<Edit size={16} className="mr-2" /> Editar Detalhes <Button variant="outline" onClick={handleManageTeam} className="text-sm">
</Button> <Users size={16} className="mr-2" /> Gerenciar
<Button variant="outline" onClick={handleManageTeam} className="text-sm"> Equipe
<Users size={16} className="mr-2" /> Gerenciar </Button>
Equipe </>
</Button> )}
</> {user.role === UserRole.EVENT_OWNER &&
)} selectedEvent.status !== EventStatus.ARCHIVED && (
{user.role === UserRole.EVENT_OWNER && <Button
selectedEvent.status !== EventStatus.ARCHIVED && ( variant="outline"
<Button onClick={() => setView("edit")}
variant="outline" className="text-sm"
onClick={() => setView("edit")} >
className="text-sm" <Edit size={16} className="mr-2" /> Editar
> Informações
<Edit size={16} className="mr-2" /> Editar </Button>
Informações )}
</Button> <Button
)} variant="outline"
onClick={handleOpenMaps}
className="text-sm"
>
<Map size={16} className="mr-2" /> Abrir no Maps
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* Quick Info Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Tipo</p>
<p className="font-semibold text-gray-900">{selectedEvent.type}</p>
</div>
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Data</p>
<p className="font-semibold text-gray-900">{new Date(selectedEvent.date + 'T00:00:00').toLocaleDateString('pt-BR')}</p>
</div>
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Horário</p>
<p className="font-semibold text-gray-900">{selectedEvent.time}</p>
</div>
</div> </div>
{/* Institution Information */} {/* Institution Information */}
@ -467,89 +528,64 @@ export const Dashboard: React.FC<DashboardProps> = ({
)} )}
</div> </div>
<div className="lg:col-span-1 space-y-4 sm:space-y-6"> <div className="lg:col-span-1 space-y-4">
<div {/* Localização Card */}
className={`p-6 rounded-sm border ${STATUS_COLORS[selectedEvent.status] <div className="border p-5 rounded bg-gray-50">
} bg-opacity-10`} <h4 className="font-bold text-sm mb-3 text-gray-700 flex items-center gap-2">
> <MapPin size={16} className="text-brand-gold" />
<h4 className="font-bold uppercase tracking-widest text-xs mb-2 opacity-70">
Status Atual
</h4>
<p className="text-xl font-serif font-bold">
{selectedEvent.status}
</p>
</div>
<div className="border p-6 rounded-sm bg-gray-50">
<h4 className="font-bold uppercase tracking-widest text-xs mb-4 text-gray-400">
Localização Localização
</h4> </h4>
<p className="font-medium text-lg"> <p className="font-medium text-base mb-1">
{selectedEvent.address.street},{" "} {selectedEvent.address.street}, {selectedEvent.address.number}
{selectedEvent.address.number}
</p> </p>
<p className="text-gray-500 mb-4"> <p className="text-gray-600 text-sm">
{selectedEvent.address.city} -{" "} {selectedEvent.address.city} - {selectedEvent.address.state}
{selectedEvent.address.state}
</p> </p>
{selectedEvent.address.zip && (
{selectedEvent.address.mapLink ? ( <p className="text-gray-500 text-xs mt-1">CEP: {selectedEvent.address.zip}</p>
<Button
variant="secondary"
size="sm"
className="w-full"
onClick={handleOpenMaps}
>
<Map size={16} className="mr-2" /> Abrir no Google
Maps
</Button>
) : (
<Button
variant="outline"
size="sm"
className="w-full bg-white"
onClick={handleOpenMaps}
>
<Map size={16} className="mr-2" /> Buscar no Maps
</Button>
)} )}
</div> </div>
{/* Equipe Designada */}
{(selectedEvent.photographerIds.length > 0 || {(selectedEvent.photographerIds.length > 0 ||
user.role === UserRole.BUSINESS_OWNER) && ( user.role === UserRole.BUSINESS_OWNER ||
<div className="border p-6 rounded-sm"> user.role === UserRole.SUPERADMIN) && (
<div className="flex justify-between items-center mb-4"> <div className="border p-5 rounded bg-white">
<h4 className="font-bold uppercase tracking-widest text-xs text-gray-400"> <div className="flex justify-between items-center mb-3">
Equipe Designada <h4 className="font-bold text-sm text-gray-700 flex items-center gap-2">
<Users size={16} className="text-brand-gold" />
Equipe ({selectedEvent.photographerIds.length})
</h4> </h4>
{(user.role === UserRole.BUSINESS_OWNER || {(user.role === UserRole.BUSINESS_OWNER ||
user.role === UserRole.SUPERADMIN) && ( user.role === UserRole.SUPERADMIN) && (
<button <button
onClick={handleManageTeam} onClick={handleManageTeam}
className="text-brand-gold hover:text-brand-black" className="text-brand-gold hover:text-brand-black transition-colors"
title="Adicionar fotógrafo"
> >
<PlusCircle size={16} /> <PlusCircle size={18} />
</button> </button>
)} )}
</div> </div>
{selectedEvent.photographerIds.length > 0 ? ( {selectedEvent.photographerIds.length > 0 ? (
<div className="flex -space-x-2"> <div className="space-y-2">
{selectedEvent.photographerIds.map((id, idx) => ( {selectedEvent.photographerIds.map((id, idx) => (
<div <div key={id} className="flex items-center gap-2 text-sm">
key={id} <div
className="w-10 h-10 rounded-full border-2 border-white bg-gray-300" className="w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
style={{ style={{
backgroundImage: `url(https://i.pravatar.cc/100?u=${id})`, backgroundImage: `url(https://i.pravatar.cc/100?u=${id})`,
backgroundSize: "cover", backgroundSize: "cover",
}} }}
title={id} ></div>
></div> <span className="text-gray-700">{id}</span>
</div>
))} ))}
</div> </div>
) : ( ) : (
<p className="text-sm text-gray-400 italic"> <p className="text-sm text-gray-400 italic">
Nenhum profissional atribuído. Nenhum profissional atribuído
</p> </p>
)} )}
</div> </div>

View file

@ -42,35 +42,19 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
} }
return ( return (
<div className="min-h-screen flex flex-col lg:flex-row bg-white"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4">
{/* Left Side - Image */} <div className="w-full max-w-md space-y-8 fade-in">
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden"> {/* Logo */}
<img <div className="flex justify-center mb-8">
src="https://plus.unsplash.com/premium_photo-1713296255442-e9338f42aad8?q=80&w=722&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" <img
alt="Photum Login" src="/logo.png"
className="absolute inset-0 w-full h-full object-cover" alt="Photum Formaturas"
/> className="h-20 w-auto object-contain"
<div className="absolute inset-0 bg-gradient-to-br from-[#492E61]/90 to-[#492E61]/70 flex items-center justify-center"> />
<div className="text-center text-white p-12">
<h1 className="text-5xl font-serif font-bold mb-4">Photum Formaturas</h1>
<p className="text-xl font-light tracking-wide max-w-md mx-auto">Gestão de eventos premium para quem não abre mão da excelência.</p>
</div>
</div> </div>
</div>
{/* Right Side - Form */} <div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<div className="w-full lg:w-1/2 flex items-center justify-center p-4 sm:p-6 md:p-8 lg:p-16"> <div className="text-center">
<div className="max-w-md w-full space-y-6 sm:space-y-8 fade-in">
{/* Logo Mobile */}
<div className="lg:hidden flex justify-center mb-6">
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-16 w-auto object-contain"
/>
</div>
<div className="text-center lg:text-left">
<span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Bem-vindo de volta</span> <span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Bem-vindo de volta</span>
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Acesse sua conta</h2> <h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Acesse sua conta</h2>
<p className="mt-2 text-xs sm:text-sm text-gray-600"> <p className="mt-2 text-xs sm:text-sm text-gray-600">
@ -135,39 +119,39 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
{isLoading ? 'Entrando...' : 'Entrar no Sistema'} {isLoading ? 'Entrando...' : 'Entrar no Sistema'}
</button> </button>
</form> </form>
</div>
{/* Demo Users Quick Select - Melhorado para Mobile */} {/* Demo Users Quick Select */}
<div className="mt-6 sm:mt-10 pt-6 sm:pt-10 border-t border-gray-200"> <div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6">
<p className="text-[10px] sm:text-xs uppercase tracking-widest mb-3 sm:mb-4 text-center text-gray-400">Usuários de Demonstração (Clique para preencher)</p> <p className="text-[10px] sm:text-xs uppercase tracking-widest mb-3 sm:mb-4 text-center text-gray-400">Usuários de Demonstração (Clique para preencher)</p>
<div className="space-y-2"> <div className="space-y-2">
{availableUsers.map(user => ( {availableUsers.map(user => (
<button <button
key={user.id} key={user.id}
onClick={() => fillCredentials(user.email)} onClick={() => fillCredentials(user.email)}
className="w-full flex items-center justify-between p-3 sm:p-4 border-2 rounded-xl hover:bg-gray-50 transition-all duration-300 text-left group transform hover:scale-[1.01] active:scale-[0.99]" className="w-full flex items-center justify-between p-3 sm:p-4 border-2 rounded-xl hover:bg-gray-50 transition-all duration-300 text-left group transform hover:scale-[1.01] active:scale-[0.99]"
style={{ borderColor: '#e5e7eb' }} style={{ borderColor: '#e5e7eb' }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#B9CF33'; e.currentTarget.style.borderColor = '#B9CF33';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(185, 207, 51, 0.15)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(185, 207, 51, 0.15)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb'; e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = 'none';
}} }}
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-sm sm:text-base font-bold text-gray-900 truncate">{user.name}</span> <span className="text-sm sm:text-base font-bold text-gray-900 truncate">{user.name}</span>
<span className="text-[10px] sm:text-xs uppercase tracking-wide font-semibold px-2 py-0.5 rounded-full whitespace-nowrap" style={{ backgroundColor: '#B9CF33', color: '#fff' }}>{getRoleLabel(user.role)}</span> <span className="text-[10px] sm:text-xs uppercase tracking-wide font-semibold px-2 py-0.5 rounded-full whitespace-nowrap" style={{ backgroundColor: '#B9CF33', color: '#fff' }}>{getRoleLabel(user.role)}</span>
</div>
<span className="text-xs sm:text-sm text-gray-500 truncate block">{user.email}</span>
</div> </div>
<svg className="w-5 h-5 text-gray-400 group-hover:text-[#B9CF33] transition-colors flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <span className="text-xs sm:text-sm text-gray-500 truncate block">{user.email}</span>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> </div>
</svg> <svg className="w-5 h-5 text-gray-400 group-hover:text-[#B9CF33] transition-colors flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</button> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
))} </svg>
</div> </button>
))}
</div> </div>
</div> </div>
</div> </div>

View file

@ -128,40 +128,22 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
} }
return ( return (
<div className="min-h-screen flex flex-col lg:flex-row bg-white"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 py-12">
{/* Left Side - Image */} <div className="w-full max-w-md space-y-8 fade-in">
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden"> {/* Logo */}
<img <div className="flex justify-center mb-8">
src="https://images.unsplash.com/photo-1541339907198-e08756dedf3f?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" <img
alt="Photum Cadastro" src="/logo.png"
className="absolute inset-0 w-full h-full object-cover" alt="Photum Formaturas"
/> className="h-20 w-auto object-contain"
<div className="absolute inset-0 bg-gradient-to-br from-[#492E61]/90 to-[#492E61]/70 flex items-center justify-center"> />
<div className="text-center text-white p-12">
<h1 className="text-5xl font-serif font-bold mb-4">Faça parte da Photum</h1>
<p className="text-xl font-light tracking-wide max-w-md mx-auto">
Eternize seus momentos especiais com a melhor plataforma de gestão de eventos fotográficos.
</p>
</div>
</div> </div>
</div>
{/* Right Side - Form */} <div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<div className="w-full lg:w-1/2 flex items-center justify-center p-4 sm:p-6 md:p-8 lg:p-16"> <div className="text-center">
<div className="max-w-md w-full space-y-6 sm:space-y-8 fade-in"> <span className="font-bold tracking-widest uppercase text-sm" style={{ color: '#B9CF33' }}>Comece agora</span>
{/* Logo Mobile */} <h2 className="mt-3 text-3xl font-serif font-bold text-gray-900">Crie sua conta</h2>
<div className="lg:hidden flex justify-center mb-6"> <p className="mt-3 text-sm text-gray-600">
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-16 w-auto object-contain"
/>
</div>
<div className="text-center lg:text-left">
<span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Comece agora</span>
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Crie sua conta</h2>
<p className="mt-2 text-xs sm:text-sm text-gray-600">
tem uma conta?{' '} tem uma conta?{' '}
<button <button
onClick={() => onNavigate('login')} onClick={() => onNavigate('login')}
@ -173,8 +155,8 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
</p> </p>
</div> </div>
<form className="mt-6 sm:mt-8 space-y-4 sm:space-y-5" onSubmit={handleSubmit}> <form className="mt-8 space-y-5" onSubmit={handleSubmit}>
<div className="space-y-3 sm:space-y-4"> <div className="space-y-4">
<Input <Input
label="Nome Completo" label="Nome Completo"
type="text" type="text"