atualização

This commit is contained in:
Yago Santana 2025-12-01 10:59:24 -03:00
parent 9940f4f967
commit 44ed329b68
14 changed files with 969 additions and 97 deletions

127
App.tsx
View file

@ -70,19 +70,120 @@ const AppContent: React.FC = () => {
<main>{renderPage()}</main>
{currentPage === "home" && (
<footer className="bg-white border-t border-gray-100 py-12">
<div className="max-w-7xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500">
<p>&copy; 2025 PhotumFormaturas. Todos os direitos reservados.</p>
<div className="flex space-x-6 mt-4 md:mt-0">
<a href="#" className="hover:text-brand-black">
Política de Privacidade
</a>
<a href="#" className="hover:text-brand-black">
Termos de Uso
</a>
<a href="#" className="hover:text-brand-black">
Instagram
</a>
<footer className="bg-brand-black text-white py-20">
<div className="w-full max-w-[1400px] mx-auto px-8 sm:px-12 lg:px-20">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-16 lg:gap-24 mb-20">
{/* Logo e Descrição */}
<div className="lg:col-span-1">
<h3 className="text-2xl font-serif font-bold mb-4" style={{color: '#B9CF33'}}>
Photum Formaturas
</h3>
<p className="text-gray-400 text-sm leading-relaxed">
Eternizando momentos únicos com excelência e profissionalismo desde 2020.
</p>
</div>
{/* Serviços */}
<div>
<h4 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">Serviços</h4>
<ul className="space-y-3 text-gray-400 text-sm">
<li>
<a href="#" className="hover:text-brand-gold transition-colors">
Fotografia de Eventos
</a>
</li>
<li>
<a href="#" className="hover:text-brand-gold transition-colors">
Álbuns Personalizados
</a>
</li>
<li>
<a href="#" className="hover:text-brand-gold transition-colors">
Gestão de Formaturas
</a>
</li>
<li>
<a href="#" className="hover:text-brand-gold transition-colors">
Cobertura Completa
</a>
</li>
</ul>
</div>
{/* Links Úteis */}
<div>
<h4 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">Links Úteis</h4>
<ul className="space-y-3 text-gray-400 text-sm">
<li>
<a href="#" onClick={() => setCurrentPage('login')} className="hover:text-brand-gold transition-colors">
Área do Cliente
</a>
</li>
<li>
<a href="#" onClick={() => setCurrentPage('register')} className="hover:text-brand-gold transition-colors">
Cadastre sua Formatura
</a>
</li>
<li>
<a href="#" className="hover:text-brand-gold transition-colors">
Portfólio
</a>
</li>
<li>
<a href="#" className="hover:text-brand-gold transition-colors">
FAQ
</a>
</li>
</ul>
</div>
{/* Contato */}
<div>
<h4 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">Contato</h4>
<ul className="space-y-3 text-gray-400 text-sm">
<li className="flex items-center gap-2">
<span>📧</span>
<a href="mailto:contato@photum.com.br" className="hover:text-brand-gold transition-colors">
contato@photum.com.br
</a>
</li>
<li className="flex items-center gap-2">
<span>📱</span>
<span>(11) 99999-9999</span>
</li>
<li className="flex items-center gap-2">
<span>📍</span>
<span>São Paulo, SP</span>
</li>
<li className="flex gap-4 mt-4">
<a href="#" className="hover:text-brand-gold transition-colors text-xl">
📷
</a>
<a href="#" className="hover:text-brand-gold transition-colors text-xl">
👥
</a>
<a href="#" className="hover:text-brand-gold transition-colors text-xl">
</a>
</li>
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500">
<p>&copy; 2025 PhotumFormaturas. Todos os direitos reservados.</p>
<div className="flex space-x-8 mt-4 md:mt-0">
<a href="#" className="hover:text-brand-gold transition-colors">
Política de Privacidade
</a>
<a href="#" className="hover:text-brand-gold transition-colors">
Termos de Uso
</a>
<a href="#" className="hover:text-brand-gold transition-colors">
LGPD
</a>
</div>
</div>
</div>
</footer>

View file

@ -3,10 +3,12 @@ import React, { useState, useEffect } from 'react';
import { EventType, EventStatus, Address } from '../types';
import { Input, Select } from './Input';
import { Button } from './Button';
import { MapPin, Upload, Plus, X, Check, FileText, ExternalLink, Search, CheckCircle } from 'lucide-react';
import { MapPin, Upload, Plus, X, Check, FileText, ExternalLink, Search, CheckCircle, Building2, AlertCircle } from 'lucide-react';
import { searchLocationWithGemini, GeoResult } from '../services/genaiService';
import { useAuth } from '../contexts/AuthContext';
import { useData } from '../contexts/DataContext';
import { UserRole } from '../types';
import { InstitutionForm } from './InstitutionForm';
interface EventFormProps {
onCancel: () => void;
@ -16,11 +18,21 @@ interface EventFormProps {
export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initialData }) => {
const { user } = useAuth();
const { institutions, getInstitutionsByUserId, addInstitution } = useData();
const [activeTab, setActiveTab] = useState<'details' | 'location' | 'briefing' | 'files'>('details');
const [addressQuery, setAddressQuery] = useState('');
const [addressResults, setAddressResults] = useState<GeoResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [showToast, setShowToast] = useState(false);
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
// Get institutions based on user role
// Business owners and admins see all institutions, clients see only their own
const userInstitutions = user
? (user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN
? institutions
: getInstitutionsByUserId(user.id))
: [];
// Default State or Initial Data
const [formData, setFormData] = useState(initialData || {
@ -33,7 +45,8 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
briefing: '',
contacts: [{ name: '', role: '', phone: '' }],
files: [] as File[],
coverImage: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80' // Default
coverImage: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80', // Default
institutionId: ''
});
const isClientRequest = user?.role === UserRole.EVENT_OWNER;
@ -96,6 +109,12 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
};
const handleSubmit = () => {
// Validate institution selection
if (!formData.institutionId) {
alert('Por favor, selecione uma instituição antes de continuar.');
return;
}
// Show toast
setShowToast(true);
// Call original submit after small delay for visual effect or immediately
@ -104,6 +123,30 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
}, 1000);
};
const handleInstitutionSubmit = (institutionData: any) => {
const newInstitution = {
...institutionData,
id: `inst-${Date.now()}`,
ownerId: user?.id || ''
};
addInstitution(newInstitution);
setFormData(prev => ({ ...prev, institutionId: newInstitution.id }));
setShowInstitutionForm(false);
};
// Show institution form modal
if (showInstitutionForm) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<InstitutionForm
onCancel={() => setShowInstitutionForm(false)}
onSubmit={handleInstitutionSubmit}
userId={user?.id || ''}
/>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-4xl mx-auto border border-gray-100 slide-up relative">
@ -194,6 +237,70 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
/>
{/* Institution Selection - OBRIGATÓRIO */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
Universidade* <span className="text-brand-gold">(Obrigatório)</span>
</label>
{userInstitutions.length === 0 ? (
<div className="border-2 border-dashed border-amber-300 bg-amber-50 rounded-sm p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="text-amber-600 flex-shrink-0 mt-0.5" size={20} />
<div className="flex-1">
<p className="text-sm font-medium text-amber-900 mb-2">
Nenhuma universidade cadastrada
</p>
<p className="text-xs text-amber-700 mb-3">
Você precisa cadastrar uma universidade antes de criar um evento.
Trabalhamos exclusivamente com eventos fotográficos em universidades.
</p>
<button
type="button"
onClick={() => setShowInstitutionForm(true)}
className="text-xs font-bold text-amber-900 hover:text-amber-700 underline flex items-center"
>
<Plus size={14} className="mr-1" />
Cadastrar minha primeira universidade
</button>
</div>
</div>
</div>
) : (
<div className="space-y-2">
<select
className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors"
value={formData.institutionId}
onChange={(e) => setFormData({ ...formData, institutionId: e.target.value })}
required
>
<option value="">Selecione uma universidade</option>
{userInstitutions.map(inst => (
<option key={inst.id} value={inst.id}>
{inst.name} - {inst.type}
</option>
))}
</select>
<button
type="button"
onClick={() => setShowInstitutionForm(true)}
className="text-xs text-brand-gold hover:underline flex items-center"
>
<Plus size={12} className="mr-1" />
Cadastrar nova universidade
</button>
{formData.institutionId && (
<div className="bg-green-50 border border-green-200 rounded-sm p-3 flex items-center">
<Check size={16} className="text-green-600 mr-2" />
<span className="text-xs text-green-800">Universidade selecionada com sucesso</span>
</div>
)}
</div>
)}
</div>
{/* Cover Image Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
@ -299,7 +406,12 @@ export const EventForm: React.FC<EventFormProps> = ({ onCancel, onSubmit, initia
label="Número"
placeholder="123"
value={formData.address.number}
onChange={(e) => setFormData({ ...formData, address: { ...formData.address, number: e.target.value } })}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '');
setFormData({ ...formData, address: { ...formData.address, number: value } });
}}
type="text"
inputMode="numeric"
/>
</div>
<div className="grid grid-cols-2 gap-4">

View file

@ -1,22 +1,91 @@
import React from 'react';
import React, { useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
mask?: 'phone' | 'cnpj' | 'cep';
}
export const Input: React.FC<InputProps> = ({ label, error, className = '', ...props }) => {
export const Input: React.FC<InputProps> = ({ label, error, className = '', type, mask, onChange, ...props }) => {
const [showPassword, setShowPassword] = useState(false);
const isPassword = type === 'password';
const inputType = isPassword && showPassword ? 'text' : type;
const applyMask = (value: string, maskType?: 'phone' | 'cnpj' | 'cep') => {
if (!maskType) return value;
const numbers = value.replace(/\D/g, '');
switch (maskType) {
case 'phone':
// Limita a 11 dígitos (celular)
const phoneNumbers = numbers.slice(0, 11);
if (phoneNumbers.length <= 10) {
return phoneNumbers.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
}
return phoneNumbers.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
case 'cnpj':
// Limita a 14 dígitos
const cnpjNumbers = numbers.slice(0, 14);
return cnpjNumbers.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/, '$1.$2.$3/$4-$5');
case 'cep':
// Limita a 8 dígitos
const cepNumbers = numbers.slice(0, 8);
return cepNumbers.replace(/(\d{5})(\d{0,3})/, '$1-$2');
default:
return value;
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (mask) {
const maskedValue = applyMask(e.target.value, mask);
e.target.value = maskedValue;
}
onChange?.(e);
};
// Define maxLength baseado na máscara
const getMaxLength = () => {
if (!mask) return undefined;
switch (mask) {
case 'phone': return 15; // (00) 00000-0000
case 'cnpj': return 18; // 00.000.000/0000-00
case 'cep': return 9; // 00000-000
default: return undefined;
}
};
return (
<div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
{label}
</label>
<input
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
${error ? 'border-red-500' : 'border-gray-300'}
${className}`}
{...props}
/>
<div className="relative">
<input
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
${error ? 'border-red-500' : 'border-gray-300'}
${isPassword ? 'pr-10' : ''}
${className}`}
type={inputType}
onChange={handleChange}
maxLength={getMaxLength()}
{...props}
/>
{isPassword && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
)}
</div>
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
</div>
);

View file

@ -0,0 +1,248 @@
import React, { useState } from 'react';
import { Institution, Address } from '../types';
import { Input, Select } from './Input';
import { Button } from './Button';
import { Building2, X, Check } from 'lucide-react';
interface InstitutionFormProps {
onCancel: () => void;
onSubmit: (data: Partial<Institution>) => void;
initialData?: Institution;
userId: string;
}
const INSTITUTION_TYPES = [
'Universidade Pública',
'Universidade Privada',
'Faculdade',
'Instituto Federal',
'Centro Universitário',
'Campus Universitário'
];
export const InstitutionForm: React.FC<InstitutionFormProps> = ({
onCancel,
onSubmit,
initialData,
userId
}) => {
const [formData, setFormData] = useState<Partial<Institution>>(initialData || {
name: '',
type: '',
cnpj: '',
phone: '',
email: '',
description: '',
ownerId: userId,
address: {
street: '',
number: '',
city: '',
state: '',
zip: ''
}
});
const [showToast, setShowToast] = useState(false);
const [stateError, setStateError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setShowToast(true);
setTimeout(() => {
onSubmit(formData);
}, 1000);
};
const handleChange = (field: keyof Institution, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleAddressChange = (field: keyof Address, value: string) => {
setFormData(prev => ({
...prev,
address: {
...prev.address!,
[field]: value
}
}));
};
return (
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl mx-auto border border-gray-100 slide-up relative">
{/* Success Toast */}
{showToast && (
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
<Check className="text-brand-gold h-6 w-6" />
<div>
<h4 className="font-bold text-sm">Sucesso!</h4>
<p className="text-xs text-gray-300">Universidade cadastrada com sucesso.</p>
</div>
</div>
)}
{/* Form Header */}
<div className="bg-gray-50 border-b px-8 py-6 flex justify-between items-center">
<div className="flex items-center space-x-3">
<Building2 className="text-brand-gold h-8 w-8" />
<div>
<h2 className="text-2xl font-serif text-brand-black">
{initialData ? 'Editar Universidade' : 'Cadastrar Universidade'}
</h2>
<p className="text-sm text-gray-500 mt-1">
Registre a universidade onde os eventos fotográficos serão realizados
</p>
</div>
</div>
<button
onClick={onCancel}
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
>
<X size={20} className="text-gray-600" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
{/* Informações Básicas */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
Informações Básicas
</h3>
<Input
label="Nome da Universidade*"
placeholder="Ex: Universidade Federal do Rio Grande do Sul"
value={formData.name || ''}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
<div className="grid grid-cols-2 gap-4">
<Select
label="Tipo de Universidade*"
options={INSTITUTION_TYPES.map(t => ({ value: t, label: t }))}
value={formData.type || ''}
onChange={(e) => handleChange('type', e.target.value)}
required
/>
<Input
label="CNPJ (Opcional)"
placeholder="00.000.000/0000-00"
value={formData.cnpj || ''}
onChange={(e) => handleChange('cnpj', e.target.value)}
mask="cnpj"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
label="Telefone*"
type="tel"
placeholder="(00) 00000-0000"
value={formData.phone || ''}
onChange={(e) => handleChange('phone', e.target.value)}
mask="phone"
required
/>
<Input
label="E-mail*"
type="email"
placeholder="contato@instituicao.com"
value={formData.email || ''}
onChange={(e) => handleChange('email', e.target.value)}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
Descrição (Opcional)
</label>
<textarea
className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-24 text-sm"
placeholder="Ex: Campus principal, informações sobre o campus, áreas para eventos..."
value={formData.description || ''}
onChange={(e) => handleChange('description', e.target.value)}
/>
</div>
</div>
{/* Endereço */}
<div className="space-y-4 border-t pt-6">
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
Endereço (Opcional)
</h3>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">
<Input
label="Rua"
placeholder="Nome da rua"
value={formData.address?.street || ''}
onChange={(e) => handleAddressChange('street', e.target.value)}
/>
</div>
<Input
label="Número"
placeholder="123"
value={formData.address?.number || ''}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '');
handleAddressChange('number', value);
}}
type="text"
inputMode="numeric"
/>
</div>
<div className="grid grid-cols-3 gap-4">
<Input
label="Cidade"
placeholder="Cidade"
value={formData.address?.city || ''}
onChange={(e) => handleAddressChange('city', e.target.value)}
/>
<Input
label="Estado"
placeholder="UF"
value={formData.address?.state || ''}
onChange={(e) => {
const hasNumbers = /[0-9]/.test(e.target.value);
if (hasNumbers) {
setStateError('O campo Estado aceita apenas letras');
setTimeout(() => setStateError(''), 3000);
}
const value = e.target.value.replace(/[0-9]/g, '').toUpperCase();
handleAddressChange('state', value);
}}
maxLength={2}
error={stateError}
/>
<Input
label="CEP"
placeholder="00000-000"
value={formData.address?.zip || ''}
onChange={(e) => handleAddressChange('zip', e.target.value)}
mask="cep"
/>
</div>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t">
<Button variant="outline" onClick={onCancel} type="button">
Cancelar
</Button>
<Button type="submit" variant="secondary">
{initialData ? 'Salvar Alterações' : 'Cadastrar Universidade'}
</Button>
</div>
</form>
</div>
);
};

