feat: melhorias no dashboard e correções no perfil

- Implementa filtros de Empresa e Instituição no Dashboard.
- Adiciona barra de estatísticas de equipe (fotógrafos, cinegrafistas, recepcionistas) na modal de Gerenciar Equipe.
- Corrige bug de atualização da interface após editar evento (mapeamento snake_case).
- Adiciona máscaras de input (CPF/CNPJ, Telefone) na página de Perfil.
- Corrige ordenação e persistência da listagem de eventos por FOT.
- Corrige crash e corrupção de dados na página de Perfil.

fix: permite reenviar notificação de logística

- Remove bloqueio do botão de notificação de logística quando já enviada.
- Altera texto do botão para "Reenviar Notificação" quando aplicável.

feat: melhorias no dashboard, perfil e logística

- Implementa filtros de Empresa e Instituição no Dashboard.
- Adiciona barra de estatísticas de equipe na modal de Gerenciar Equipe.
- Desacopla notificação de logística da aprovação do evento (agora apenas manual).
- Permite reenviar notificação de logística e remove exibição redundante de data.
- Adiciona máscaras de input (CPF/CNPJ, Telefone) no Perfil.
- Corrige atualização da interface pós-edição de evento.
- Corrige crash, ordenação e persistência na listagem de eventos e perfil.
This commit is contained in:
NANDO9322 2026-02-08 12:54:41 -03:00
parent 1c20b570c0
commit 788e0dca70
12 changed files with 453 additions and 95 deletions

View file

@ -440,9 +440,10 @@ func (s *Service) UpdateStatus(ctx context.Context, agendaID uuid.UUID, status s
}
// Se o evento for confirmado, enviar notificações com logística
if status == "Confirmado" {
go s.NotifyLogistics(context.Background(), agendaID, nil, regiao)
}
// [MODIFIED] User requested to NOT send notification on approval. Only manually via logistics panel.
// if status == "Confirmado" {
// go s.NotifyLogistics(context.Background(), agendaID, nil, regiao)
// }
return agenda, nil
}

View file

@ -59,6 +59,7 @@ func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefo
SenhaHash: string(hashedPassword),
Role: role,
TipoProfissional: toPgText(&tipoProfissional),
Ativo: false,
})
if err != nil {
return nil, err

View file

@ -475,6 +475,23 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
return i, err
}
const linkProfissionalToUsuario = `-- name: LinkProfissionalToUsuario :exec
UPDATE cadastro_profissionais
SET usuario_id = $2
WHERE id = $1 AND regiao = $3
`
type LinkProfissionalToUsuarioParams struct {
ID pgtype.UUID `json:"id"`
UsuarioID pgtype.UUID `json:"usuario_id"`
Regiao pgtype.Text `json:"regiao"`
}
func (q *Queries) LinkProfissionalToUsuario(ctx context.Context, arg LinkProfissionalToUsuarioParams) error {
_, err := q.db.Exec(ctx, linkProfissionalToUsuario, arg.ID, arg.UsuarioID, arg.Regiao)
return err
}
const linkUserToProfessional = `-- name: LinkUserToProfessional :exec
UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1
`

View file

