Skip to content

Membros

Visão Geral

O módulo de Membros gerencia o cadastro de pessoas da igreja — separado do User model. Todo membro pode ter um User associado (para acesso ao sistema), mas nem todo membro precisa de um User (crianças, idosos, etc.). A relação completa Member ↔ User ↔ Staff está descrita em Modelo conceitual (User vs Member vs Staff).

Vínculo e-mail ↔ login

  • User.email é o login (Fortify autentica por ele).
  • Member.email é o contato do membro (unique, nullable).
  • Quando existe user_id na Member, os dois devem ser o mesmo valor — é o que o auto-cadastro garante.

Auto-cadastro (comportamento da RegisterMemberUserAction)

Quando alguém se cadastra pelo /register ou via social login:

  1. Cria User (sem role — não consome slot de max_users).
  2. Verifica se já existe um Member com o mesmo e-mail e sem user_id. Se existir, apenas vincula esse Member ao User recém-criado (evita duplicata — o admin pode ter cadastrado a pessoa antes).
  3. Caso contrário, cria um novo Member com nome derivado do name (primeira palavra = nome, o resto = sobrenome), email copiado do User e user_id apontando pra ele. Dispara MemberCreated (Observer + Scout).

Tudo dentro de uma transação.

O User criado não tem role — não consome slot de max_users do plano. Só consome quando um admin atribui role via PATCH /users/{ulid}/role.

Member Model

Namespace: App\Domain\Member\Models\Member

Campos principais

CampoTipoDescrição
ulidstringIdentificador público
nome / sobrenomestringNome completo
emailstringE-mail de contato (unique, nullable)
cpfstring(11)CPF (unique, nullable)
data_nascimentodateData de nascimento
sexoenum(M, F)Sexo
estado_civilenumsolteiro, casado, divorciado, viuvo, separado

Dados complementares (preenchidos pelo próprio membro via "Meu Cadastro")

CampoTipoDescrição
nome_maestringNome da mãe
nome_paistringNome do pai
nome_conjugestringNome do cônjuge (texto livre)
data_casamentodateData do casamento
num_filhostinyintNúmero de filhos
local_trabalhostringEmpresa / local de trabalho
aptidoestextHabilidades, dons, aptidões

Dados eclesiásticos (exclusivo da secretaria)

CampoTipoDescrição
igreja_situacao_idFKEm comunhão, Congregado, Visitante, etc.
igreja_funcao_idFKMembro, Diácono, Presbítero, etc.
igreja_cargo_idFKCargos da igreja
igreja_modo_admissao_idFKBatismo, Conversão, Carta, etc.
congregation_idFKCongregação vinculada
data_admissaodateData de admissão
data_batismodateData do batismo nas águas
data_batismo_espirito_santodateData do batismo no Espírito Santo
local_batismo_aguasstringLocal do batismo nas águas

Vínculos

CampoTipoDescrição
user_idFK (unique)Vínculo com User do sistema
conjuge_idFK (self)Cônjuge (self-referência FK, secretaria)
responsavel_idFK (self)Responsável (menores de idade)

Relationships

  • phones() — hasMany MemberPhone (tipo + número + is_whatsapp)
  • address() — morphOne Address (polimórfico, reutilizado)
  • dependentes() — hasMany self (via responsavel_id)

MemberPhone Model

CampoTipo
member_idFK
telefone_tipo_idFK (Celular, Residencial, WhatsApp)
numerostring(20)
is_whatsappboolean

Actions

ActionDescrição
CreateMemberActionCria membro + endereço + telefones
UpdateMemberActionAtualiza + sync endereço/telefones
DeleteMemberActionSoft delete
LinkMemberToUserActionVincula membro a user (valida unicidade)
LinkSpouseActionVincula cônjuge bidirecional

Auto-serviço: Meu Cadastro

Qualquer membro que tenha um User vinculado pode atualizar seus próprios dados pessoais em Configurações → Meu Cadastro (/settings/member-profile).

O que o membro pode preencher

  • Dados pessoais: RG, órgão expedidor, data de nascimento, sexo, estado civil
  • Filiação: nome da mãe, nome do pai
  • Cônjuge e família: nome do cônjuge (texto livre), data do casamento, número de filhos
  • Naturalidade e formação: país de nascimento, UF, cidade, escolaridade, profissão, local de trabalho
  • Aptidões: campo texto livre
  • Endereço residencial
  • Telefones (com tipo e flag WhatsApp)

O que permanece exclusivo da secretaria

  • Situação, função, cargo, modo de admissão
  • Data e local de batismo (nas águas e Espírito Santo)
  • Data de admissão, congregação vinculada
  • Observações internas
  • Cônjuge por FK (conjuge_id) — vínculo entre membros cadastrados

Controller e FormRequest

ArquivoDescrição
App\Http\Controllers\Settings\MemberProfileControllershow() + update()
App\Http\Requests\Member\StoreMemberProfileRequestValida apenas campos do auto-serviço

Sincronização CPF User ↔ Member

Quando o usuário atualiza o CPF em Configurações → Perfil (PUT /settings/profile), o ProfileController sincroniza automaticamente o mesmo valor no Member vinculado:

