Skip to content

feat(panel-admin): UserResource com List/View/Edit e Role hierarchy#455

Open
YuriSouzaDev wants to merge 1 commit into
he4rt:4.xfrom
YuriSouzaDev:feat/user-resource-panel-admin
Open

feat(panel-admin): UserResource com List/View/Edit e Role hierarchy#455
YuriSouzaDev wants to merge 1 commit into
he4rt:4.xfrom
YuriSouzaDev:feat/user-resource-panel-admin

Conversation

@YuriSouzaDev

Copy link
Copy Markdown
Contributor

Closes #424

Contexto

Não existia UserResource no painel admin. Staff/moderação precisava de uma tela única pra ver e editar um membro por inteiro — os dados estavam espalhados entre Character, ExternalIdentity, Profile, Address e ModerationCase.

Este PR entrega List, Edit e View para os dados editáveis pelo admin, mantendo como seções agregadas somente-leitura os dados que vêm de outros domínios (respeitando a fronteira presentation/core — sem duplicar lógica de escrita de outros módulos). Create ficou fora de escopo por decisão explícita no issue: contas só nascem via OAuth, e criação manual seria admitir débito técnico.

O que entra

Identity — hierarquia de roles (pré-requisito pro Policy do Resource)

  • Enum Role (Staff, Compliance, Recruiter, SquadCaptain, Member) com isStaff(), isCompliance(), canViewUsers().
  • Migration adicionando role e soft deletes ao users; o unique index de username virou parcial (WHERE deleted_at IS NULL) pra não travar reuso de username por conta soft-deletada (regressão pega no fluxo de merge de contas).
  • UserPolicy: viewAny/view liberam staff/compliance/recruiter/squad captain; update/delete restritos a staff; restore/forceDelete restritos a compliance — hard delete nunca é o padrão, sempre com a confirmação explícita que o Filament já exige.
  • Relações profile(), workExperiences(), profileSkills() no User (aceitável no core por poder ser reaproveitado por outros módulos, conforme discutido no issue).

Panel-admin — UserResource

  • List: colunas de username/name/email (buscáveis), role, senioridade, disponibilidade, cidade, nível, status computado (ativo/suspenso/banido/removido) e donator; paginação [25, 50, 100]; filtros de role, senioridade e disponibilidade.
  • Edit (staff-only): identidade (username/name/email/role/is_donator), perfil profissional (nickname, headline, about, senioridade, disponibilidade, pretensão salarial, redes sociais, preferências de remoto/relocação/contratação/deficiência) e endereço — tudo num único submit.
  • View (staff/compliance/recruiter/squad captain): mesmos dados em modo leitura, mais as seções agregadas:
    • Gamificação (nível/XP/reputação/badges/carteira via character()) — 100% somente-leitura, sem action de conceder badge (adiado pro issue de badges por evento, já combinado na thread).
    • Atividade (conexões, total de mensagens, horas de voice aproximadas, cargos do Discord via providers()).
    • Moderação (casos como autor/responsável, suspensão/banimento) — oculta pra quem não é staff/compliance.
  • RelationManagers de Skills e Experiências profissionais: create/edit/delete pra staff, somente leitura pra recruiter/squad captain.

Decisões registradas durante a implementação

  • Skills RelationManager opera sobre profileSkills() (registros ProfileSkill diretos) em vez de Profile::skills() (BelongsToMany) — mesmo resultado prático, menos retrabalho sobre o que já existia.
  • preferences do perfil é um cast custom (AsWorkPreferences), não array puro — o form usa os hooks nativos do Filament (mutateRelationshipDataBeforeFill/SaveUsing) pra achatar/reagrupar os campos, seguindo o mesmo padrão manual já usado em ProfilePage (portal).

Testes

  • UserPolicyTest: cobre a hierarquia de roles em todas as abilities.
  • UserResourceTest (18 testes): autorização por página/seção (List/Edit/View × staff/compliance/recruiter/squad captain/member), edição multi-seção com persistência de preferences/social_links/endereço, validação de username duplicado, estado vazio pra usuário sem Character, RelationManagers (create/edit/delete pra staff, ocultas pra recruiter), soft delete como ação padrão, hard delete restrito a compliance.