View file

@ -1,8 +1,27 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { EventData, EventStatus, EventType, Attachment } from '../types';
import { EventData, EventStatus, EventType, Attachment, Institution } from '../types';
// Initial Mock Data
const INITIAL_INSTITUTIONS: Institution[] = [
{
id: 'inst-1',
name: 'Universidade Federal do Rio Grande do Sul',
type: 'Universidade Pública',
phone: '(51) 3308-3333',
email: 'eventos@ufrgs.br',
address: {
street: 'Av. Paulo Gama',
number: '110',
city: 'Porto Alegre',
state: 'RS',
zip: '90040-060'
},
description: 'Campus Central - Principais eventos realizados no Salão de Atos',
ownerId: 'client-1'
}
];
const INITIAL_EVENTS: EventData[] = [
{
id: '1',
@ -27,7 +46,8 @@ const INITIAL_EVENTS: EventData[] = [
{ name: 'Ensaio 2', size: '2mb', type: 'image/jpeg', url: 'https://images.unsplash.com/photo-1511285560982-1351cdeb9821?auto=format&fit=crop&w=400&q=80' }
],
ownerId: 'client-1',
photographerIds: ['photographer-1']
photographerIds: ['photographer-1'],
institutionId: 'inst-1'
},
{
id: '2',
@ -55,17 +75,23 @@ const INITIAL_EVENTS: EventData[] = [
interface DataContextType {
events: EventData[];
institutions: Institution[];
addEvent: (event: EventData) => void;
updateEventStatus: (id: string, status: EventStatus) => void;
assignPhotographer: (eventId: string, photographerId: string) => void;
getEventsByRole: (userId: string, role: string) => EventData[];
addAttachment: (eventId: string, attachment: Attachment) => void;
addInstitution: (institution: Institution) => void;
updateInstitution: (id: string, institution: Partial<Institution>) => void;
getInstitutionsByUserId: (userId: string) => Institution[];
getInstitutionById: (id: string) => Institution | undefined;
}
const DataContext = createContext<DataContextType | undefined>(undefined);
export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [events, setEvents] = useState<EventData[]>(INITIAL_EVENTS);
const [institutions, setInstitutions] = useState<Institution[]>(INITIAL_INSTITUTIONS);
const addEvent = (event: EventData) => {
setEvents(prev => [event, ...prev]);
@ -109,8 +135,38 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}));
};
const addInstitution = (institution: Institution) => {
setInstitutions(prev => [...prev, institution]);
};
const updateInstitution = (id: string, updatedData: Partial<Institution>) => {
setInstitutions(prev => prev.map(inst =>
inst.id === id ? { ...inst, ...updatedData } : inst
));
};
const getInstitutionsByUserId = (userId: string) => {
return institutions.filter(inst => inst.ownerId === userId);
};
const getInstitutionById = (id: string) => {
return institutions.find(inst => inst.id === id);
};
return (
<DataContext.Provider value={{ events, addEvent, updateEventStatus, assignPhotographer, getEventsByRole, addAttachment }}>
<DataContext.Provider value={{
events,
institutions,
addEvent,
updateEventStatus,
assignPhotographer,
getEventsByRole,
addAttachment,
addInstitution,
updateInstitution,
getInstitutionsByUserId,
getInstitutionById
}}>
{children}
</DataContext.Provider>
);

