photum/frontend/pages/Team.tsx
2025-12-04 00:58:49 -03:00

756 lines
27 KiB
TypeScript

import React, { useState } from "react";
import {
Users,
Camera,
Mail,
Phone,
MapPin,
Star,
Plus,
Search,
Filter,
User,
Upload,
X,
} from "lucide-react";
import { Button } from "../components/Button";
interface Photographer {
id: string;
name: string;
email: string;
phone: string;
location: string;
specialties: string[];
rating: number;
eventsCompleted: number;
status: "active" | "inactive" | "busy";
avatar: string;
joinDate: string;
}
const MOCK_PHOTOGRAPHERS: Photographer[] = [
{
id: "1",
name: "Carlos Silva",
email: "carlos.silva@photum.com",
phone: "(41) 99999-1111",
location: "Curitiba, PR",
specialties: ["Formaturas", "Eventos Corporativos"],
rating: 4.8,
eventsCompleted: 45,
status: "active",
avatar: "https://i.pravatar.cc/150?img=12",
joinDate: "2023-01-15",
},
{
id: "2",
name: "Ana Paula Mendes",
email: "ana.mendes@photum.com",
phone: "(41) 99999-2222",
location: "Curitiba, PR",
specialties: ["Casamentos", "Formaturas"],
rating: 4.9,
eventsCompleted: 62,
status: "busy",
avatar: "https://i.pravatar.cc/150?img=5",
joinDate: "2022-08-20",
},
{
id: "3",
name: "Roberto Costa",
email: "roberto.costa@photum.com",
phone: "(41) 99999-3333",
location: "São José dos Pinhais, PR",
specialties: ["Formaturas", "Eventos Sociais"],
rating: 4.7,
eventsCompleted: 38,
status: "active",
avatar: "https://i.pravatar.cc/150?img=33",
joinDate: "2023-03-10",
},
{
id: "4",
name: "Juliana Santos",
email: "juliana.santos@photum.com",
phone: "(41) 99999-4444",
location: "Curitiba, PR",
specialties: ["Casamentos", "Ensaios"],
rating: 5.0,
eventsCompleted: 71,
status: "active",
avatar: "https://i.pravatar.cc/150?img=9",
joinDate: "2022-05-12",
},
{
id: "5",
name: "Fernando Oliveira",
email: "fernando.oliveira@photum.com",
phone: "(41) 99999-5555",
location: "Pinhais, PR",
specialties: ["Eventos Corporativos", "Formaturas"],
rating: 4.6,
eventsCompleted: 29,
status: "inactive",
avatar: "https://i.pravatar.cc/150?img=15",
joinDate: "2023-07-01",
},
{
id: "6",
name: "Mariana Rodrigues",
email: "mariana.rodrigues@photum.com",
phone: "(41) 99999-6666",
location: "Curitiba, PR",
specialties: ["Formaturas", "Eventos Sociais", "Casamentos"],
rating: 4.9,
eventsCompleted: 54,
status: "busy",
avatar: "https://i.pravatar.cc/150?img=10",
joinDate: "2022-11-05",
},
];
export const TeamPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState<
"all" | "active" | "busy" | "inactive"
>("all");
const [selectedPhotographer, setSelectedPhotographer] =
useState<Photographer | null>(null);
const [showAddModal, setShowAddModal] = useState(false);
const [newPhotographer, setNewPhotographer] = useState({
name: "",
email: "",
phone: "",
location: "",
specialties: [] as string[],
avatar: "",
});
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string>("");
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setAvatarFile(file);
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const removeAvatar = () => {
setAvatarFile(null);
setAvatarPreview("");
};
const getStatusColor = (status: Photographer["status"]) => {
switch (status) {
case "active":
return "bg-green-100 text-green-800";
case "busy":
return "bg-yellow-100 text-yellow-800";
case "inactive":
return "bg-gray-100 text-gray-800";
}
};
const getStatusLabel = (status: Photographer["status"]) => {
switch (status) {
case "active":
return "Disponível";
case "busy":
return "Em Evento";
case "inactive":
return "Inativo";
}
};
const filteredPhotographers = MOCK_PHOTOGRAPHERS.filter((photographer) => {
const matchesSearch =
photographer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
photographer.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus =
statusFilter === "all" || photographer.status === statusFilter;
return matchesSearch && matchesStatus;
});
const stats = {
total: MOCK_PHOTOGRAPHERS.length,
active: MOCK_PHOTOGRAPHERS.filter((p) => p.status === "active").length,
busy: MOCK_PHOTOGRAPHERS.filter((p) => p.status === "busy").length,
avgRating: (
MOCK_PHOTOGRAPHERS.reduce((acc, p) => acc + p.rating, 0) /
MOCK_PHOTOGRAPHERS.length
).toFixed(1),
};
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-6 sm:mb-8">
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black mb-2">
Equipe & Fotógrafos
</h1>
<p className="text-sm sm:text-base text-gray-600">
Gerencie sua equipe de fotógrafos profissionais
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-6 mb-6 sm:mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">
Total de Fotógrafos
</p>
<p className="text-3xl font-bold text-brand-black">
{stats.total}
</p>
</div>
<Users className="text-brand-gold" size={32} />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Disponíveis</p>
<p className="text-3xl font-bold text-green-600">
{stats.active}
</p>
</div>
<Camera className="text-green-600" size={32} />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Em Evento</p>
<p className="text-3xl font-bold text-yellow-600">
{stats.busy}
</p>
</div>
<Camera className="text-yellow-600" size={32} />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Avaliação Média</p>
<p className="text-3xl font-bold text-brand-gold">
{stats.avgRating}
</p>
</div>
<Star className="text-brand-gold" size={32} fill="#B9CF33" />
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3 sm:p-4 md:p-6 mb-4 sm:mb-6">
<div className="flex flex-col gap-3 sm:gap-4">
<div className="flex-1 relative">
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
<input
type="text"
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setStatusFilter("all")}
className={`px-4 py-2 rounded-md font-medium transition-colors ${statusFilter === "all"
? "bg-brand-gold text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
Todos
</button>
<button
onClick={() => setStatusFilter("active")}
className={`px-4 py-2 rounded-md font-medium transition-colors ${statusFilter === "active"
? "bg-green-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
Disponíveis
</button>
<button
onClick={() => setStatusFilter("busy")}
className={`px-4 py-2 rounded-md font-medium transition-colors ${statusFilter === "busy"
? "bg-yellow-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
Em Evento
</button>
</div>
<Button
size="md"
variant="secondary"
onClick={() => setShowAddModal(true)}
>
<Plus size={20} className="mr-2" />
Adicionar Fotógrafo
</Button>
</div>
</div>
{/* Photographers Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{filteredPhotographers.map((photographer) => (
<div
key={photographer.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
onClick={() => setSelectedPhotographer(photographer)}
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<img
src={photographer.avatar}
alt={photographer.name}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h3 className="font-semibold text-lg text-brand-black">
{photographer.name}
</h3>
<div className="flex items-center gap-1 mt-1">
<Star
size={14}
fill="#B9CF33"
className="text-brand-gold"
/>
<span className="text-sm font-medium">
{photographer.rating}
</span>
<span className="text-xs text-gray-500">
({photographer.eventsCompleted} eventos)
</span>
</div>
</div>
</div>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(
photographer.status
)}`}
>
{getStatusLabel(photographer.status)}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center text-sm text-gray-600">
<Mail size={16} className="mr-2 text-brand-gold" />
{photographer.email}
</div>
<div className="flex items-center text-sm text-gray-600">
<Phone size={16} className="mr-2 text-brand-gold" />
{photographer.phone}
</div>
<div className="flex items-center text-sm text-gray-600">
<MapPin size={16} className="mr-2 text-brand-gold" />
{photographer.location}
</div>
</div>
<div className="flex flex-wrap gap-2">
{photographer.specialties.map((specialty, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs font-medium"
>
{specialty}
</span>
))}
</div>
</div>
</div>
))}
</div>
{filteredPhotographers.length === 0 && (
<div className="text-center py-12">
<Users size={48} className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500">Nenhum fotógrafo encontrado</p>
</div>
)}
</div>
{/* Add Photographer Modal */}
{showAddModal && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={() => setShowAddModal(false)}
>
<div
className="bg-white rounded-lg max-w-2xl w-full p-8 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-serif font-bold text-brand-black">
Adicionar Novo Fotógrafo
</h2>
<button
onClick={() => setShowAddModal(false)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
alert(
"Fotógrafo adicionado com sucesso!\n\n" +
JSON.stringify(
{ ...newPhotographer, avatarFile: avatarFile?.name },
null,
2
)
);
setShowAddModal(false);
setNewPhotographer({
name: "",
email: "",
phone: "",
location: "",
specialties: [],
avatar: "",
});
setAvatarFile(null);
setAvatarPreview("");
}}
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foto de Perfil
</label>
<div className="flex items-center gap-4">
{avatarPreview ? (
<div className="relative">
<img
src={avatarPreview}
alt="Preview"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200"
/>
<button
type="button"
onClick={removeAvatar}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
>
<X size={16} />
</button>
</div>
) : (
<div className="w-24 h-24 rounded-full bg-gray-100 border-2 border-dashed border-gray-300 flex items-center justify-center">
<Camera size={32} className="text-gray-400" />
</div>
)}
<div className="flex-1">
<label className="cursor-pointer">
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 border border-gray-300 rounded-md hover:bg-gray-100 transition-colors w-fit">
<Upload size={18} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700">
{avatarFile ? "Trocar foto" : "Selecionar foto"}
</span>
</div>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
</label>
<p className="text-xs text-gray-500 mt-1">
JPG, PNG ou GIF (máx. 5MB)
</p>
{avatarFile && (
<p className="text-xs text-brand-gold mt-1 font-medium">
{avatarFile.name}
</p>
)}
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nome Completo *
</label>
<div className="relative">
<User
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
<input
type="text"
required
value={newPhotographer.name}
onChange={(e) =>
setNewPhotographer({
...newPhotographer,
name: e.target.value,
})
}
placeholder="Ex: João Silva"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<div className="relative">
<Mail
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
<input
type="email"
required
value={newPhotographer.email}
onChange={(e) =>
setNewPhotographer({
...newPhotographer,
email: e.target.value,
})
}
placeholder="joao.silva@photum.com"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Telefone *
</label>
<div className="relative">
<Phone
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
<input
type="tel"
required
value={newPhotographer.phone}
onChange={(e) =>
setNewPhotographer({
...newPhotographer,
phone: e.target.value,
})
}
placeholder="(41) 99999-0000"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Localização *
</label>
<div className="relative">
<MapPin
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
<input
type="text"
required
value={newPhotographer.location}
onChange={(e) =>
setNewPhotographer({
...newPhotographer,
location: e.target.value,
})
}
placeholder="Curitiba, PR"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Especialidades
</label>
<div className="space-y-2">
{[
"Formaturas",
"Casamentos",
"Eventos Corporativos",
"Eventos Sociais",
"Ensaios",
].map((specialty) => (
<label
key={specialty}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={newPhotographer.specialties.includes(
specialty
)}
onChange={(e) => {
if (e.target.checked) {
setNewPhotographer({
...newPhotographer,
specialties: [
...newPhotographer.specialties,
specialty,
],
});
} else {
setNewPhotographer({
...newPhotographer,
specialties: newPhotographer.specialties.filter(
(s) => s !== specialty
),
});
}
}}
className="w-4 h-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
/>
<span className="text-sm text-gray-700">{specialty}</span>
</label>
))}
</div>
</div>
<div className="pt-6 border-t border-gray-200 flex gap-3">
<button
type="button"
onClick={() => setShowAddModal(false)}
className="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium"
>
Cancelar
</button>
<button
type="submit"
className="flex-1 px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
>
Adicionar Fotógrafo
</button>
</div>
</form>
</div>
</div>
)}
{/* Photographer Detail Modal */}
{selectedPhotographer && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={() => setSelectedPhotographer(null)}
>
<div
className="bg-white rounded-lg max-w-2xl w-full p-8"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start gap-4 mb-6">
<img
src={selectedPhotographer.avatar}
alt={selectedPhotographer.name}
className="w-24 h-24 rounded-full object-cover"
/>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-serif font-bold text-brand-black mb-1">
{selectedPhotographer.name}
</h2>
<div className="flex items-center gap-2 mb-2">
<Star
size={18}
fill="#B9CF33"
className="text-brand-gold"
/>
<span className="font-semibold">
{selectedPhotographer.rating}
</span>
<span className="text-sm text-gray-500">
({selectedPhotographer.eventsCompleted} eventos
concluídos)
</span>
</div>
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(
selectedPhotographer.status
)}`}
>
{getStatusLabel(selectedPhotographer.status)}
</span>
</div>
<button
onClick={() => setSelectedPhotographer(null)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
</div>
</div>
<div className="space-y-4 mb-6">
<div className="flex items-center text-gray-700">
<Mail size={20} className="mr-3 text-brand-gold" />
<span>{selectedPhotographer.email}</span>
</div>
<div className="flex items-center text-gray-700">
<Phone size={20} className="mr-3 text-brand-gold" />
<span>{selectedPhotographer.phone}</span>
</div>
<div className="flex items-center text-gray-700">
<MapPin size={20} className="mr-3 text-brand-gold" />
<span>{selectedPhotographer.location}</span>
</div>
</div>
<div className="mb-6">
<h3 className="font-semibold mb-2">Especialidades</h3>
<div className="flex flex-wrap gap-2">
{selectedPhotographer.specialties.map((specialty, index) => (
<span
key={index}
className="px-3 py-1 bg-brand-gold/10 text-brand-gold rounded-full text-sm font-medium"
>
{specialty}
</span>
))}
</div>
</div>
<div className="pt-6 border-t border-gray-200 flex gap-3">
<button
onClick={() => setSelectedPhotographer(null)}
className="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium"
>
Fechar
</button>
<button className="flex-1 px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium">
Ver Agenda
</button>
</div>
</div>
</div>
)}
</div>
);
};