vendor/bin/pest app-modules/identity app-modules/panel-admin
# 114 passed

PHPStan (nível 6), Pint e Rector limpos nos arquivos tocados.

Como testar manualmente

  1. make dev, logar em /admin com uma conta staff ou compliance.
  2. Acessar Admin ▸ Usuários — conferir busca, filtros e paginação da listagem.
  3. Abrir um usuário (View) — conferir as abas Identidade/Perfil/Endereço/Gamificação/Atividade/Moderação.
  4. Editar um usuário — alterar campos de perfil (incluindo preferências e redes sociais) e endereço num único submit, salvar e conferir persistência.
  5. Testar a ação de excluir (soft delete por padrão) e, com uma conta compliance, a exclusão física (hard delete, com confirmação).

Adiciona uma tela única no /admin para staff visualizar e editar um
membro por inteiro, agregando dados hoje espalhados entre Character,
ExternalIdentity, Profile, Address e ModerationCase — mantendo a
fronteira presentation/core (seções agregadas são lidas via
relacionamento, sem duplicar lógica de escrita de outros domínios).

Identity:
- Enum Role (Staff, Compliance, Recruiter, SquadCaptain, Member) com
  hierarquia isStaff()/isCompliance()/canViewUsers().
- SoftDeletes no User + migration adicionando `role` e `deleted_at`;
  unique index de `username` vira parcial (WHERE deleted_at IS NULL)
  para não travar reuso de username por conta soft-deletada.
- UserPolicy: viewAny/view liberam staff/compliance/recruiter/squad
  captain; update/delete restritos a staff; restore/forceDelete
  restritos a compliance (hard delete nunca é o padrão).
- Relações profile()/workExperiences()/profileSkills() no User.

Panel-admin (UserResource, sem Create — contas só nascem via OAuth):
- List: colunas de senioridade/disponibilidade/cidade/nível/status
  computado (ativo/suspenso/banido/removido), paginação [25,50,100],
  filtros de role/senioridade/disponibilidade/trashed.
- Edit: identidade (username/name/email/role/is_donator), perfil
  profissional via Section::relationship('profile') com hooks pra
  achatar/reagrupar o cast custom de preferences, e endereço via
  Section::relationship('address').
- View: mesmos dados em modo leitura, mais Gamificação/Atividade/
  Moderação agregadas por relacionamento; seção de Moderação oculta
  para quem não é staff.
- RelationManagers de Skills e Experiências (create/edit/delete
  staff-only; somente leitura para recruiter/squad captain).

Testes: UserPolicyTest cobrindo a hierarquia de roles; UserResource-
Test cobrindo autorização por página/seção, edição multi-seção com
persistência de preferences/social_links/endereço, relation managers,
soft delete padrão e hard delete restrito a compliance.
@YuriSouzaDev
YuriSouzaDev requested a review from a team July 26, 2026 20:32
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds role-based user authorization, soft deletes, localized role labels, and profile relationships. Introduces a Filament Users resource with list, view, edit, forms, tables, infolists, relation managers, and admin navigation registration. Adds feature and unit tests covering permissions, profile data, related records, moderation visibility, and deletion behavior.

Possibly related PRs

Suggested labels: profile, difficulty:easy

Suggested reviewers: clintonrocha98, gvieira18, danielhe4rt

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title matches the main change: the new admin UserResource and role hierarchy.
Description check ✅ Passed Description is directly related and describes the UserResource scope, role hierarchy, and excluded create flow.
Linked Issues check ✅ Passed The PR implements the requested UserResource, policies, soft deletes, sidebar registration, relation managers, and read-only aggregated sections.
Out of Scope Changes check ✅ Passed No unrelated changes stand out; the added migrations, translations, and policies support the UserResource work.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (8)
app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php (1)

77-80: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Global search over email exposes PII to every panel role.