@ -46,7 +46,7 @@ func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroC
const createUsuario = `-- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
VALUES ($1, $2, $3, $4, false)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, email, senha_hash, role, tipo_profissional, ativo, criado_em, atualizado_em, regioes_permitidas
`
@ -55,6 +55,7 @@ type CreateUsuarioParams struct {
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
Ativo bool `json:"ativo"`
}
func (q *Queries) CreateUsuario(ctx context.Context, arg CreateUsuarioParams) (Usuario, error) {
@ -63,6 +64,7 @@ func (q *Queries) CreateUsuario(ctx context.Context, arg CreateUsuarioParams) (U
arg.SenhaHash,
arg.Role,
arg.TipoProfissional,
arg.Ativo,
)
var i Usuario
err := row.Scan(

View file

@ -112,6 +112,11 @@ RETURNING *;
DELETE FROM cadastro_profissionais
WHERE id = $1 AND regiao = @regiao;
-- name: LinkProfissionalToUsuario :exec
UPDATE cadastro_profissionais
SET usuario_id = $2
WHERE id = $1 AND regiao = @regiao;
-- name: SearchProfissionais :many
SELECT p.*,
COALESCE(

View file

@ -1,6 +1,6 @@
-- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
VALUES ($1, $2, $3, $4, false)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: GetUsuarioByEmail :one

View file

@ -9,6 +9,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt"
)
type Service struct {
@ -270,7 +271,7 @@ func (s *Service) GetByID(ctx context.Context, id string, regiao string) (*gener
type UpdateProfissionalInput struct {
Nome string `json:"nome"`
FuncaoProfissionalID string `json:"funcao_profissional_id"`
FuncoesIds []string `json:"funcoes_ids"` // New field
FuncoesIds []string `json:"funcoes_ids"`
Endereco *string `json:"endereco"`
Cidade *string `json:"cidade"`
Uf *string `json:"uf"`
@ -295,6 +296,7 @@ type UpdateProfissionalInput struct {
Equipamentos *string `json:"equipamentos"`
Email *string `json:"email"`
AvatarURL *string `json:"avatar_url"`
Senha *string `json:"senha"` // New field for password update
}
func (s *Service) Update(ctx context.Context, id string, input UpdateProfissionalInput, regiao string) (*generated.CadastroProfissionai, error) {
@ -303,6 +305,127 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
return nil, errors.New("invalid id")
}
// 1. Password Update Logic (if provided)
if input.Senha != nil && *input.Senha != "" {
fmt.Printf("[DEBUG] Updating password for professional %s. New Password Length: %d\n", id, len(*input.Senha))
// Get Professional to find UsuarioID
// Requires region to be safe, though ID is unique. Using passed region.
currentProf, err := s.queries.GetProfissionalByID(ctx, generated.GetProfissionalByIDParams{
ID: pgtype.UUID{Bytes: uuidVal, Valid: true},
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
fmt.Printf("[DEBUG] Error fetching professional for password update: %v\n", err)
} else {
fmt.Printf("[DEBUG] Professional found. UsuarioID Valid: %v, UUID: %v\n", currentProf.UsuarioID.Valid, currentProf.UsuarioID.Bytes)
}
if err == nil && currentProf.UsuarioID.Valid {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*input.Senha), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("erro ao gerar hash da senha: %v", err)
}
fmt.Printf("[DEBUG] Updating UsuarioID %v with new hash\n", currentProf.UsuarioID.Bytes)
err = s.queries.UpdateUsuarioSenha(ctx, generated.UpdateUsuarioSenhaParams{
ID: currentProf.UsuarioID,
SenhaHash: string(hashedPassword),
})
if err != nil {
fmt.Printf("[DEBUG] Error updating user password: %v\n", err)
return nil, fmt.Errorf("erro ao atualizar senha do usuário: %v", err)
}
fmt.Println("[DEBUG] Password updated successfully")
} else {
// No UsuarioID found. We need to create one or link to existing email.
emailToUse := input.Email
if emailToUse == nil || *emailToUse == "" {
if currentProf.Email.Valid {
e := currentProf.Email.String
emailToUse = &e
}
}
if emailToUse != nil && *emailToUse != "" {
fmt.Printf("[DEBUG] User not linked. Attempting to link/create for email: %s\n", *emailToUse)
// 1. Check if user exists
existingUser, err := s.queries.GetUsuarioByEmail(ctx, *emailToUse)
var userID pgtype.UUID
hashedPassword, hashErr := bcrypt.GenerateFromPassword([]byte(*input.Senha), bcrypt.DefaultCost)
if hashErr != nil {
return nil, fmt.Errorf("erro ao gerar hash da senha: %v", hashErr)
}
if err == nil {
// User exists. Link it and update password.
fmt.Printf("[DEBUG] User exists with ID %v. Linking...\n", existingUser.ID.Bytes)
userID = existingUser.ID
// Update password for existing user
err = s.queries.UpdateUsuarioSenha(ctx, generated.UpdateUsuarioSenhaParams{
ID: userID,
SenhaHash: string(hashedPassword),
})
if err != nil {
return nil, fmt.Errorf("erro ao atualizar senha do usuário existente: %v", err)
}
} else {
// User does not exist. Create new user.
fmt.Println("[DEBUG] User does not exist. Creating new user...")
// Determine role based on function. Default to PHOTOGRAPHER.
role := "PHOTOGRAPHER" // Default
// Ideally we should use the function to determine role, but for now safe default or "RESEARCHER" check?
// input.FuncaoProfissionalID/FuncoesIds might be present.
// Let's rely on default for now as we don't have easy access to role logic here without circular dependency or extra queries.
// Actually, we can just set it to PHOTOGRAPHER as it grants access to app. They need proper access.
newUser, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
Email: *emailToUse,
SenhaHash: string(hashedPassword),
Role: role,
TipoProfissional: pgtype.Text{String: "Fotógrafo", Valid: true}, // Placeholder, should be aligned with function
Ativo: true,
})
if err != nil {
return nil, fmt.Errorf("erro ao criar novo usuário para profissional: %v", err)
}
userID = newUser.ID
fmt.Printf("[DEBUG] Created new user with ID %v\n", userID.Bytes)
// Ensure region access
if regiao != "" {
_ = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
ID: userID,
RegioesPermitidas: []string{regiao},
})
}
}
// Link Professional to User
err = s.queries.LinkProfissionalToUsuario(ctx, generated.LinkProfissionalToUsuarioParams{
ID: pgtype.UUID{Bytes: uuidVal, Valid: true},
UsuarioID: userID,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
// If link fails, we might leave a dangling user if created, but that's acceptable for now.
return nil, fmt.Errorf("erro ao vincular profissional ao usuário: %v", err)
}
fmt.Println("[DEBUG] Professional successfully linked to User.")
} else {
fmt.Println("[DEBUG] Cannot create user: No email available for professional.")
}
}
} else {
fmt.Println("[DEBUG] No password provided for update")
}
funcaoUUID, err := uuid.Parse(input.FuncaoProfissionalID)
if err != nil {
return nil, errors.New("invalid funcao_profissional_id")

View file

@ -1,28 +1,36 @@
import React from "react";
import { Calendar, Hash, Filter, X } from "lucide-react";
import { Calendar, Hash, Filter, X, Building2 } from "lucide-react";
export interface EventFilters {
date: string;
fotId: string;
type: string;
company: string;
institution: string;
}
interface EventFiltersBarProps {
filters: EventFilters;
onFilterChange: (filters: EventFilters) => void;
availableTypes: string[];
availableCompanies: string[];
availableInstitutions: string[];
}
export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
filters,
onFilterChange,
availableTypes,
availableCompanies,
availableInstitutions,
}) => {
const handleReset = () => {
onFilterChange({
date: "",
fotId: "",
type: "",
company: "",
institution: "",
});
};
@ -46,7 +54,7 @@ export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3">
{/* Filtro por FOT */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
@ -103,6 +111,50 @@ export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
))}
</select>
</div>
{/* Filtro por Empresa */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<Building2 size={14} className="text-brand-gold" />
Empresa
</label>
<select
value={filters.company}
onChange={(e) =>
onFilterChange({ ...filters, company: e.target.value })
}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white"
>
<option value="">Todas as empresas</option>
{availableCompanies.map((comp) => (
<option key={comp} value={comp}>
{comp}
</option>
))}
</select>
</div>
{/* Filtro por Instituição */}
<div className="flex flex-col">
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
<Building2 size={14} className="text-brand-gold" />
Instituição
</label>
<select
value={filters.institution}
onChange={(e) =>
onFilterChange({ ...filters, institution: e.target.value })
}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white"
>
<option value="">Todas as instituições</option>
{availableInstitutions.map((inst) => (
<option key={inst} value={inst}>
{inst}
</option>
))}
</select>
</div>
</div>
{/* Active Filters Display */}
@ -146,6 +198,28 @@ export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
</button>
</span>
)}
{filters.company && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
Empresa: {filters.company}
<button
onClick={() => onFilterChange({ ...filters, company: "" })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
{filters.institution && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-brand-gold/10 text-brand-gold text-xs rounded">
Inst: {filters.institution}
<button
onClick={() => onFilterChange({ ...filters, institution: "" })}
className="hover:text-brand-black"
>
<X size={12} />
</button>
</span>
)}
</div>
</div>
)}

