photum/pages/Dashboard.tsx
2025-12-01 10:59:24 -03:00

508 lines
26 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, Upload, Edit, Users, Map, Image as ImageIcon, Building2 } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useData } from '../contexts/DataContext';
import { STATUS_COLORS } from '../constants';
interface DashboardProps {
initialView?: 'list' | 'create' | 'uploads';
}
export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) => {
const { user } = useAuth();
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, addAttachment, getInstitutionById } = useData();
const [view, setView] = useState<'list' | 'create' | 'edit' | 'details' | 'uploads'>(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: [],
attachments: [],
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);
}
};
const handleUploadPhoto = () => {
if (!selectedEvent) return;
// Mock Upload Action
const newPhoto = {
name: `Foto_${Date.now()}.jpg`,
size: '3.5MB',
type: 'image/jpeg',
url: `https://picsum.photos/id/${Math.floor(Math.random() * 100)}/400/400`
};
addAttachment(selectedEvent.id, newPhoto);
// Force refresh of selectedEvent state from context source
const updated = events.find(e => e.id === selectedEvent.id);
if (updated) {
// manually inject the new attachment for immediate UI feedback if context isn't enough
setSelectedEvent({...updated, attachments: [...updated.attachments, newPhoto]});
}
};
// --- 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 realize uploads.</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 border rounded-lg overflow-hidden shadow-sm">
<div className="h-64 w-full relative">
<img src={selectedEvent.coverImage} className="w-full h-full object-cover" alt="Cover" />
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<h1 className="text-4xl font-serif text-white font-bold text-center px-4 drop-shadow-lg">{selectedEvent.name}</h1>
</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.PHOTOGRAPHER && (
<Button onClick={() => setView('uploads')} className="flex items-center">
<Upload size={16} className="mr-2" /> Gerenciar Uploads
</Button>
)}
{(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="bg-gradient-to-br from-brand-gold/10 to-transparent border border-brand-gold/30 rounded-sm p-6">
<div className="flex items-start space-x-4">
<div className="bg-brand-gold/20 p-3 rounded-full">
<Building2 className="text-brand-gold" size={24} />
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-brand-black mb-1">{institution.name}</h3>
<p className="text-sm text-brand-gold uppercase tracking-wide font-medium mb-3">{institution.type}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div>
<p className="text-gray-500 text-xs uppercase tracking-wide">Contato</p>
<p className="text-gray-700 font-medium">{institution.phone}</p>
<p className="text-gray-600">{institution.email}</p>
</div>
{institution.address && (
<div>
<p className="text-gray-500 text-xs uppercase tracking-wide">Endereço</p>
<p className="text-gray-700">{institution.address.street}, {institution.address.number}</p>
<p className="text-gray-600">{institution.address.city} - {institution.address.state}</p>
</div>
)}
</div>
{institution.description && (
<p className="text-gray-600 text-sm mt-3 italic border-t border-brand-gold/20 pt-3">
{institution.description}
</p>
)}
</div>
</div>
</section>
);
}
return null;
})()}
<section>
<h3 className="text-lg font-bold border-b pb-2 mb-4 text-brand-black">Sobre o Evento</h3>
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{selectedEvent.briefing || "Sem briefing detalhado."}</p>
</section>
{selectedEvent.contacts.length > 0 && (
<section>
<h3 className="text-lg font-bold border-b pb-2 mb-4 text-brand-black">Contatos & Responsáveis</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{selectedEvent.contacts.map((c, i) => (
<div key={i} className="bg-gray-50 p-4 rounded-sm border border-gray-100">
<p className="font-bold text-sm">{c.name}</p>
<p className="text-xs text-brand-gold uppercase tracking-wide">{c.role}</p>
<p className="text-sm text-gray-500 mt-1">{c.phone}</p>
</div>
))}
</div>
</section>
)}
</div>
<div className="col-span-1 space-y-6">
<div className={`p-6 rounded-sm border ${STATUS_COLORS[selectedEvent.status]} bg-opacity-10`}>
<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</h4>
<p className="font-medium text-lg">{selectedEvent.address.street}, {selectedEvent.address.number}</p>
<p className="text-gray-500 mb-4">{selectedEvent.address.city} - {selectedEvent.address.state}</p>
{selectedEvent.address.mapLink ? (
<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>
{(selectedEvent.photographerIds.length > 0 || user.role === UserRole.BUSINESS_OWNER) && (
<div className="border p-6 rounded-sm">
<div className="flex justify-between items-center mb-4">
<h4 className="font-bold uppercase tracking-widest text-xs text-gray-400">Equipe Designada</h4>
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && (
<button onClick={handleManageTeam} className="text-brand-gold hover:text-brand-black"><PlusCircle size={16}/></button>
)}
</div>
{selectedEvent.photographerIds.length > 0 ? (
<div className="flex -space-x-2">
{selectedEvent.photographerIds.map((id, idx) => (
<div key={id} className="w-10 h-10 rounded-full border-2 border-white bg-gray-300"
style={{backgroundImage: `url(https://i.pravatar.cc/100?u=${id})`, backgroundSize: 'cover'}}
title={id}
></div>
))}
</div>
) : (
<p className="text-sm text-gray-400 italic">Nenhum profissional atribuído.</p>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{view === 'uploads' && (
<div className="fade-in">
{/* Check if user came from 'details' of a selected event OR came from Navbar */}
{selectedEvent ? (
<div>
<Button variant="ghost" onClick={() => setView('details')} className="mb-4 pl-0">
Voltar para Detalhes
</Button>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-serif text-brand-black">Galeria de Evento: {selectedEvent.name}</h2>
<p className="text-gray-500 text-sm">Gerencie as fotos e faça novos uploads.</p>
</div>
<Button variant="outline" onClick={() => setSelectedEvent(null)}>
Trocar Evento
</Button>
</div>
{/* Drag and Drop Area */}
<div
className="border-2 border-dashed border-gray-300 rounded-lg p-12 text-center bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer group mb-8"
onClick={handleUploadPhoto}
>
<Upload size={48} className="mx-auto text-gray-400 mb-4 group-hover:text-brand-gold transition-colors" />
<h3 className="text-xl font-medium text-gray-700 mb-2">Adicionar Novas Fotos</h3>
<p className="text-gray-500">Clique aqui para simular o upload de uma nova imagem</p>
</div>
{/* Gallery Grid */}
<div className="space-y-4">
<h3 className="font-bold text-lg flex items-center">
<ImageIcon className="mr-2 text-brand-gold" size={20}/>
Fotos do Evento ({selectedEvent.attachments.filter(a => a.type.startsWith('image')).length})
</h3>
{selectedEvent.attachments.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{selectedEvent.attachments.map((file, idx) => (
<div key={idx} className="relative group aspect-square bg-gray-100 rounded overflow-hidden shadow-sm hover:shadow-md transition-all">
{file.url ? (
<img src={file.url} alt={file.name} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
<ImageIcon size={32}/>
</div>
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-2">
<span className="text-white text-xs truncate w-full">{file.name}</span>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-10 bg-white border rounded">
<p className="text-gray-400">Nenhuma foto carregada ainda.</p>
</div>
)}
</div>
</div>
) : (
// Logic when clicking "Meus Uploads" in navbar: Select an Event first
<div>
<h2 className="text-2xl font-serif text-brand-black mb-6">Selecione um evento para gerenciar uploads</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{myEvents.map(event => (
<div
key={event.id}
className="bg-white border hover:border-brand-gold rounded-lg p-6 cursor-pointer hover:shadow-lg transition-all"
onClick={() => setSelectedEvent(event)}
>
<h3 className="font-bold text-lg mb-2">{event.name}</h3>
<p className="text-gray-500 text-sm mb-4">{new Date(event.date).toLocaleDateString()}</p>
<div className="flex items-center text-brand-gold text-sm font-medium">
<ImageIcon size={16} className="mr-2"/>
{event.attachments.length} arquivos
</div>
</div>
))}
{myEvents.length === 0 && (
<p className="text-gray-500 col-span-3 text-center py-10">Você não possui eventos designados no momento.</p>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
};