Appearance
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_idna 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:
- Cria
User(sem role — não consome slot demax_users). - Verifica se já existe um
Membercom o mesmo e-mail e semuser_id. Se existir, apenas vincula esse Member ao User recém-criado (evita duplicata — o admin pode ter cadastrado a pessoa antes). - Caso contrário, cria um novo
Membercomnomederivado doname(primeira palavra =nome, o resto =sobrenome),emailcopiado do User euser_idapontando pra ele. DisparaMemberCreated(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
| Campo | Tipo | Descrição |
|---|---|---|
| ulid | string | Identificador público |
| nome / sobrenome | string | Nome completo |
| string | E-mail de contato (unique, nullable) | |
| cpf | string(11) | CPF (unique, nullable) |
| data_nascimento | date | Data de nascimento |
| sexo | enum(M, F) | Sexo |
| estado_civil | enum | solteiro, casado, divorciado, viuvo, separado |
Dados complementares (preenchidos pelo próprio membro via "Meu Cadastro")
| Campo | Tipo | Descrição |
|---|---|---|
| nome_mae | string | Nome da mãe |
| nome_pai | string | Nome do pai |
| nome_conjuge | string | Nome do cônjuge (texto livre) |
| data_casamento | date | Data do casamento |
| num_filhos | tinyint | Número de filhos |
| local_trabalho | string | Empresa / local de trabalho |
| aptidoes | text | Habilidades, dons, aptidões |
Dados eclesiásticos (exclusivo da secretaria)
| Campo | Tipo | Descrição |
|---|---|---|
| igreja_situacao_id | FK | Em comunhão, Congregado, Visitante, etc. |
| igreja_funcao_id | FK | Membro, Diácono, Presbítero, etc. |
| igreja_cargo_id | FK | Cargos da igreja |
| igreja_modo_admissao_id | FK | Batismo, Conversão, Carta, etc. |
| congregation_id | FK | Congregação vinculada |
| data_admissao | date | Data de admissão |
| data_batismo | date | Data do batismo nas águas |
| data_batismo_espirito_santo | date | Data do batismo no Espírito Santo |
| local_batismo_aguas | string | Local do batismo nas águas |
Vínculos
| Campo | Tipo | Descrição |
|---|---|---|
| user_id | FK (unique) | Vínculo com User do sistema |
| conjuge_id | FK (self) | Cônjuge (self-referência FK, secretaria) |
| responsavel_id | FK (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
| Campo | Tipo |
|---|---|
| member_id | FK |
| telefone_tipo_id | FK (Celular, Residencial, WhatsApp) |
| numero | string(20) |
| is_whatsapp | boolean |
Actions
| Action | Descrição |
|---|---|
| CreateMemberAction | Cria membro + endereço + telefones |
| UpdateMemberAction | Atualiza + sync endereço/telefones |
| DeleteMemberAction | Soft delete |
| LinkMemberToUserAction | Vincula membro a user (valida unicidade) |
| LinkSpouseAction | Vincula 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
| Arquivo | Descrição |
|---|---|
App\Http\Controllers\Settings\MemberProfileController | show() + update() |
App\Http\Requests\Member\StoreMemberProfileRequest | Valida 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étodo | Rota | Descrição |
|---|---|---|
| GET | /members | Lista com filtros (nome, situação, congregação) |
| POST | /members | Criar membro |
| GET | /members/ | Perfil do membro |
| PUT | /members/ | Atualizar |
| DELETE | /members/ | Soft delete |
| GET | /settings/member-profile | Auto-serviço: visualizar próprio cadastro |
| PUT | /settings/member-profile | Auto-serviço: atualizar próprio cadastro |
Permissões
| Permissão | Quem tem |
|---|---|
| member.view | admin-igreja, secretario, admin-congregacao |
| member.create | admin-igreja, secretario |
| member.edit | admin-igreja, secretario, admin-congregacao |
| member.delete | admin-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õegetFirstMediaUrl('foto')ou string vazia - Eager load:
MemberController@indexincluimedianowith(...)para evitar N+1 - Form Requests:
StoreMemberRequest/UpdateMemberRequestvalidamfoto(image|mimes:jpeg,jpg,png,webp|max:5120) eremove_foto(boolean, só no Update) - Frontend:
Members/Create.vueeMembers/Edit.vueenviam via InertiauseForm(auto-converte para FormData quando háFile). O Edit usa_method: 'PUT'e transformaremove_fotoem1/0antes de enviar. - Remoção: usuário clica em "Remover foto" →
remove_foto=1→UpdateMemberActionchamaclearMediaCollection('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:
- Rota
media/{media}/{fileName}emroutes/tenant.php(namemedia.show) que serve via$media->getPath()— caminho já suffixado pelo tenancy. App\Support\MediaLibrary\TenantAwareUrlGenerator(configurado emconfig/media-library.php) que, em contexto tenant, retornaroute('media.show', ...)em vez da URL do disk.nginx/default.confadicionatry_files $uri /index.php?$query_string;no blocolocation ~* \.(js|css|png|…)— sem isso, o nginx retornava 404 para arquivos que não existem nopublic/e oerror_page 404 /index.phppreservava 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
DocumentLogcom tipocarteirinha_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 exibido | Fonte |
|---|---|
| Nome do membro | nome + sobrenome |
| Congregação | congregation.nome |
| Status | Ativo/Inativo (baseado em soft delete) |
| Nome da igreja | ChurchProfile.nome_fantasia |
Não exibe dados sensíveis (CPF, RG, endereço, telefone, foto).
Rotas
| Método | Rota | Descrição |
|---|---|---|
| POST | /documents/member-card | Gerar 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