photum/pages/Dashboard.tsx
2025-12-02 22:38:12 -03:00

527 lines
30 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { UserRole, EventData, EventStatus, EventType } from '../types';
import { EventCard } from '../components/EventCard';
import { EventForm } from '../components/EventForm';
import { Button } from '../components/Button';
import { PlusCircle, Search, CheckCircle, Clock, Edit, Users, Map, Building2, Phone, Mail, MapPin, FileText, Camera, Star } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useData } from '../contexts/DataContext';
import { STATUS_COLORS } from '../constants';
interface DashboardProps {
initialView?: 'list' | 'create';
}
export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) => {
const { user } = useAuth();
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, addAttachment, getPhotographerById, getInstitutionById } = useData();
const [view, setView] = useState<'list' | 'create' | 'edit' | 'details'>(initialView);
const [searchTerm, setSearchTerm] = useState('');
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
const [activeFilter, setActiveFilter] = useState<string>('all');
// Reset view when initialView prop changes
useEffect(() => {
if (initialView) {
setView(initialView);
if (initialView === 'create') setSelectedEvent(null);
}
}, [initialView]);
// Guard Clause for basic security
if (!user) return <div className="p-10 text-center">Acesso Negado. Faça login.</div>;
const myEvents = getEventsByRole(user.id, user.role);
// Filter Logic
const filteredEvents = myEvents.filter(e => {
const matchesSearch = e.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = activeFilter === 'all' ||
(activeFilter === 'pending' && e.status === EventStatus.PENDING_APPROVAL) ||
(activeFilter === 'active' && e.status !== EventStatus.ARCHIVED && e.status !== EventStatus.PENDING_APPROVAL);
return matchesSearch && matchesStatus;
});
const handleSaveEvent = (data: any) => {
const isClient = user.role === UserRole.EVENT_OWNER;
if (view === 'edit' && selectedEvent) {
const updatedEvent = { ...selectedEvent, ...data };
console.log("Updated", updatedEvent);
setSelectedEvent(updatedEvent);
setView('details');
} else {
const initialStatus = isClient ? EventStatus.PENDING_APPROVAL : EventStatus.PLANNING;
const newEvent: EventData = {
...data,
id: Math.random().toString(36).substr(2, 9),
status: initialStatus,
checklist: [],
ownerId: isClient ? user.id : 'unknown',
photographerIds: []
};
addEvent(newEvent);
setView('list');
}
};
const handleApprove = (e: React.MouseEvent, eventId: string) => {
e.stopPropagation();
updateEventStatus(eventId, EventStatus.CONFIRMED);
};
const handleOpenMaps = () => {
if (!selectedEvent) return;
if (selectedEvent.address.mapLink) {
window.open(selectedEvent.address.mapLink, '_blank');
return;
}
const { street, number, city, state } = selectedEvent.address;
const query = encodeURIComponent(`${street}, ${number}, ${city} - ${state}`);
window.open(`https://www.google.com/maps/search/?api=1&query=${query}`, '_blank');
};
const handleManageTeam = () => {
if (!selectedEvent) return;
const newId = window.prompt("ID do Fotógrafo para adicionar (ex: photographer-1):", "photographer-1");
if (newId) {
assignPhotographer(selectedEvent.id, newId);
alert("Fotógrafo atribuído com sucesso!");
const updated = events.find(e => e.id === selectedEvent.id);
if (updated) setSelectedEvent(updated);
}
};
// --- RENDERS PER ROLE ---
const renderRoleSpecificHeader = () => {
if (user.role === UserRole.EVENT_OWNER) {
return (
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black">Meus Eventos</h1>
<p className="text-gray-500 mt-1">Acompanhe seus eventos ou solicite novos orçamentos.</p>
</div>
);
}
if (user.role === UserRole.PHOTOGRAPHER) {
return (
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black">Eventos Designados</h1>
<p className="text-gray-500 mt-1">Gerencie seus trabalhos e visualize detalhes.</p>
</div>
);
}
return (
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black">Gestão Geral</h1>
<p className="text-gray-500 mt-1">Controle total de eventos, aprovações e equipes.</p>
</div>
);
};
const renderRoleSpecificActions = () => {
if (user.role === UserRole.PHOTOGRAPHER) return null;
const label = user.role === UserRole.EVENT_OWNER ? "Solicitar Novo Evento" : "Novo Evento";
return (
<Button onClick={() => setView('create')} className="shadow-lg">
<PlusCircle className="mr-2 h-5 w-5" /> {label}
</Button>
);
};
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 ---
return (
<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">
{/* Header */}
{view === 'list' && (
<div className="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4 fade-in">
{renderRoleSpecificHeader()}
{renderRoleSpecificActions()}
</div>
)}
{/* Content Switcher */}
{view === 'list' && (
<div className="space-y-6 fade-in">
{/* Filters 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="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" />
<input
type="text"
placeholder="Buscar evento..."
className="w-full pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-sm focus:outline-none focus:border-brand-gold text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && (
<div className="flex space-x-2 bg-white p-1 rounded border border-gray-200">
<button
onClick={() => setActiveFilter('all')}
className={`px-3 py-1 text-xs font-medium rounded-sm ${activeFilter === 'all' ? 'bg-brand-black text-white' : 'text-gray-600 hover:bg-gray-100'}`}
>
Todos
</button>
<button
onClick={() => setActiveFilter('pending')}
className={`px-3 py-1 text-xs font-medium rounded-sm flex items-center ${activeFilter === 'pending' ? 'bg-brand-gold text-white' : 'text-gray-600 hover:bg-gray-100'}`}
>
<Clock size={12} className="mr-1"/> Pendentes
</button>
</div>
)}
</div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredEvents.map(event => (
<div key={event.id} className="relative group">
{renderAdminActions(event)}
<EventCard
event={event}
onClick={() => { setSelectedEvent(event); setView('details'); }}
/>
</div>
))}
</div>
{filteredEvents.length === 0 && (
<div className="text-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200">
<p className="text-gray-500 mb-4">Nenhum evento encontrado com os filtros atuais.</p>
</div>
)}
</div>
)}
{(view === 'create' || view === 'edit') && (
<EventForm
onCancel={() => setView(view === 'edit' ? 'details' : 'list')}
onSubmit={handleSaveEvent}
initialData={view === 'edit' ? selectedEvent : undefined}
/>
)}
{view === 'details' && selectedEvent && (
<div className="fade-in">
<Button variant="ghost" onClick={() => setView('list')} className="mb-4 pl-0">
Voltar para lista
</Button>
{/* Status Banner */}
{selectedEvent.status === EventStatus.PENDING_APPROVAL && user.role === UserRole.EVENT_OWNER && (
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 rounded-lg mb-6 flex items-start">
<Clock className="mr-3 flex-shrink-0" />
<div>
<h4 className="font-bold">Solicitação em Análise</h4>
<p className="text-sm mt-1">Seu evento foi enviado e está aguardando aprovação da equipe Photum.</p>
</div>
</div>
)}
<div className="bg-white rounded-2xl overflow-hidden shadow-2xl animate-slideUp">
<div className="h-80 w-full relative group overflow-hidden">
<img src={selectedEvent.coverImage} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" alt="Cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent flex items-center justify-center">
<div className="text-center px-6">
<h1 className="text-5xl font-serif text-white font-bold drop-shadow-2xl mb-3 animate-fadeIn">{selectedEvent.name}</h1>
<div className="flex items-center justify-center gap-3 animate-fadeIn" style={{ animationDelay: '0.2s' }}>
<span className="px-4 py-2 bg-white/90 backdrop-blur-sm rounded-full text-sm font-semibold text-gray-800 shadow-lg">
{selectedEvent.type}
</span>
<span className="px-4 py-2 bg-[#B9CF33] backdrop-blur-sm rounded-full text-sm font-semibold text-white shadow-lg">
{new Date(selectedEvent.date).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' })}
</span>
</div>
</div>
</div>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="col-span-2 space-y-8">
{/* Actions Toolbar */}
<div className="flex flex-wrap gap-3 border-b pb-4">
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && (
<>
<Button variant="outline" onClick={() => setView('edit')}>
<Edit size={16} className="mr-2"/> Editar Detalhes
</Button>
<Button variant="outline" onClick={handleManageTeam}>
<Users size={16} className="mr-2"/> Gerenciar Equipe
</Button>
</>
)}
{user.role === UserRole.EVENT_OWNER && selectedEvent.status !== EventStatus.ARCHIVED && (
<Button variant="outline" onClick={() => setView('edit')}>
<Edit size={16} className="mr-2"/> Editar Informações
</Button>
)}
</div>
{/* Institution Information */}
{selectedEvent.institutionId && (() => {
const institution = getInstitutionById(selectedEvent.institutionId);
if (institution) {
return (
<section className="relative bg-gradient-to-br from-[#B9CF33]/5 via-white to-[#C2388B]/5 border-2 border-gray-100 rounded-2xl p-8 hover:shadow-lg transition-all duration-300 group overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-[#B9CF33]/0 to-[#C2388B]/0 group-hover:from-[#B9CF33]/5 group-hover:to-[#C2388B]/5 transition-all duration-500" />
<div className="relative flex items-start gap-5">
<div className="flex-shrink-0">
<div className="w-20 h-20 bg-gradient-to-br from-[#B9CF33] to-[#a5bd2e] rounded-2xl flex items-center justify-center shadow-xl group-hover:scale-110 group-hover:rotate-6 transition-transform duration-300">
<Building2 className="text-white" size={36} />
</div>
</div>
<div className="flex-1">
<h3 className="text-2xl font-bold text-brand-black mb-2 group-hover:text-[#492E61] transition-colors">{institution.name}</h3>
<span className="inline-block px-4 py-1.5 bg-gradient-to-r from-[#B9CF33] to-[#a5bd2e] text-white text-xs uppercase tracking-wide font-bold rounded-full mb-5 shadow-md">
{institution.type}
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center flex-shrink-0">
<Phone className="text-[#B9CF33]" size={20} />
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-semibold mb-1">Contato</p>
<p className="text-gray-800 font-semibold">{institution.phone}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center flex-shrink-0">
<Mail className="text-[#B9CF33]" size={20} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-500 uppercase tracking-wide font-semibold mb-1">Email</p>
<p className="text-gray-800 font-medium truncate">{institution.email}</p>
</div>
</div>
</div>
{institution.address && (
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center flex-shrink-0">
<MapPin className="text-[#B9CF33]" size={20} />
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-semibold mb-1">Endereço</p>
<p className="text-gray-800 font-medium">{institution.address.street}, {institution.address.number}</p>
<p className="text-gray-600">{institution.address.city} - {institution.address.state}</p>
</div>
</div>
)}
</div>
{institution.description && (
<div className="mt-5 pt-5 border-t border-gray-200">
<p className="text-gray-600 text-sm leading-relaxed italic">
{institution.description}
</p>
</div>
)}
</div>
</div>
</section>
);
}
return null;
})()}
<section className="bg-gradient-to-br from-gray-50 to-white rounded-2xl p-6 border border-gray-100 hover:shadow-lg transition-all duration-300">
<div className="flex items-center gap-3 mb-5">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center">
<FileText className="text-[#B9CF33]" size={20} />
</div>
<h3 className="text-xl font-bold text-brand-black">Sobre o Evento</h3>
</div>
<p className="text-gray-700 leading-relaxed whitespace-pre-wrap text-base">{selectedEvent.briefing || "Sem briefing detalhado."}</p>
</section>
{selectedEvent.contacts.length > 0 && (
<section>
<div className="flex items-center gap-3 mb-5">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center">
<Users className="text-[#B9CF33]" size={20} />
</div>
<h3 className="text-xl font-bold text-brand-black">Contatos & Responsáveis</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{selectedEvent.contacts.map((c, i) => (
<div key={i} className="group bg-gradient-to-br from-white to-gray-50 p-5 rounded-xl border-2 border-gray-100 hover:border-[#B9CF33] hover:shadow-lg transition-all duration-300">
<div className="flex items-start gap-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-[#492E61] to-[#C2388B] flex items-center justify-center text-white font-bold text-lg flex-shrink-0 group-hover:scale-110 transition-transform">
{c.name.charAt(0).toUpperCase()}
</div>
<div className="flex-1">
<p className="font-bold text-base text-gray-800 mb-1">{c.name}</p>
<span className="inline-block px-3 py-1 bg-[#B9CF33]/10 text-[#B9CF33] text-xs uppercase tracking-wide font-bold rounded-full mb-2">{c.role}</span>
<div className="flex items-center gap-2 text-gray-600 mt-2">
<Phone size={14} className="text-[#B9CF33]" />
<p className="text-sm font-medium">{c.phone}</p>
</div>
</div>
</div>
</div>
))}
</div>
</section>
)}
</div>
<div className="col-span-1 space-y-6">
<div className="relative overflow-hidden rounded-2xl p-6 bg-gradient-to-br from-white to-gray-50 border-2 border-gray-100 hover:shadow-xl transition-all duration-300 group">
<div className="absolute inset-0 bg-gradient-to-r from-[#B9CF33]/0 to-[#B9CF33]/0 group-hover:from-[#B9CF33]/5 group-hover:to-[#B9CF33]/5 transition-all" />
<div className="relative">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center group-hover:scale-110 transition-transform">
<CheckCircle className="text-[#B9CF33]" size={20} />
</div>
<h4 className="font-bold uppercase tracking-widest text-xs text-gray-500">Status Atual</h4>
</div>
<span className={`inline-block px-4 py-2 rounded-xl text-sm font-bold ${STATUS_COLORS[selectedEvent.status]} shadow-sm`}>
{selectedEvent.status}
</span>
</div>
</div>
<div className="rounded-2xl bg-gradient-to-br from-white to-gray-50 p-6 border-2 border-gray-100 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center gap-3 mb-5">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center group-hover:scale-110 transition-transform">
<MapPin className="text-[#B9CF33]" size={20} />
</div>
<h4 className="font-bold uppercase tracking-widest text-xs text-gray-500">Localização</h4>
</div>
<p className="font-bold text-base text-gray-800 mb-1">{selectedEvent.address.street}, {selectedEvent.address.number}</p>
<p className="text-gray-600 mb-5 text-sm">{selectedEvent.address.city} - {selectedEvent.address.state}</p>
{selectedEvent.address.mapLink ? (
<button
onClick={handleOpenMaps}
className="w-full px-4 py-3 bg-gradient-to-r from-[#B9CF33] to-[#a5bd2e] text-white rounded-xl font-semibold hover:shadow-lg hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2"
>
<Map size={18} /> Abrir no Google Maps
</button>
) : (
<button
onClick={handleOpenMaps}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 text-gray-700 rounded-xl font-semibold hover:border-[#B9CF33] hover:text-[#B9CF33] hover:shadow-lg transition-all flex items-center justify-center gap-2"
>
<Map size={18} /> Buscar no Maps
</button>
)}
</div>
{(selectedEvent.photographerIds.length > 0 || user.role === UserRole.BUSINESS_OWNER) && (
<div className="rounded-2xl bg-gradient-to-br from-white to-gray-50 p-6 border-2 border-gray-100 hover:shadow-xl transition-all duration-300 group">
<div className="flex justify-between items-center mb-5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-[#B9CF33]/10 flex items-center justify-center group-hover:scale-110 transition-transform">
<Camera className="text-[#B9CF33]" size={20} />
</div>
<h4 className="font-bold uppercase tracking-widest text-xs text-gray-500">Equipe Designada</h4>
</div>
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && (
<button
onClick={handleManageTeam}
className="w-8 h-8 rounded-lg bg-[#B9CF33]/10 hover:bg-[#B9CF33] text-[#B9CF33] hover:text-white transition-all flex items-center justify-center hover:scale-110"
>
<PlusCircle size={18}/>
</button>
)}
</div>
{selectedEvent.photographerIds.length > 0 ? (
<div className="space-y-3">
{selectedEvent.photographerIds.map((id) => {
const photographer = getPhotographerById(id);
return (
<div
key={id}
className="flex items-center gap-3 p-3 bg-white rounded-xl border border-gray-200 hover:border-[#B9CF33] hover:shadow-md transition-all group/item"
>
<div className="relative flex-shrink-0">
<img
src={photographer?.avatar || `https://i.pravatar.cc/100?u=${id}`}
alt={photographer?.name || id}
className="w-12 h-12 rounded-xl object-cover ring-2 ring-gray-100 group-hover/item:ring-[#B9CF33] transition-all"
/>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white" />
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-gray-800 text-sm truncate group-hover/item:text-[#B9CF33] transition-colors">
{photographer?.name || 'Fotógrafo'}
</p>
<p className="text-xs text-gray-500">Fotógrafo</p>
</div>
</div>
);
})}
</div>
) : (
<p className="text-sm text-gray-500 italic bg-gray-50 p-3 rounded-lg border border-dashed border-gray-200">Nenhum profissional atribuído</p>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
</div>
<style>{`
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-slideUp {
animation: slideUp 0.5s ease-out;
}
.animate-fadeIn {
animation: fadeIn 0.6s ease-out;
}
`}</style>
</div>
);
};