567 lines
22 KiB
TypeScript
567 lines
22 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,
|
|
} 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,
|
|
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 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.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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|