View file

@ -197,11 +197,7 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, isEditable: p
<Truck className="w-5 h-5 mr-2 text-orange-500" />
Logística de Transporte
</h3>
{isEditable && eventData?.logisticaNotificacaoEnviadaEm && (
<div className="text-sm text-green-700 bg-green-50 px-2 py-1 rounded border border-green-200">
Notificação enviada em: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()}
</div>
)}
<button
onClick={() => window.location.href = `/painel?eventId=${agendaId}`}
className="ml-auto flex items-center gap-2 px-3 py-1.5 bg-white border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-50 transition-colors shadow-sm"
@ -341,16 +337,15 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, isEditable: p
<div className="flex flex-col items-center pt-4 border-t border-gray-100">
<button
onClick={handleNotifyLogistics}
disabled={!!eventData?.logisticaNotificacaoEnviadaEm}
className={`px-6 py-2.5 rounded-md flex items-center text-sm font-medium shadow-sm transition-colors w-full justify-center ${
eventData?.logisticaNotificacaoEnviadaEm
? "bg-gray-400 cursor-not-allowed text-white"
? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-green-600 hover:bg-green-700 text-white"
}`}
title={eventData?.logisticaNotificacaoEnviadaEm ? "Notificação já enviada" : "Confirmar logística e notificar equipe"}
title={eventData?.logisticaNotificacaoEnviadaEm ? "Reenviar notificação para a equipe" : "Confirmar logística e notificar equipe"}
>
<Send className="w-5 h-5 mr-2" />
{eventData?.logisticaNotificacaoEnviadaEm ? "Notificação Enviada" : "Finalizar Logística e Notificar Equipe"}
{eventData?.logisticaNotificacaoEnviadaEm ? "Reenviar Notificação" : "Finalizar Logística e Notificar Equipe"}
</button>
{eventData?.logisticaNotificacaoEnviadaEm && (
<p className="mt-2 text-xs text-gray-500">

View file

@ -75,6 +75,7 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
const [avatarPreview, setAvatarPreview] = useState<string>("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isEditingPassword, setIsEditingPassword] = useState(false); // Toggle for edit mode
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoadingCep, setIsLoadingCep] = useState(false);
@ -126,6 +127,7 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
setAvatarPreview("");
}
setAvatarFile(null);
setIsEditingPassword(false);
}
}, [isOpen, professional]); // user dependency intentionally omitted to avoid reset loop, but safe to add if needed
@ -285,7 +287,7 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
setIsSubmitting(true);
try {
if (!professional && (formData.senha || formData.confirmarSenha)) {
if (formData.senha || formData.confirmarSenha) {
if (formData.senha !== formData.confirmarSenha) {
alert("As senhas não coincidem!");
setIsSubmitting(false);
@ -309,8 +311,19 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
}
const payload: any = { ...formData, avatar_url: finalAvatarUrl };
// Handle password logic
if (!payload.senha) {
delete payload.senha;
delete payload.confirmarSenha;
} else {
// Password is set, ensure confirmation matches (already checked above? No, only for !professional)
// We need to re-check match here if it wasn't caught by the generic check which was conditional.
// Let's rely on the check below/above or add one here.
// Currently the check at line 288 is `if (!professional && ...)` which skips for edit.
// We should move that check to be general.
}
delete payload.confirmarSenha; // Always remove confirm from payload to backend
if (professional) {
// Update
@ -471,18 +484,38 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
<input required type="email" value={formData.email || ""} onChange={e => setFormData({ ...formData, email: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
</div>
{!professional && (
<>
{/* Password Fields */}
{(!professional || isEditingPassword) && (
<div className="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-200">
<div className="flex justify-between items-center mb-2">
<h4 className="text-sm font-medium text-gray-900">{professional ? "Alterar Senha" : "Definir Senha"}</h4>
{professional && (
<button
type="button"
onClick={() => {
setIsEditingPassword(false);
setFormData(prev => ({ ...prev, senha: "", confirmarSenha: "" }));
}}
className="text-xs text-red-600 hover:text-red-800"
>
Cancelar
</button>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Senha *</label>
<label className="block text-sm font-medium text-gray-700">
{professional ? "Nova Senha" : "Senha *"}
</label>
<div className="relative mt-1">
<input
required
required={!professional}
type={showPassword ? "text" : "password"}
value={formData.senha || ""}
onChange={e => setFormData({ ...formData, senha: e.target.value })}
minLength={6}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border pr-10"
placeholder={professional ? "Digitar nova senha" : ""}
/>
<button
type="button"
@ -494,15 +527,18 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Confirmar Senha *</label>
<label className="block text-sm font-medium text-gray-700">
{professional ? "Confirmar Nova Senha" : "Confirmar Senha *"}
</label>
<div className="relative mt-1">
<input
required
required={!professional || (!!formData.senha && formData.senha.length > 0)}
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmarSenha || ""}
onChange={e => setFormData({ ...formData, confirmarSenha: e.target.value })}
minLength={6}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border pr-10"
placeholder={professional ? "Confirmar nova senha" : ""}
/>
<button
type="button"
@ -513,7 +549,20 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
</button>
</div>
</div>
</>
</div>
)}
{professional && !isEditingPassword && (
<div className="mb-4">
<Button
type="button"
variant="outline"
onClick={() => setIsEditingPassword(true)}
className="w-full md:w-auto"
>
Alterar Senha de Acesso
</Button>
</div>
)}
<div>

View file

@ -1180,6 +1180,18 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
briefing: data.observacoes_evento || evt.briefing,
fotId: data.fot_id || evt.fotId,
empresaId: data.empresa_id || evt.empresaId, // If provided
// Map team resource fields
qtdFormandos: data.qtd_formandos ?? evt.qtdFormandos,
qtdFotografos: data.qtd_fotografos ?? evt.qtdFotografos,
qtdRecepcionistas: data.qtd_recepcionistas ?? evt.qtdRecepcionistas,
qtdCinegrafistas: data.qtd_cinegrafistas ?? evt.qtdCinegrafistas,
qtdEstudios: data.qtd_estudios ?? evt.qtdEstudios,
qtdPontosFoto: data.qtd_pontos_foto ?? evt.qtdPontosFoto,
qtdPontosDecorados: data.qtd_pontos_decorados ?? evt.qtdPontosDecorados,
qtdPontosLed: data.qtd_pontos_led ?? evt.qtdPontosLed,
qtdPlataforma360: data.qtd_plataforma_360 ?? evt.qtdPlataforma360,
// Address is hard to parse back to object from payload without logic
};
}

View file

@ -19,6 +19,7 @@ import {
X,
UserCheck,
UserX,
AlertCircle,
} from "lucide-react";
import { useAuth } from "../contexts/AuthContext";
import { useData } from "../contexts/DataContext";
@ -61,16 +62,29 @@ export const Dashboard: React.FC<DashboardProps> = ({
// Force reload of view to reflect changes (or rely on DataContext optimistic update)
// But DataContext optimistic update only touched generic fields.
// Address might still be old in 'selectedEvent' state if we don't update it.
// Updating selectedEvent manually as well to be safe:
const updatedEvent = { ...selectedEvent, ...data, date: data.date || data.data_evento?.split('T')[0] || selectedEvent.date };
// Updating selectedEvent manually as well to be safe, mapping snake_case payload to camelCase state:
const updatedEvent = {
...selectedEvent,
...data,
date: data.date || data.data_evento?.split('T')[0] || selectedEvent.date,
// Map snake_case fields used in EventForm payload to camelCase fields used in UI
qtdFormandos: data.qtd_formandos ?? selectedEvent.qtdFormandos,
qtdFotografos: data.qtd_fotografos ?? selectedEvent.qtdFotografos,
qtdRecepcionistas: data.qtd_recepcionistas ?? selectedEvent.qtdRecepcionistas,
qtdCinegrafistas: data.qtd_cinegrafistas ?? selectedEvent.qtdCinegrafistas,
qtdEstudios: data.qtd_estudios ?? selectedEvent.qtdEstudios,
qtdPontosFoto: data.qtd_pontos_foto ?? selectedEvent.qtdPontosFoto,
qtdPontosDecorados: data.qtd_pontos_decorados ?? selectedEvent.qtdPontosDecorados,
qtdPontosLed: data.qtd_pontos_led ?? selectedEvent.qtdPontosLed,
qtdPlataforma360: data.qtd_plataforma_360 ?? selectedEvent.qtdPlataforma360,
name: data.observacoes_evento || selectedEvent.name,
briefing: data.observacoes_evento || selectedEvent.briefing,
time: data.horario || selectedEvent.time,
};
setSelectedEvent(updatedEvent);
setView("details");
// Optional: Reload page safely if critical fields changed that DataContext map didn't catch?
// For now, trust DataContext + local state update.
// Actually, DataContext refetch logic was "try import...", so it might be async.
// Let's reload window to be 100% sure for the user as requested "mudei a data e não mudou".
// setView("details"); // Already called above
// removing window.location.reload() to maintain SPA feel
// Reloading window to ensure total consistency with backend as fallback
// window.location.reload(); // Commented out to try SPA update first
} else {
console.error("Update function not available");
}
@ -122,6 +136,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
date: "",
fotId: "",
type: "",
company: "",
institution: "",
});
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
const [viewingProfessional, setViewingProfessional] = useState<Professional | null>(null);
@ -221,7 +237,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
fotoFaltante,
recepFaltante,
cineFaltante,
profissionaisOK
profissionaisOK,
qtdFotografos,
qtdRecepcionistas,
qtdCinegrafistas
};
};
@ -347,10 +366,14 @@ export const Dashboard: React.FC<DashboardProps> = ({
: undefined;
// Extract unique values for filters
const { availableTypes } = useMemo(() => {
const { availableTypes, availableCompanies, availableInstitutions } = useMemo(() => {
const types = [...new Set(myEvents.map((e) => e.type))].sort();
const companies = [...new Set(myEvents.map((e) => e.empresa).filter(Boolean))].sort();
const institutions = [...new Set(myEvents.map((e) => e.instituicao).filter(Boolean))].sort();
return {
availableTypes: types,
availableCompanies: companies,
availableInstitutions: institutions,
};
}, [myEvents]);
@ -384,14 +407,16 @@ export const Dashboard: React.FC<DashboardProps> = ({
String(e.fot || "").toLowerCase().includes(advancedFilters.fotId.toLowerCase());
const matchesType =
!advancedFilters.type || e.type === advancedFilters.type;
const matchesCompany =
!advancedFilters.company || e.empresa === advancedFilters.company;
const matchesInstitution =
!advancedFilters.institution || e.instituicao === advancedFilters.institution;
return (
matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType
matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType && matchesCompany && matchesInstitution
);
});
// Keep selectedEvent in sync with global events state
useEffect(() => {
if (selectedEvent) {
@ -690,6 +715,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
filters={advancedFilters}
onFilterChange={setAdvancedFilters}
availableTypes={availableTypes}
availableCompanies={availableCompanies}
availableInstitutions={availableInstitutions}
/>
{/* Results Count */}
@ -1438,27 +1465,79 @@ export const Dashboard: React.FC<DashboardProps> = ({
{isTeamModalOpen && selectedEvent && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-6xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 flex justify-between items-center">
{/* Header do Modal */}
<div className="flex items-center justify-between p-6 border-b border-gray-100">
<div>
<h2 className="text-2xl font-bold text-white mb-1">
<h2 className="text-xl font-serif font-bold text-brand-black">
Gerenciar Equipe
</h2>
<p className="text-white/80 text-sm">
{selectedEvent.name} -{" "}
{new Date(
selectedEvent.date + "T00:00:00"
).toLocaleDateString("pt-BR")}
<p className="text-sm text-gray-500">
{selectedEvent.name} - {new Date(selectedEvent.date + 'T00:00:00').toLocaleDateString('pt-BR')}
</p>
</div>
<button
onClick={closeTeamModal}
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors"
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
>
<X size={24} />
<X size={20} className="text-gray-500" />
</button>
</div>
{/* Resource Summary Bar */}
<div className="bg-blue-50 px-6 py-3 border-b border-blue-100 flex flex-wrap gap-4 md:gap-8 overflow-x-auto">
{(() => {
const stats = calculateTeamStatus(selectedEvent);
return (
<>
<div className="flex items-center gap-2 min-w-fit">
<span className="text-xs font-bold text-blue-800 uppercase tracking-wider">Fotógrafos:</span>
<div className="flex items-baseline gap-1">
<span className={`text-sm font-bold ${stats.fotoFaltante > 0 ? 'text-red-600' : 'text-green-600'}`}>
{stats.acceptedFotografos}
</span>
<span className="text-xs text-blue-600">/ {stats.qtdFotografos}</span>
{stats.fotoFaltante === 0 ? (
<CheckCircle size={14} className="text-green-500 ml-1" />
) : (
<AlertCircle size={14} className="text-red-500 ml-1" />
)}
</div>
</div>
<div className="flex items-center gap-2 min-w-fit">
<span className="text-xs font-bold text-blue-800 uppercase tracking-wider">Cinegrafistas:</span>
<div className="flex items-baseline gap-1">
<span className={`text-sm font-bold ${stats.cineFaltante > 0 ? 'text-red-600' : 'text-green-600'}`}>
{stats.acceptedCinegrafistas}
</span>
<span className="text-xs text-blue-600">/ {stats.qtdCinegrafistas}</span>
{stats.cineFaltante === 0 ? (
<CheckCircle size={14} className="text-green-500 ml-1" />
) : (
<AlertCircle size={14} className="text-red-500 ml-1" />
)}
</div>
</div>
<div className="flex items-center gap-2 min-w-fit">
<span className="text-xs font-bold text-blue-800 uppercase tracking-wider">Recepcionistas:</span>
<div className="flex items-baseline gap-1">
<span className={`text-sm font-bold ${stats.recepFaltante > 0 ? 'text-red-600' : 'text-green-600'}`}>
{stats.acceptedRecepcionistas}
</span>
<span className="text-xs text-blue-600">/ {stats.qtdRecepcionistas}</span>
{stats.recepFaltante === 0 ? (
<CheckCircle size={14} className="text-green-500 ml-1" />
) : (
<AlertCircle size={14} className="text-red-500 ml-1" />
)}
</div>
</div>
</>
);
})()}
</div>
{/* Body */}
<div className="flex-1 overflow-auto p-6">
<div className="mb-4">