php
if ($user->wasChanged('cpf')) {
    Member::where('user_id', $user->id)->update(['cpf' => $user->cpf]);
}

Endpoints

MétodoRotaDescrição
GET/membersLista com filtros (nome, situação, congregação)
POST/membersCriar membro
GET/members/Perfil do membro
PUT/members/Atualizar
DELETE/members/Soft delete
GET/settings/member-profileAuto-serviço: visualizar próprio cadastro
PUT/settings/member-profileAuto-serviço: atualizar próprio cadastro

Permissões

PermissãoQuem tem
member.viewadmin-igreja, secretario, admin-congregacao
member.createadmin-igreja, secretario
member.editadmin-igreja, secretario, admin-congregacao
member.deleteadmin-igreja

Membros com user_id podem ver/editar seu próprio registro via policy.

A rota /settings/member-profile não exige permissão específica — basta estar autenticado. O controller localiza o Member pelo user_id do usuário logado.

Observer

MemberObserver — no evento created, verifica se o tenant atingiu 90% do limite de membros e registra log de warning.

Foto do Membro

Cadastro e edição de membros permitem upload de foto (avatar), usada no Members/Show, na listagem Members/Index, no widget de aniversariantes da dashboard e na carteirinha.

  • Storage: Spatie MediaLibrary, collection foto (singleFile, image/jpeg|png|webp)
  • Registrada em: Member::registerMediaCollections()
  • Limite: 5 MB por arquivo
  • Acessor: Member::foto_url (appended no JSON); expõe getFirstMediaUrl('foto') ou string vazia
  • Eager load: MemberController@index inclui media no with(...) para evitar N+1
  • Form Requests: StoreMemberRequest / UpdateMemberRequest validam foto (image|mimes:jpeg,jpg,png,webp|max:5120) e remove_foto (boolean, só no Update)
  • Frontend: Members/Create.vue e Members/Edit.vue enviam via Inertia useForm (auto-converte para FormData quando há File). O Edit usa _method: 'PUT' e transforma remove_foto em 1/0 antes de enviar.
  • Remoção: usuário clica em "Remover foto" → remove_foto=1UpdateMemberAction chama clearMediaCollection('foto')

Exibição com tamanho fixo

Avatares em listagens usam <img class="size-10 rounded-full object-cover"> (ou fallback com iniciais). Evita-se <Avatar> do PrimeVue sem size em listas — a imagem original de social login (ex.: Facebook) pode vir em alta resolução e sem constraint fica gigante.

URL de mídia em multi-tenant

Spatie MediaLibrary + stancl/tenancy: o stancl sufixa o root do disk public por tenant (storage/tenant{id}/app/public/) mas não sufixa a URL, deixando ela em {APP_URL}/storage (symlink central). Pra servir os arquivos corretamente o projeto usa:

  1. Rota media/{media}/{fileName} em routes/tenant.php (name media.show) que serve via $media->getPath() — caminho já suffixado pelo tenancy.
  2. App\Support\MediaLibrary\TenantAwareUrlGenerator (configurado em config/media-library.php) que, em contexto tenant, retorna route('media.show', ...) em vez da URL do disk.
  3. nginx/default.conf adiciona try_files $uri /index.php?$query_string; no bloco location ~* \.(js|css|png|…) — sem isso, o nginx retornava 404 para arquivos que não existem no public/ e o error_page 404 /index.php preservava o status mesmo quando o Laravel respondia 200.

Carteirinha de Membro

Credencial de identificação em formato de cartão (85.6mm x 54mm), gerada como PDF.

Geração

  • Action: GenerateMemberCardAction
  • Individual: Botão "Carteirinha" na página do membro (Members/Show)
  • Em lote: Seleção com checkbox na listagem de membros (máximo 50 por vez)
  • Layout: A4 landscape, 3 pares (frente + verso) por página
  • Permission: document.generate
  • Registro: Cada geração cria um DocumentLog com tipo carteirinha_membro

Layout da Carteirinha

Frente:

  • Logo e nome da igreja (header com gradiente navy)
  • Foto do membro (via MediaLibrary collection foto, ou placeholder silhueta)
  • Nome completo, cargo, função, congregação
  • Status "Membro Ativo" + número de controle (8 últimos chars do ULID)

Verso:

  • QR Code (aponta para rota pública de verificação)
  • Endereço e telefone da igreja
  • Texto de identificação + data de emissão

Verificação por QR Code

Rota pública GET /verify/member/{ulid} (com throttle 30,1) exibe:

Dado exibidoFonte
Nome do membronome + sobrenome
Congregaçãocongregation.nome
StatusAtivo/Inativo (baseado em soft delete)
Nome da igrejaChurchProfile.nome_fantasia

Não exibe dados sensíveis (CPF, RG, endereço, telefone, foto).

Rotas

MétodoRotaDescrição
POST/documents/member-cardGerar carteirinha(s) — recebe member_ulids[]
GET/verify/member/{member}Verificação pública (QR code, sem auth)

Testes

bash
docker compose exec app php artisan test --filter=MemberCard
docker compose exec app php artisan test --filter=MemberVerify