63
package-lock.json generated
View file

@ -11,7 +11,8 @@
"@google/genai": "^1.30.0",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6"
},
"devDependencies": {
"@types/node": "^22.14.0",
@ -1416,6 +1417,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2046,6 +2060,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -2063,6 +2078,44 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
"integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
"license": "MIT",
"dependencies": {
"react-router": "7.9.6"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@ -2156,6 +2209,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2584,4 +2643,4 @@
"license": "ISC"
}
}
}
}

View file

@ -12,7 +12,8 @@
"@google/genai": "^1.30.0",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6"
},
"devDependencies": {
"@types/node": "^22.14.0",
@ -20,4 +21,4 @@
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}
}

View file

@ -4,7 +4,7 @@ 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 } from 'lucide-react';
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';
@ -15,7 +15,7 @@ interface DashboardProps {
export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) => {
const { user } = useAuth();
const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, addAttachment } = useData();
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);
@ -298,6 +298,49 @@ export const Dashboard: React.FC<DashboardProps> = ({ initialView = 'list' }) =>
)}
</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>

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Camera, Heart, Shield, Star } from 'lucide-react';
import { Camera, Heart, Shield, Star, BookOpen } from 'lucide-react';
const HERO_IMAGES = [
"/banner2.jpg",
@ -59,59 +59,150 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
</div>
</div>
{/* Features Section */}
<section className="py-20 bg-white">
{/* Albums Section */}
<section className="py-24 relative overflow-hidden" style={{ backgroundColor: '#ffffff' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-sm font-bold tracking-widest text-brand-gold uppercase mb-2">Por que nós?</h2>
<h3 className="text-3xl md:text-4xl font-serif text-brand-black">Excelência em cada detalhe</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<div className="text-center group">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 text-gray-700 mb-6 transition-all">
<Camera size={40} strokeWidth={1.5} />
<div className="flex flex-col lg:flex-row items-center gap-16">
{/* Left side - Icon and Text */}
<div className="lg:w-1/2 text-left fade-in">
<div className="inline-flex items-center justify-center w-36 h-36 mb-8 transform transition-transform duration-300 hover:scale-110 hover:rotate-3">
<img src="/HOME_17.png" alt="Álbuns" className="w-full h-full object-contain drop-shadow-2xl" />
</div>
<h2 className="text-5xl md:text-6xl font-bold mb-8 leading-tight" style={{color: '#B9CF33'}}>
ÁLBUNS<br/>PERSONALIZADOS
</h2>
<div className="space-y-3 text-gray-700">
<p className="text-lg leading-relaxed">
Escolha a cor, tamanho, tecido, acabamento, modelo,
</p>
<p className="text-lg leading-relaxed">
laminação, tipo de impressão e muito mais!
</p>
<p className="text-xl font-semibold text-brand-black mt-4">
Tenha seu álbum exclusivo e de acordo com o seu gosto.
</p>
</div>
<h4 className="text-xl font-bold mb-3 text-brand-black">Qualidade Impecável</h4>
<p className="text-gray-600 leading-relaxed">
Equipamentos de última geração e profissionais premiados.
</p>
</div>
<div className="text-center group">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 text-gray-700 mb-6 transition-all">
<Shield size={40} strokeWidth={1.5} />
{/* Right side - CTA */}
<div className="lg:w-2/8 flex flex-col items-center lg:items-start justify-center ml-40 slide-up">
<div className="bg-white rounded-lg shadow-2xl p-10 border-t-4 transform transition-all duration-300 hover:shadow-xl hover:-translate-y-2" style={{ borderColor: '#B9CF33' }}>
<div className="flex justify-center mb-6">
<div className="w-29 h-29 transform transition-transform duration-300 hover:scale-110">
<img src="/logo.png" alt="Cadastrar" className="w-full h-full object-contain drop-shadow-lg" />
</div>
</div>
<p className="text-gray-700 text-xl mb-5 text-center font-medium leading-relaxed">
Faça parte da Photum e cadastre sua formatura.
</p>
<button
onClick={onEnter}
className="w-full px-10 py-5 text-white font-bold text-lg rounded-md transition-all duration-300 transform hover:scale-105 hover:shadow-2xl active:scale-95"
style={{ backgroundColor: '#B9CF33' }}
>
Cadastrar Formatura
</button>
</div>
<h4 className="text-xl font-bold mb-3 text-brand-black">Segurança Total</h4>
<p className="text-gray-600 leading-relaxed">
Backup duplo em nuvem e contratos transparentes.
</p>
</div>
<div className="text-center group">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 text-gray-700 mb-6 transition-all">
<Heart size={40} strokeWidth={1.5} />
</div>
<h4 className="text-xl font-bold mb-3 text-brand-black">Atendimento Humanizado</h4>
<p className="text-gray-600 leading-relaxed">
Entendemos que seu evento é um sonho a ser realizado.
</p>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-10 right-10 w-32 h-32 rounded-full opacity-10" style={{ backgroundColor: '#B9CF33' }}></div>
<div className="absolute bottom-10 left-10 w-24 h-24 rounded-full opacity-10" style={{ backgroundColor: '#C2388B' }}></div>
</section>
{/* Testimonials */}
<section className="py-20 bg-brand-black text-white">
<div className="max-w-4xl mx-auto px-4 text-center">
<Star className="text-brand-gold mx-auto mb-6" size={40} fill="#c5a059" />
<blockquote className="text-2xl md:text-3xl font-serif italic leading-relaxed mb-8">
"A equipe do Photum superou todas as expectativas. O sistema de acompanhamento nos deixou tranquilos durante todo o processo e as fotos ficaram incríveis."
</blockquote>
<cite className="not-italic">
<span className="font-bold block text-brand-gold">Mariana & Pedro</span>
<span className="text-sm text-gray-400 uppercase tracking-widest">Casamento em Campos do Jordão</span>
</cite>
{/* Contact Section */}
<section className="py-24 relative overflow-hidden" style={{ backgroundColor: '#492E61' }}>
<div className="max-w-7xl mx-auto px-8 sm:px-12 lg:px-20">
<div className="text-center mb-16">
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4 tracking-wide">ENTRE EM CONTATO</h2>
<p className="text-white/90 text-lg">Envie sua mensagem, ligue ou faça uma visita em nossa empresa!</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20 items-start max-w-6xl mx-auto">
{/* Left side - Contact Info */}
<div className="space-y-6 text-white">
<div className="group">
<div className="flex items-start gap-4 p-3 rounded-lg transition-all duration-300 hover:bg-white/10">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div className="flex-1">
<p className="font-semibold mb-1">Rua Bom Recreio, 305</p>
<p className="text-white/80 text-sm">Jd. Boer II Americana SP</p>
<p className="text-white/80 text-sm mb-1">CEP 13477-720</p>
<a
href="https://goo.gl/maps/4EwukztUUXP2"
target="_blank"
rel="noopener noreferrer"
className="text-xs underline hover:text-white/70 transition-colors inline-flex items-center gap-1"
>
Ver no mapa
</a>
</div>
</div>
</div>
<div className="group">
<div className="flex items-start gap-4 p-3 rounded-lg transition-all duration-300 hover:bg-white/10">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<div className="flex-1">
<p>(19) 3405.5024</p>
<p>(19) 3621.4621</p>
</div>
</div>
</div>
<div className="group">
<div className="flex items-start gap-4 p-3 rounded-lg transition-all duration-300 hover:bg-white/10">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1">
<a href="mailto:contato@photum.com.br" className="hover:text-white/70 transition-colors">
contato@photum.com.br
</a>
</div>
</div>
</div>
</div>
{/* Right side - Contact Form */}
<div className="space-y-5 bg-white/5 p-6 rounded-xl backdrop-blur-sm">
<input
type="text"
placeholder="Nome"
className="w-full px-3 py-3 bg-transparent border-b-2 border-white/30 text-white placeholder-white/60 focus:border-white focus:outline-none transition-colors"
/>
<input
type="tel"
placeholder="Telefone"
className="w-full px-3 py-3 bg-transparent border-b-2 border-white/30 text-white placeholder-white/60 focus:border-white focus:outline-none transition-colors"
/>
<textarea
placeholder="Mensagem"
rows={4}
className="w-full px-3 py-3 bg-transparent border-b-2 border-white/30 text-white placeholder-white/60 focus:border-white focus:outline-none transition-colors resize-none"
></textarea>
<button
className="w-full px-8 py-3 bg-white text-brand-black font-bold rounded-lg hover:bg-white/90 transition-all transform hover:scale-[1.02] active:scale-95 shadow-lg"
>
enviar
</button>
</div>
</div>
</div>
</section>
</div>

View file

@ -2,24 +2,31 @@
import React, { useState } from 'react';
import { Button } from '../components/Button';
import { Input } from '../components/Input';
import { InstitutionForm } from '../components/InstitutionForm';
import { useData } from '../contexts/DataContext';
interface RegisterProps {
onNavigate: (page: string) => void;
}
export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
const { addInstitution } = useData();
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
confirmPassword: '',
wantsToAddInstitution: false
});
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
const [tempUserId] = useState(`user-${Date.now()}`);
const handleChange = (field: string, value: string) => {
const handleChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
setError('');
};
@ -29,6 +36,13 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
setIsLoading(true);
setError('');
// Validação do checkbox de termos
if (!agreedToTerms) {
setError('Você precisa concordar com os termos de uso para continuar');
setIsLoading(false);
return;
}
// Validações
if (formData.password !== formData.confirmPassword) {
setError('As senhas não coincidem');
@ -42,6 +56,13 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
return;
}
// If user wants to add institution, show form
if (formData.wantsToAddInstitution) {
setIsLoading(false);
setShowInstitutionForm(true);
return;
}
// Simular registro (conta será criada como Cliente/EVENT_OWNER automaticamente)
setTimeout(() => {
setIsLoading(false);
@ -52,6 +73,44 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
}, 1500);
};
const handleInstitutionSubmit = (institutionData: any) => {
const newInstitution = {
...institutionData,
id: `inst-${Date.now()}`,
ownerId: tempUserId
};
addInstitution(newInstitution);
setShowInstitutionForm(false);
// Complete registration
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setSuccess(true);
setTimeout(() => {
onNavigate('login');
}, 2000);
}, 1500);
};
// Show institution form modal
if (showInstitutionForm) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<InstitutionForm
onCancel={() => {
// Apenas fecha o modal e volta para o formulário
setShowInstitutionForm(false);
// Desmarca a opção de cadastrar universidade
setFormData(prev => ({ ...prev, wantsToAddInstitution: false }));
}}
onSubmit={handleInstitutionSubmit}
userId={tempUserId}
/>
</div>
);
}
if (success) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
@ -73,7 +132,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
{/* Left Side - Image */}
<div className="hidden lg:block lg:w-1/2 relative overflow-hidden">
<img
src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80"
src="https://images.unsplash.com/photo-1541339907198-e08756dedf3f?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Photum Cadastro"
className="absolute inset-0 w-full h-full object-cover"
/>
@ -131,6 +190,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
placeholder="(00) 00000-0000"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
mask="phone"
/>
<Input
@ -149,36 +209,54 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
placeholder="••••••••"
value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
error={error}
error={error && (error.includes('senha') || error.includes('coincidem')) ? error : undefined}
/>
</div>
<div className="flex items-start">
<div>
<div className="flex items-start">
<input
type="checkbox"
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
className="mt-1 h-4 w-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
/>
<label className="ml-2 text-sm text-gray-600">
Concordo com os{' '}
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
termos de uso
</a>{' '}
e{' '}
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
política de privacidade
</a>
</label>
</div>
{error && error.includes('termos') && (
<span className="text-xs text-red-500 mt-1 block ml-6">{error}</span>
)}
</div>
<div className="flex items-start bg-gray-50 border border-gray-200 rounded-sm p-4">
<input
type="checkbox"
required
checked={formData.wantsToAddInstitution}
onChange={(e) => setFormData(prev => ({ ...prev, wantsToAddInstitution: e.target.checked }))}
className="mt-1 h-4 w-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
/>
<label className="ml-2 text-sm text-gray-600">
Concordo com os{' '}
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
termos de uso
</a>{' '}
e{' '}
<a href="#" className="text-brand-gold hover:text-brand-gold/80">
política de privacidade
</a>
<label className="ml-2 text-sm text-gray-700">
<span className="font-medium">Cadastrar universidade agora (Opcional)</span>
<p className="text-xs text-gray-500 mt-1">
Você pode cadastrar sua universidade durante o cadastro ou posteriormente no sistema.
Trabalhamos exclusivamente com eventos fotográficos em universidades.
</p>
</label>
</div>
<Button type="submit" className="w-full" size="lg" isLoading={isLoading}>
Criar Conta
{formData.wantsToAddInstitution ? 'Continuar para Universidade' : 'Criar Conta'}
</Button>
</form>
<div className="text-center text-xs text-gray-500 pt-4">
Ao criar uma conta, você concorda em receber atualizações e novidades sobre nossos serviços.
</div>
</div>
</div>
</div>

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/logo23.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/logofav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

@ -29,6 +29,19 @@ export interface User {
email: string;
role: UserRole;
avatar?: string;
institutionId?: string; // Instituição vinculada ao usuário
}
export interface Institution {
id: string;
name: string;
type: string; // Ex: Universidade Pública, Universidade Privada, Faculdade, etc.
cnpj?: string;
phone: string;
email: string;
address?: Address;
description?: string;
ownerId: string; // ID do usuário que criou a instituição
}
export interface Address {
@ -79,4 +92,5 @@ export interface EventData {
attachments: Attachment[];
ownerId: string; // ID do cliente dono do evento
photographerIds: string[]; // IDs dos fotógrafos designados
institutionId?: string; // ID da instituição vinculada (obrigatório)
}