Recruiters/squad captains get read access to this resource; global search results render matched attributes. Consider limiting to username/name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php` around
lines 77 - 80, Update UserResource::getGloballySearchableAttributes() to remove
email from the globally searchable attributes, retaining only username and name
so global search does not expose email values to panel roles.
app-modules/panel-admin/src/Filament/Resources/Users/Tables/UsersTable.php (1)

113-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

operator: named argument is being used to pass a value.

It works only because Eloquent re-shuffles a non-string operator into the value slot. Pass it positionally as the value.

Proposed fix
-                        true: static fn (Builder $query): Builder => $query->whereRelation('profile', 'available_for_proposals', operator: true),
-                        false: static fn (Builder $query): Builder => $query->whereRelation('profile', 'available_for_proposals', operator: false),
+                        true: static fn (Builder $query): Builder => $query->whereRelation('profile', 'available_for_proposals', '=', true),
+                        false: static fn (Builder $query): Builder => $query->whereRelation('profile', 'available_for_proposals', '=', false),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/src/Filament/Resources/Users/Tables/UsersTable.php`
around lines 113 - 116, Update the true and false callbacks in the queries
configuration to pass the boolean available_for_proposals values positionally to
whereRelation instead of using the operator named argument; preserve the
existing profile relationship and filtering behavior.
app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php (3)

27-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Loop hides which role failed.

Use a Pest dataset so failures name the role.

Proposed fix
-test('staff, recruiter e squad captain listam os usuários', function (): void {
+test('staff, recruiter e squad captain listam os usuários', function (string $state): void {
     $member = User::factory()->create();
 
-    foreach (['staff', 'recruiter', 'squadCaptain'] as $state) {
-        $viewer = User::factory()->{$state}()->create();
+    $viewer = User::factory()->{$state}()->create();
 
-        $this->actingAs($viewer);
+    $this->actingAs($viewer);
 
-        livewire(ListUsers::class)
-            ->loadTable()
-            ->assertCanSeeTableRecords([$viewer, $member]);
-    }
-});
+    livewire(ListUsers::class)
+        ->loadTable()
+        ->assertCanSeeTableRecords([$viewer, $member]);
+})->with(['staff', 'recruiter', 'squadCaptain']);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php` around
lines 27 - 39, Replace the foreach in the “staff, recruiter e squad captain
listam os usuários” test with a Pest dataset that supplies each role to a
separate test case, preserving the existing viewer creation, authentication, and
table assertions so failures identify the specific role.

90-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the name uniqueness rule flagged in UserForm.php.

No test exercises editing a user whose name collides with another — add one, or drop the rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php` around
lines 90 - 128, Extend the user edit feature tests around EditUser to cover the
UserForm name uniqueness rule: create two users, edit one while assigning the
other’s name, submit the form, and assert the validation error is present and
the update is not accepted. Keep the existing successful multi-field submission
test unchanged.

279-298: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test asserts less than its name claims.

Only create is checked; edit/delete visibility is untested. Seed a WorkExperience/ProfileSkill and assert those actions are hidden too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php` around
lines 279 - 298, Expand the test “recrutador não vê ações de
criar/editar/excluir nas relation managers” by creating representative
WorkExperience and ProfileSkill records for the target, then assert the create,
edit, and delete table actions are hidden in both WorkExperiencesRelationManager
and ProfileSkillsRelationManager. Keep the existing authentication and page
setup unchanged.
app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserInfolist.php (2)

49-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated inline staff check.

auth()->user()?->role->isStaff() ?? false is repeated here and in both relation managers. Extract a single helper/gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserInfolist.php`
at line 49, Extract the repeated `auth()->user()?->role->isStaff() ?? false`
check into a shared helper or gate, then update the visibility callback in the
UserInfolist and both relation managers to reuse it. Preserve the existing
boolean behavior for authenticated staff and all other users.

366-369: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Magic string 'joined' for voice state.

Use the state enum instead of a literal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserInfolist.php`
around lines 366 - 369, Update the Voice query in UserInfolist to replace the
literal 'joined' state with the corresponding Voice state enum value, reusing
the existing enum symbol and leaving the query behavior unchanged.
app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php (1)

103-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Staff authorization is inlined in three places instead of going through UserPolicy. auth()->user()?->role->isStaff() ?? false is copy-pasted, so a role-hierarchy change must be applied in every copy and never gets policy coverage.

  • app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php#L103-L106: replace the method body with a Gate/policy ability check (e.g. Gate::allows('update', $this->getOwnerRecord())).
  • app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/ProfileSkillsRelationManager.php#L84-L87: same replacement.
  • app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserInfolist.php#L49-L49: gate the Moderação tab on the same shared ability instead of the inline role check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php`
