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:
parent
1c20b570c0
commit
788e0dca70
12 changed files with 453 additions and 95 deletions
|
|
@ -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
|
// Se o evento for confirmado, enviar notificações com logística
|
||||||
if status == "Confirmado" {
|
// [MODIFIED] User requested to NOT send notification on approval. Only manually via logistics panel.
|
||||||
go s.NotifyLogistics(context.Background(), agendaID, nil, regiao)
|
// if status == "Confirmado" {
|
||||||
}
|
// go s.NotifyLogistics(context.Background(), agendaID, nil, regiao)
|
||||||
|
// }
|
||||||
|
|
||||||
return agenda, nil
|
return agenda, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefo
|
||||||
SenhaHash: string(hashedPassword),
|
SenhaHash: string(hashedPassword),
|
||||||
Role: role,
|
Role: role,
|
||||||
TipoProfissional: toPgText(&tipoProfissional),
|
TipoProfissional: toPgText(&tipoProfissional),
|
||||||
|
Ativo: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,23 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
|
||||||
return i, err
|
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
|
const linkUserToProfessional = `-- name: LinkUserToProfessional :exec
|
||||||
UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1
|
UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroC
|
||||||
|
|
||||||
const createUsuario = `-- name: CreateUsuario :one
|
const createUsuario = `-- name: CreateUsuario :one
|
||||||
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
|
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
|
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"`
|
SenhaHash string `json:"senha_hash"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
TipoProfissional pgtype.Text `json:"tipo_profissional"`
|
TipoProfissional pgtype.Text `json:"tipo_profissional"`
|
||||||
|
Ativo bool `json:"ativo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUsuario(ctx context.Context, arg CreateUsuarioParams) (Usuario, error) {
|
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.SenhaHash,
|
||||||
arg.Role,
|
arg.Role,
|
||||||
arg.TipoProfissional,
|
arg.TipoProfissional,
|
||||||
|
arg.Ativo,
|
||||||
)
|
)
|
||||||
var i Usuario
|
var i Usuario
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,11 @@ RETURNING *;
|
||||||
DELETE FROM cadastro_profissionais
|
DELETE FROM cadastro_profissionais
|
||||||
WHERE id = $1 AND regiao = @regiao;
|
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
|
-- name: SearchProfissionais :many
|
||||||
SELECT p.*,
|
SELECT p.*,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
-- name: CreateUsuario :one
|
-- name: CreateUsuario :one
|
||||||
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
|
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
|
||||||
VALUES ($1, $2, $3, $4, false)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetUsuarioByEmail :one
|
-- name: GetUsuarioByEmail :one
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|
@ -270,7 +271,7 @@ func (s *Service) GetByID(ctx context.Context, id string, regiao string) (*gener
|
||||||
type UpdateProfissionalInput struct {
|
type UpdateProfissionalInput struct {
|
||||||
Nome string `json:"nome"`
|
Nome string `json:"nome"`
|
||||||
FuncaoProfissionalID string `json:"funcao_profissional_id"`
|
FuncaoProfissionalID string `json:"funcao_profissional_id"`
|
||||||
FuncoesIds []string `json:"funcoes_ids"` // New field
|
FuncoesIds []string `json:"funcoes_ids"`
|
||||||
Endereco *string `json:"endereco"`
|
Endereco *string `json:"endereco"`
|
||||||
Cidade *string `json:"cidade"`
|
Cidade *string `json:"cidade"`
|
||||||
Uf *string `json:"uf"`
|
Uf *string `json:"uf"`
|
||||||
|
|
@ -295,6 +296,7 @@ type UpdateProfissionalInput struct {
|
||||||
Equipamentos *string `json:"equipamentos"`
|
Equipamentos *string `json:"equipamentos"`
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
AvatarURL *string `json:"avatar_url"`
|
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) {
|
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")
|
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)
|
funcaoUUID, err := uuid.Parse(input.FuncaoProfissionalID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid funcao_profissional_id")
|
return nil, errors.New("invalid funcao_profissional_id")
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,36 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Calendar, Hash, Filter, X } from "lucide-react";
|
import { Calendar, Hash, Filter, X, Building2 } from "lucide-react";
|
||||||
|
|
||||||
export interface EventFilters {
|
export interface EventFilters {
|
||||||
date: string;
|
date: string;
|
||||||
fotId: string;
|
fotId: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
company: string;
|
||||||
|
institution: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventFiltersBarProps {
|
interface EventFiltersBarProps {
|
||||||
filters: EventFilters;
|
filters: EventFilters;
|
||||||
onFilterChange: (filters: EventFilters) => void;
|
onFilterChange: (filters: EventFilters) => void;
|
||||||
availableTypes: string[];
|
availableTypes: string[];
|
||||||
|
availableCompanies: string[];
|
||||||
|
availableInstitutions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
|
export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
|
||||||
filters,
|
filters,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
availableTypes,
|
availableTypes,
|
||||||
|
availableCompanies,
|
||||||
|
availableInstitutions,
|
||||||
}) => {
|
}) => {
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
onFilterChange({
|
onFilterChange({
|
||||||
date: "",
|
date: "",
|
||||||
fotId: "",
|
fotId: "",
|
||||||
type: "",
|
type: "",
|
||||||
|
company: "",
|
||||||
|
institution: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,7 +54,7 @@ export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Filtro por FOT */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="text-xs font-medium text-gray-600 mb-1 flex items-center gap-1">
|
<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>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Active Filters Display */}
|
{/* Active Filters Display */}
|
||||||
|
|
@ -146,6 +198,28 @@ export const EventFiltersBar: React.FC<EventFiltersBarProps> = ({
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -197,11 +197,7 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, isEditable: p
|
||||||
<Truck className="w-5 h-5 mr-2 text-orange-500" />
|
<Truck className="w-5 h-5 mr-2 text-orange-500" />
|
||||||
Logística de Transporte
|
Logística de Transporte
|
||||||
</h3>
|
</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
|
<button
|
||||||
onClick={() => window.location.href = `/painel?eventId=${agendaId}`}
|
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"
|
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">
|
<div className="flex flex-col items-center pt-4 border-t border-gray-100">
|
||||||
<button
|
<button
|
||||||
onClick={handleNotifyLogistics}
|
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 ${
|
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
|
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"
|
: "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" />
|
<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>
|
</button>
|
||||||
{eventData?.logisticaNotificacaoEnviadaEm && (
|
{eventData?.logisticaNotificacaoEnviadaEm && (
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
|
||||||
const [avatarPreview, setAvatarPreview] = useState<string>("");
|
const [avatarPreview, setAvatarPreview] = useState<string>("");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [isEditingPassword, setIsEditingPassword] = useState(false); // Toggle for edit mode
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
||||||
|
|
||||||
|
|
@ -126,6 +127,7 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
|
||||||
setAvatarPreview("");
|
setAvatarPreview("");
|
||||||
}
|
}
|
||||||
setAvatarFile(null);
|
setAvatarFile(null);
|
||||||
|
setIsEditingPassword(false);
|
||||||
}
|
}
|
||||||
}, [isOpen, professional]); // user dependency intentionally omitted to avoid reset loop, but safe to add if needed
|
}, [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);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!professional && (formData.senha || formData.confirmarSenha)) {
|
if (formData.senha || formData.confirmarSenha) {
|
||||||
if (formData.senha !== formData.confirmarSenha) {
|
if (formData.senha !== formData.confirmarSenha) {
|
||||||
alert("As senhas não coincidem!");
|
alert("As senhas não coincidem!");
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
@ -309,8 +311,19 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: any = { ...formData, avatar_url: finalAvatarUrl };
|
const payload: any = { ...formData, avatar_url: finalAvatarUrl };
|
||||||
delete payload.senha;
|
|
||||||
delete payload.confirmarSenha;
|
// 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) {
|
if (professional) {
|
||||||
// Update
|
// Update
|
||||||
|
|
@ -465,55 +478,91 @@ export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
|
||||||
|
|
||||||
{/* Functions removed from here */}
|
{/* Functions removed from here */}
|
||||||
|
|
||||||
{/* Email & Pass */}
|
{/* Email & Pass */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Email *</label>
|
<label className="block text-sm font-medium text-gray-700">Email *</label>
|
||||||
<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" />
|
<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>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Senha *</label>
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
<div className="relative mt-1">
|
{professional ? "Nova Senha" : "Senha *"}
|
||||||
<input
|
</label>
|
||||||
required
|
<div className="relative mt-1">
|
||||||
type={showPassword ? "text" : "password"}
|
<input
|
||||||
value={formData.senha || ""}
|
required={!professional}
|
||||||
onChange={e => setFormData({ ...formData, senha: e.target.value })}
|
type={showPassword ? "text" : "password"}
|
||||||
minLength={6}
|
value={formData.senha || ""}
|
||||||
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"
|
onChange={e => setFormData({ ...formData, senha: e.target.value })}
|
||||||
/>
|
minLength={6}
|
||||||
<button
|
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"
|
||||||
type="button"
|
placeholder={professional ? "Digitar nova senha" : ""}
|
||||||
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
|
/>
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
<button
|
||||||
>
|
type="button"
|
||||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
</button>
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
</div>
|
>
|
||||||
</div>
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
<div>
|
</button>
|
||||||
<label className="block text-sm font-medium text-gray-700">Confirmar Senha *</label>
|
</div>
|
||||||
<div className="relative mt-1">
|
</div>
|
||||||
<input
|
<div>
|
||||||
required
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
type={showConfirmPassword ? "text" : "password"}
|
{professional ? "Confirmar Nova Senha" : "Confirmar Senha *"}
|
||||||
value={formData.confirmarSenha || ""}
|
</label>
|
||||||
onChange={e => setFormData({ ...formData, confirmarSenha: e.target.value })}
|
<div className="relative mt-1">
|
||||||
minLength={6}
|
<input
|
||||||
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"
|
required={!professional || (!!formData.senha && formData.senha.length > 0)}
|
||||||
/>
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
<button
|
value={formData.confirmarSenha || ""}
|
||||||
type="button"
|
onChange={e => setFormData({ ...formData, confirmarSenha: e.target.value })}
|
||||||
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
|
minLength={6}
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
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" : ""}
|
||||||
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
/>
|
||||||
</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
</div>
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
</>
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
|
</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>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1180,6 +1180,18 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
briefing: data.observacoes_evento || evt.briefing,
|
briefing: data.observacoes_evento || evt.briefing,
|
||||||
fotId: data.fot_id || evt.fotId,
|
fotId: data.fot_id || evt.fotId,
|
||||||
empresaId: data.empresa_id || evt.empresaId, // If provided
|
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
|
// Address is hard to parse back to object from payload without logic
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
X,
|
X,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
UserX,
|
UserX,
|
||||||
|
AlertCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useData } from "../contexts/DataContext";
|
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)
|
// Force reload of view to reflect changes (or rely on DataContext optimistic update)
|
||||||
// But DataContext optimistic update only touched generic fields.
|
// But DataContext optimistic update only touched generic fields.
|
||||||
// Address might still be old in 'selectedEvent' state if we don't update it.
|
// Address might still be old in 'selectedEvent' state if we don't update it.
|
||||||
// Updating selectedEvent manually as well to be safe:
|
// 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 };
|
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);
|
setSelectedEvent(updatedEvent);
|
||||||
setView("details");
|
setView("details");
|
||||||
// Optional: Reload page safely if critical fields changed that DataContext map didn't catch?
|
// Reloading window to ensure total consistency with backend as fallback
|
||||||
// For now, trust DataContext + local state update.
|
// window.location.reload(); // Commented out to try SPA update first
|
||||||
// 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
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Update function not available");
|
console.error("Update function not available");
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +136,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
date: "",
|
date: "",
|
||||||
fotId: "",
|
fotId: "",
|
||||||
type: "",
|
type: "",
|
||||||
|
company: "",
|
||||||
|
institution: "",
|
||||||
});
|
});
|
||||||
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
|
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
|
||||||
const [viewingProfessional, setViewingProfessional] = useState<Professional | null>(null);
|
const [viewingProfessional, setViewingProfessional] = useState<Professional | null>(null);
|
||||||
|
|
@ -221,7 +237,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
fotoFaltante,
|
fotoFaltante,
|
||||||
recepFaltante,
|
recepFaltante,
|
||||||
cineFaltante,
|
cineFaltante,
|
||||||
profissionaisOK
|
profissionaisOK,
|
||||||
|
qtdFotografos,
|
||||||
|
qtdRecepcionistas,
|
||||||
|
qtdCinegrafistas
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -347,10 +366,14 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Extract unique values for filters
|
// Extract unique values for filters
|
||||||
const { availableTypes } = useMemo(() => {
|
const { availableTypes, availableCompanies, availableInstitutions } = useMemo(() => {
|
||||||
const types = [...new Set(myEvents.map((e) => e.type))].sort();
|
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 {
|
return {
|
||||||
availableTypes: types,
|
availableTypes: types,
|
||||||
|
availableCompanies: companies,
|
||||||
|
availableInstitutions: institutions,
|
||||||
};
|
};
|
||||||
}, [myEvents]);
|
}, [myEvents]);
|
||||||
|
|
||||||
|
|
@ -384,14 +407,16 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
String(e.fot || "").toLowerCase().includes(advancedFilters.fotId.toLowerCase());
|
String(e.fot || "").toLowerCase().includes(advancedFilters.fotId.toLowerCase());
|
||||||
const matchesType =
|
const matchesType =
|
||||||
!advancedFilters.type || e.type === advancedFilters.type;
|
!advancedFilters.type || e.type === advancedFilters.type;
|
||||||
|
const matchesCompany =
|
||||||
|
!advancedFilters.company || e.empresa === advancedFilters.company;
|
||||||
|
const matchesInstitution =
|
||||||
|
!advancedFilters.institution || e.instituicao === advancedFilters.institution;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType
|
matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType && matchesCompany && matchesInstitution
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Keep selectedEvent in sync with global events state
|
// Keep selectedEvent in sync with global events state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEvent) {
|
if (selectedEvent) {
|
||||||
|
|
@ -690,6 +715,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
filters={advancedFilters}
|
filters={advancedFilters}
|
||||||
onFilterChange={setAdvancedFilters}
|
onFilterChange={setAdvancedFilters}
|
||||||
availableTypes={availableTypes}
|
availableTypes={availableTypes}
|
||||||
|
availableCompanies={availableCompanies}
|
||||||
|
availableInstitutions={availableInstitutions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
|
|
@ -1438,26 +1465,78 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
{isTeamModalOpen && selectedEvent && (
|
{isTeamModalOpen && selectedEvent && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<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">
|
<div className="bg-white rounded-2xl shadow-2xl max-w-6xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
{/* Header */}
|
{/* Header do Modal */}
|
||||||
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 flex justify-between items-center">
|
<div className="flex items-center justify-between p-6 border-b border-gray-100">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-white mb-1">
|
<h2 className="text-xl font-serif font-bold text-brand-black">
|
||||||
Gerenciar Equipe
|
Gerenciar Equipe
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white/80 text-sm">
|
<p className="text-sm text-gray-500">
|
||||||
{selectedEvent.name} -{" "}
|
{selectedEvent.name} - {new Date(selectedEvent.date + 'T00:00:00').toLocaleDateString('pt-BR')}
|
||||||
{new Date(
|
</p>
|
||||||
selectedEvent.date + "T00:00:00"
|
</div>
|
||||||
).toLocaleDateString("pt-BR")}
|
<button
|
||||||
</p>
|
onClick={closeTeamModal}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<button
|
|
||||||
onClick={closeTeamModal}
|
|
||||||
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors"
|
|
||||||
>
|
|
||||||
<X size={24} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<div className="flex-1 overflow-auto p-6">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue