508 lines
26 KiB
TypeScript
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>
|
|
);
|
|
};
|