around lines 103 - 106, Replace the inline staff-role checks with the shared
UserPolicy ability check: update isEditableByCurrentUser() in
app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php:103-106
and ProfileSkillsRelationManager.php:84-87 to use Gate::allows('update',
$this->getOwnerRecord()), and update the Moderação tab condition in
UserInfolist.php:49 to use the same ability. Ensure the required Gate import is
present in each affected file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app-modules/identity/database/migrations/2026_07_26_120000_add_role_and_soft_deletes_to_users_table.php`:
- Around line 32-34: Update the rollback logic in the users-table migration
before applying unique('username') to reconcile soft-deleted duplicate
usernames, such as by renaming or otherwise separating deleted duplicates; then
restore the global username constraint and remove the role and deleted_at
columns.

In `@app-modules/identity/src/User/Models/User.php`:
- Line 61: Update the User model’s deletion behavior around SoftDeletes and
UserObserver::deleted() so address deletion occurs only when the user is
force-deleted, not when soft-deleted. Preserve address data through soft-delete
and restore flows while retaining the existing cascade for permanent deletion.

In `@app-modules/identity/tests/Unit/User/UserPolicyTest.php`:
- Around line 47-61: Extend the UserPolicyTest mutation coverage by creating
recruiter and squad-captain users and asserting UserPolicy::update, delete,
restore, and forceDelete all return false for each read-only role. Preserve the
existing member and compliance assertions.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/ProfileSkillsRelationManager.php`:
- Around line 31-36: Add scoped uniqueness validation to the skill_id Select in
ProfileSkillsRelationManager, importing the required Unique rule and Profile
symbols. Configure the rule to constrain profile_id using the owner profile and
ignore the current record, so duplicate profile-skill assignments produce
Filament validation instead of a database exception.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php`:
- Around line 46-56: Update the form schema in WorkExperiencesRelationManager so
is_currently_working_here is no longer live unless a dependent field is added,
and add validation ensuring end_date is not earlier than start_date and is
absent when the current-working checkbox is selected. Keep start_date required
and preserve the existing field labels.

In `@app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserForm.php`:
- Around line 58-61: Remove the unique validation from the name field in the
UserForm schema, specifically the TextInput::make('name') definition, while
preserving its label and required validation; keep uniqueness validation only on
the username field.
- Around line 81-95: Update the mutateRelationshipDataBeforeFillUsing callback
to verify that $data['preferences'] is an actual WorkPreferences instance before
accessing its properties. When the value is absent, null, or a plain array, use
a WorkPreferences default instance, and remove or correct the misleading `@var`
annotation while preserving the existing mapped fields.

---

Nitpick comments:
In
`@app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php`:
- Around line 103-106: Replace the inline staff-role checks with the shared
UserPolicy ability check: update isEditableByCurrentUser() in
app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php:103-106
and ProfileSkillsRelationManager.php:84-87 to use Gate::allows('update',
$this->getOwnerRecord()), and update the Moderação tab condition in
UserInfolist.php:49 to use the same ability. Ensure the required Gate import is
present in each affected file.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserInfolist.php`:
- Line 49: Extract the repeated `auth()->user()?->role->isStaff() ?? false`
check into a shared helper or gate, then update the visibility callback in the
UserInfolist and both relation managers to reuse it. Preserve the existing
boolean behavior for authenticated staff and all other users.
- Around line 366-369: Update the Voice query in UserInfolist to replace the
literal 'joined' state with the corresponding Voice state enum value, reusing
the existing enum symbol and leaving the query behavior unchanged.

In `@app-modules/panel-admin/src/Filament/Resources/Users/Tables/UsersTable.php`:
- Around line 113-116: Update the true and false callbacks in the queries
configuration to pass the boolean available_for_proposals values positionally to
whereRelation instead of using the operator named argument; preserve the
existing profile relationship and filtering behavior.

In `@app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php`:
- Around line 77-80: Update UserResource::getGloballySearchableAttributes() to
remove email from the globally searchable attributes, retaining only username
and name so global search does not expose email values to panel roles.

In `@app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php`:
- Around line 27-39: Replace the foreach in the “staff, recruiter e squad
captain listam os usuários” test with a Pest dataset that supplies each role to
a separate test case, preserving the existing viewer creation, authentication,
and table assertions so failures identify the specific role.
- Around line 90-128: Extend the user edit feature tests around EditUser to
cover the UserForm name uniqueness rule: create two users, edit one while
assigning the other’s name, submit the form, and assert the validation error is
present and the update is not accepted. Keep the existing successful multi-field
submission test unchanged.
- Around line 279-298: Expand the test “recrutador não vê ações de
criar/editar/excluir nas relation managers” by creating representative
WorkExperience and ProfileSkill records for the target, then assert the create,
edit, and delete table actions are hidden in both WorkExperiencesRelationManager
and ProfileSkillsRelationManager. Keep the existing authentication and page
setup unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ac38e745-698b-4962-b0e6-489690ffa43e

📥 Commits

Reviewing files that changed from the base of the PR and between 5c8d0bf and 0f6baed.

📒 Files selected for processing (21)
  • app-modules/identity/database/factories/UserFactory.php
  • app-modules/identity/database/migrations/2026_07_26_120000_add_role_and_soft_deletes_to_users_table.php
  • app-modules/identity/lang/en/enums.php
  • app-modules/identity/lang/pt_BR/enums.php
  • app-modules/identity/src/IdentityServiceProvider.php
  • app-modules/identity/src/User/Enums/Role.php
  • app-modules/identity/src/User/Models/User.php
  • app-modules/identity/src/User/Policies/UserPolicy.php
  • app-modules/identity/tests/Unit/User/UserPolicyTest.php
  • app-modules/panel-admin/src/Filament/Resources/Users/Pages/EditUser.php
  • app-modules/panel-admin/src/Filament/Resources/Users/Pages/ListUsers.php
  • app-modules/panel-admin/src/Filament/Resources/Users/Pages/ViewUser.php
  • app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/ProfileSkillsRelationManager.php
  • app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php
  • app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserForm.php
  • app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserInfolist.php
  • app-modules/panel-admin/src/Filament/Resources/Users/Tables/UsersTable.php
  • app-modules/panel-admin/src/Filament/Resources/Users/UserResource.php
  • app-modules/panel-admin/src/PanelAdminServiceProvider.php
  • app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php
  • app/Providers/AuthServiceProvider.php

Comment on lines +32 to +34
Schema::table('users', static function (Blueprint $table): void {
$table->unique('username');
$table->dropColumn(['role', 'deleted_at']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make rollback safe after username reuse.

After up() permits an active and soft-deleted user to share a username, unique('username') will fail during rollback. Reconcile or rename deleted duplicates before restoring the global constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/identity/database/migrations/2026_07_26_120000_add_role_and_soft_deletes_to_users_table.php`
around lines 32 - 34, Update the rollback logic in the users-table migration
before applying unique('username') to reconcile soft-deleted duplicate
usernames, such as by renaming or otherwise separating deleted duplicates; then
restore the global username constraint and remove the role and deleted_at
columns.

use HasUuids;
use InteractsWithMedia;
use Notifiable;
use SoftDeletes;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not delete addresses on soft delete.

SoftDeletes triggers UserObserver::deleted() (app-modules/identity/src/User/Observers/UserObserver.php lines 17-19), which deletes the address. A restored user therefore loses address data. Restrict this cascade to force deletes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/identity/src/User/Models/User.php` at line 61, Update the User
model’s deletion behavior around SoftDeletes and UserObserver::deleted() so
address deletion occurs only when the user is force-deleted, not when
soft-deleted. Preserve address data through soft-delete and restore flows while
retaining the existing cascade for permanent deletion.

Comment on lines +47 to +61
test('update and delete deny non-staff roles', function (): void {
$member = User::factory()->create();

expect($this->policy->update($member))->toBeFalse()
->and($this->policy->delete($member))->toBeFalse();
});

test('restore and forceDelete require compliance', function (): void {
$compliance = User::factory()->compliance()->create();
$staff = User::factory()->staff()->create();

expect($this->policy->restore($compliance))->toBeTrue()
->and($this->policy->forceDelete($compliance))->toBeTrue()
->and($this->policy->restore($staff))->toBeFalse()
->and($this->policy->forceDelete($staff))->toBeFalse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert read-only roles cannot mutate.

Add recruiter and squad-captain denial assertions for update, delete, restore, and force-delete. The current tests only cover Member for non-staff mutation denial.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/identity/tests/Unit/User/UserPolicyTest.php` around lines 47 -
61, Extend the UserPolicyTest mutation coverage by creating recruiter and
squad-captain users and asserting UserPolicy::update, delete, restore, and
forceDelete all return false for each read-only role. Preserve the existing
member and compliance assertions.

Comment on lines +31 to +36
Select::make('skill_id')
->label('Skill')
->relationship('skill', 'name')
->searchable()
->preload()
->required(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -e php . --full-path '*migrations*' --exec rg -ln 'profile_skills' {} \; | xargs -r cat -n

Repository: he4rt/heartdevs.com

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -eu

echo "== candidate files =="
fd -t f -e php . | rg 'ProfileSkillsRelationManager|Migration.*Skills|ProfileSkill|skills.*profile|profile.*skills' || true

echo "== git status short =="
git status --short

echo "== find all php files with profile_skills =="
rg -n "profile_skills|ProfileSkill|profile_id|skill_id" --glob '*.php' . | head -200

Repository: he4rt/heartdevs.com

Length of output: 15665


🏁 Script executed:

#!/bin/bash
set -eu

echo "== relation manager =="
cat -n app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/ProfileSkillsRelationManager.php | sed -n '1,140p'

echo "== migrations =="
for f in app-modules/profile/database/migrations/2026_07_07_000001_create_profile_skills_table.php app-modules/profile/database/migrations/2026_07_07_000000_create_skills_table.php; do
  echo "--- $f"
  cat -n "$f"
done

echo "== admin create/update/profile tests around profile skills =="
cat -n app-modules/panel-admin/tests/Feature/Users/UserResourceTest.php | sed -n '218,315p'

Repository: he4rt/heartdevs.com

Length of output: 15678


Add a scoped unique validation for existing skills.

profile_skills.profile_id + skill_id is unique, but this RelationManager’s form only uses a relationship Select; recreating the same skill for the same profile can raise a raw database exception instead of showing a Filament validation error. Add ->unique(modifyRuleUsing: fn (Unique $rule) => $rule->where('profile_id', Profile::ensureExists($this->getOwnerRecord()->id)->id), ignoreRecord: true).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/ProfileSkillsRelationManager.php`
around lines 31 - 36, Add scoped uniqueness validation to the skill_id Select in
ProfileSkillsRelationManager, importing the required Unique rule and Profile
symbols. Configure the rule to constrain profile_id using the owner profile and
ignore the current record, so duplicate profile-skill assignments produce
Filament validation instead of a database exception.

Comment on lines +46 to +56
DatePicker::make('start_date')
->label('Start Date')
->required(),

DatePicker::make('end_date')
->label('End Date'),

Checkbox::make('is_currently_working_here')
->label('Currently Working Here')
->live(),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

->live() has no dependents, and date consistency is unvalidated.

is_currently_working_here triggers re-renders for nothing, and nothing prevents end_date < start_date or an end date on a current job.

Proposed fix
                 DatePicker::make('start_date')
                     ->label('Start Date')
-                    ->required(),
+                    ->required()
+                    ->maxDate(now()),
 
                 DatePicker::make('end_date')
-                    ->label('End Date'),
+                    ->label('End Date')
+                    ->afterOrEqual('start_date')
+                    ->hidden(fn (Get $get): bool => (bool) $get('is_currently_working_here')),
 
                 Checkbox::make('is_currently_working_here')
                     ->label('Currently Working Here')
                     ->live(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DatePicker::make('start_date')
->label('Start Date')
->required(),
DatePicker::make('end_date')
->label('End Date'),
Checkbox::make('is_currently_working_here')
->label('Currently Working Here')
->live(),
]);
DatePicker::make('start_date')
->label('Start Date')
->required()
->maxDate(now()),
DatePicker::make('end_date')
->label('End Date')
->afterOrEqual('start_date')
->hidden(fn (Get $get): bool => (bool) $get('is_currently_working_here')),
Checkbox::make('is_currently_working_here')
->label('Currently Working Here')
->live(),
]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/panel-admin/src/Filament/Resources/Users/RelationManagers/WorkExperiencesRelationManager.php`
around lines 46 - 56, Update the form schema in WorkExperiencesRelationManager
so is_currently_working_here is no longer live unless a dependent field is
added, and add validation ensuring end_date is not earlier than start_date and
is absent when the current-working checkbox is selected. Keep start_date
required and preserve the existing field labels.

Comment on lines +58 to +61
TextInput::make('name')
->label('Name')
->required()
->unique(ignoreRecord: true),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

name should not be unique.

Display names legitimately collide; only username is a natural key. This blocks saving any user whose real name already exists.

Proposed fix
             TextInput::make('name')
                 ->label('Name')
-                ->required()
-                ->unique(ignoreRecord: true),
+                ->required(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TextInput::make('name')
->label('Name')
->required()
->unique(ignoreRecord: true),
TextInput::make('name')
->label('Name')
->required(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserForm.php`
around lines 58 - 61, Remove the unique validation from the name field in the
UserForm schema, specifically the TextInput::make('name') definition, while
preserving its label and required validation; keep uniqueness validation only on
the username field.

Comment on lines +81 to +95
->mutateRelationshipDataBeforeFillUsing(function (array $data): array {
/** @var WorkPreferences $preferences */
$preferences = $data['preferences'] ?? new WorkPreferences();

return [
...Arr::except($data, ['preferences']),
'has_disability' => $preferences->hasDisability,
'willing_to_relocate' => $preferences->willingToRelocate,
'is_open_to_remote' => $preferences->isOpenToRemote,
'employment_types' => array_map(
static fn (EmploymentType $type): string => $type->value,
$preferences->employmentTypes,
),
];
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

@var WorkPreferences annotation is wrong when the key is absent/null.

The docblock asserts the type before the null-coalesce; if preferences is a plain array (unhydrated cast) the property reads below fatal. Guard on the instance instead.

Proposed fix
-                /** `@var` WorkPreferences $preferences */
-                $preferences = $data['preferences'] ?? new WorkPreferences();
+                $preferences = $data['preferences'] instanceof WorkPreferences
+                    ? $data['preferences']
+                    : new WorkPreferences();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
->mutateRelationshipDataBeforeFillUsing(function (array $data): array {
/** @var WorkPreferences $preferences */
$preferences = $data['preferences'] ?? new WorkPreferences();
return [
...Arr::except($data, ['preferences']),
'has_disability' => $preferences->hasDisability,
'willing_to_relocate' => $preferences->willingToRelocate,
'is_open_to_remote' => $preferences->isOpenToRemote,
'employment_types' => array_map(
static fn (EmploymentType $type): string => $type->value,
$preferences->employmentTypes,
),
];
})
->mutateRelationshipDataBeforeFillUsing(function (array $data): array {
$preferences = $data['preferences'] instanceof WorkPreferences
? $data['preferences']
: new WorkPreferences();
return [
...Arr::except($data, ['preferences']),
'has_disability' => $preferences->hasDisability,
'willing_to_relocate' => $preferences->willingToRelocate,
'is_open_to_remote' => $preferences->isOpenToRemote,
'employment_types' => array_map(
static fn (EmploymentType $type): string => $type->value,
$preferences->employmentTypes,
),
];
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/panel-admin/src/Filament/Resources/Users/Schemas/UserForm.php`
around lines 81 - 95, Update the mutateRelationshipDataBeforeFillUsing callback
to verify that $data['preferences'] is an actual WorkPreferences instance before
accessing its properties. When the value is absent, null, or a plain array, use
a WorkPreferences default instance, and remove or correct the misleading `@var`
annotation while preserving the existing mapped fields.

Comment on lines +58 to +61
TextInput::make('name')
->label('Name')
->required()
->unique(ignoreRecord: true),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acho que aqui tem que se atentar por que pessoas podem ter o mesmo nome. kkk só pontuando isso por que coincidentemente eu soube de outra Bruna Domingues Leite na minha cidade sem nenhum parentesco comigo, então aqui deveria ser considerado.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(panel-admin): full CRUD User resource com informação agregada de perfil, gamificação, atividade e moderação

2 participants