From 6215759045b07a9c08be0a255946fe51fdcf19ce Mon Sep 17 00:00:00 2001 From: "Quintino A. G. Souza" Date: Wed, 4 Feb 2026 07:25:51 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20M=C3=B3dulo=20Site=20Users=20para=20cus?= =?UTF-8?q?tomiza=C3=A7=C3=A3o=20de=20perfis=20de=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Módulo Drupal para gerenciamento de campos e fotos de perfil de usuários: - Campos customizados: nome, telefone, categoria, departamento, biografia - Suporte a múltiplas fotos com seleção de foto padrão - Controle de permissões granular para visualização e edição - Bloco de informações do usuário para exibição em páginas - Configurações administrativas para limite de fotos e integração LDAP Co-Authored-By: Claude Opus 4.5 --- README.md | 141 +++++++ config/install/site_users.settings.yml | 3 + .../field.field.user.user.field_user_bio.yml | 20 + ...ld.field.user.user.field_user_category.yml | 19 + ...eld.user.user.field_user_default_photo.yml | 30 ++ ...d.field.user.user.field_user_dept_code.yml | 19 + .../field.field.user.user.field_user_name.yml | 19 + ...field.field.user.user.field_user_phone.yml | 20 + ...ield.field.user.user.field_user_photos.yml | 30 ++ .../field.storage.user.field_user_bio.yml | 18 + ...field.storage.user.field_user_category.yml | 20 + ....storage.user.field_user_default_photo.yml | 19 + ...ield.storage.user.field_user_dept_code.yml | 20 + .../field.storage.user.field_user_name.yml | 20 + .../field.storage.user.field_user_phone.yml | 18 + .../field.storage.user.field_user_photos.yml | 19 + css/site-user-info-block.css | 97 +++++ site_users.info.yml | 11 + site_users.install | 266 +++++++++++++ site_users.libraries.yml | 5 + site_users.links.menu.yml | 6 + site_users.module | 350 ++++++++++++++++++ site_users.permissions.yml | 30 ++ site_users.routing.yml | 7 + src/Form/SiteUsersSettingsForm.php | 73 ++++ src/Plugin/Block/UserInfoBlock.php | 192 ++++++++++ templates/site-user-info-block.html.twig | 64 ++++ templates/user--full.html.twig | 18 + 28 files changed, 1554 insertions(+) create mode 100644 README.md create mode 100644 config/install/site_users.settings.yml create mode 100644 config/optional/field.field.user.user.field_user_bio.yml create mode 100644 config/optional/field.field.user.user.field_user_category.yml create mode 100644 config/optional/field.field.user.user.field_user_default_photo.yml create mode 100644 config/optional/field.field.user.user.field_user_dept_code.yml create mode 100644 config/optional/field.field.user.user.field_user_name.yml create mode 100644 config/optional/field.field.user.user.field_user_phone.yml create mode 100644 config/optional/field.field.user.user.field_user_photos.yml create mode 100644 config/optional/field.storage.user.field_user_bio.yml create mode 100644 config/optional/field.storage.user.field_user_category.yml create mode 100644 config/optional/field.storage.user.field_user_default_photo.yml create mode 100644 config/optional/field.storage.user.field_user_dept_code.yml create mode 100644 config/optional/field.storage.user.field_user_name.yml create mode 100644 config/optional/field.storage.user.field_user_phone.yml create mode 100644 config/optional/field.storage.user.field_user_photos.yml create mode 100644 css/site-user-info-block.css create mode 100644 site_users.info.yml create mode 100644 site_users.install create mode 100644 site_users.libraries.yml create mode 100644 site_users.links.menu.yml create mode 100644 site_users.module create mode 100644 site_users.permissions.yml create mode 100644 site_users.routing.yml create mode 100644 src/Form/SiteUsersSettingsForm.php create mode 100644 src/Plugin/Block/UserInfoBlock.php create mode 100644 templates/site-user-info-block.html.twig create mode 100644 templates/user--full.html.twig diff --git a/README.md b/README.md new file mode 100644 index 0000000..eca9394 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Site Users + +Módulo Drupal para customização de perfis de usuário, incluindo campos adicionais e gerenciamento de fotos. + +## Funcionalidades + +- **Campos de perfil customizados:** + - Nome completo + - Telefone + - Categoria + - Código do departamento + - Biografia + +- **Gerenciamento de fotos:** + - Suporte a múltiplas fotos por usuário + - Seleção de foto padrão + - Limite configurável de quantidade de fotos + - Integração com Media Library + +- **Controle de permissões granular:** + - Visualizar/editar campos do próprio perfil + - Visualizar/editar campos de qualquer usuário + - Gerenciar fotos próprias ou de outros usuários + +- **Bloco de informações:** + - Exibe dados do usuário em páginas de perfil + - Template customizável via Twig + +## Requisitos + +- Drupal 10 ou 11 +- Módulos core: User, Telephone, Text, Media, Media Library + +## Instalação + +1. Copie o módulo para `modules/custom/site_users` +2. Ative o módulo via Drush ou interface administrativa: + +```bash +drush en site_users +``` + +## Configuração + +Acesse as configurações em: +**Administração > Configuração > Módulos Locais > Site Users** + +`/admin/config/local-modules/site-users` + +### Opções disponíveis + +| Configuração | Descrição | Padrão | +|--------------|-----------|--------| +| Quantidade de fotos | Número máximo de fotos por usuário | 5 | +| Atributo LDAP | Nome do atributo LDAP para foto (se aplicável) | jpegPhoto | + +## Permissões + +| Permissão | Descrição | +|-----------|-----------| +| `administer site_users settings` | Administrar configurações do módulo | +| `view own user profile fields` | Visualizar campos do próprio perfil | +| `view any user profile fields` | Visualizar campos de qualquer usuário | +| `edit own user profile fields` | Editar campos do próprio perfil | +| `edit any user profile fields` | Editar campos de qualquer usuário | +| `manage own user photos` | Gerenciar próprias fotos | +| `manage user photos` | Gerenciar fotos de qualquer usuário | + +## Uso do Bloco + +O módulo fornece o bloco **"Informações do Usuário"** que pode ser posicionado em regiões do tema para exibir informações do perfil nas páginas `/user/{id}`. + +Para adicionar o bloco: +1. Acesse **Estrutura > Layout de blocos** +2. Adicione o bloco "Informações do Usuário" na região desejada +3. Configure a visibilidade para páginas de usuário + +## Customização de Templates + +Os templates podem ser sobrescritos no tema: + +- `site-user-info-block.html.twig` - Bloco de informações do usuário +- `user--full.html.twig` - Página completa do usuário + +### Variáveis disponíveis no bloco + +```twig +{{ user_info.uid }} {# ID do usuário #} +{{ user_info.username }} {# Nome de exibição #} +{{ user_info.name }} {# Nome completo #} +{{ user_info.phone }} {# Telefone #} +{{ user_info.category }} {# Categoria #} +{{ user_info.dept_code }} {# Código do departamento #} +{{ user_info.bio }} {# Biografia #} +{{ user_info.photo_url }} {# URL da foto padrão #} +{{ user_info.photo_alt }} {# Texto alternativo da foto #} +{{ user }} {# Entidade completa do usuário #} +``` + +## API + +### Obter foto padrão de um usuário + +```php +$photo = site_users_get_default_photo($user); +if ($photo instanceof \Drupal\media\MediaInterface) { + // Usar a foto +} +``` + +## Estrutura do Módulo + +``` +site_users/ +├── config/ +│ ├── install/ +│ │ └── site_users.settings.yml +│ └── optional/ +│ └── field.*.yml +├── css/ +│ └── site-user-info-block.css +├── src/ +│ ├── Form/ +│ │ └── SiteUsersSettingsForm.php +│ └── Plugin/Block/ +│ └── UserInfoBlock.php +├── templates/ +│ ├── site-user-info-block.html.twig +│ └── user--full.html.twig +├── site_users.info.yml +├── site_users.install +├── site_users.libraries.yml +├── site_users.links.menu.yml +├── site_users.module +├── site_users.permissions.yml +└── site_users.routing.yml +``` + +## Licença + +Este módulo é software livre distribuído sob a licença GPL-2.0-or-later. diff --git a/config/install/site_users.settings.yml b/config/install/site_users.settings.yml new file mode 100644 index 0000000..d28ae55 --- /dev/null +++ b/config/install/site_users.settings.yml @@ -0,0 +1,3 @@ +photos: + max_count: 5 + ldap_attribute: 'jpegPhoto' diff --git a/config/optional/field.field.user.user.field_user_bio.yml b/config/optional/field.field.user.user.field_user_bio.yml new file mode 100644 index 0000000..2d8a811 --- /dev/null +++ b/config/optional/field.field.user.user.field_user_bio.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_bio + module: + - text + - user +id: user.user.field_user_bio +field_name: field_user_bio +entity_type: user +bundle: user +label: Biografia +description: 'Uma breve descrição sobre o usuário.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: text_long diff --git a/config/optional/field.field.user.user.field_user_category.yml b/config/optional/field.field.user.user.field_user_category.yml new file mode 100644 index 0000000..e458b0d --- /dev/null +++ b/config/optional/field.field.user.user.field_user_category.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_category + module: + - user +id: user.user.field_user_category +field_name: field_user_category +entity_type: user +bundle: user +label: Categoria +description: 'Categoria do usuário.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/config/optional/field.field.user.user.field_user_default_photo.yml b/config/optional/field.field.user.user.field_user_default_photo.yml new file mode 100644 index 0000000..19be6f8 --- /dev/null +++ b/config/optional/field.field.user.user.field_user_default_photo.yml @@ -0,0 +1,30 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_default_photo + - media.type.image + module: + - media + - user +id: user.user.field_user_default_photo +field_name: field_user_default_photo +entity_type: user +bundle: user +label: Foto Padrão +description: 'Selecione a foto principal do perfil.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:media' + handler_settings: + target_bundles: + image: image + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/config/optional/field.field.user.user.field_user_dept_code.yml b/config/optional/field.field.user.user.field_user_dept_code.yml new file mode 100644 index 0000000..0cdeed7 --- /dev/null +++ b/config/optional/field.field.user.user.field_user_dept_code.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_dept_code + module: + - user +id: user.user.field_user_dept_code +field_name: field_user_dept_code +entity_type: user +bundle: user +label: Código do Departamento +description: 'Código do departamento do usuário.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/config/optional/field.field.user.user.field_user_name.yml b/config/optional/field.field.user.user.field_user_name.yml new file mode 100644 index 0000000..f6c0205 --- /dev/null +++ b/config/optional/field.field.user.user.field_user_name.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_name + module: + - user +id: user.user.field_user_name +field_name: field_user_name +entity_type: user +bundle: user +label: Nome +description: 'Nome completo do usuário.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/config/optional/field.field.user.user.field_user_phone.yml b/config/optional/field.field.user.user.field_user_phone.yml new file mode 100644 index 0000000..aea3050 --- /dev/null +++ b/config/optional/field.field.user.user.field_user_phone.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_phone + module: + - telephone + - user +id: user.user.field_user_phone +field_name: field_user_phone +entity_type: user +bundle: user +label: Telefone +description: 'Número de telefone do usuário.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: telephone diff --git a/config/optional/field.field.user.user.field_user_photos.yml b/config/optional/field.field.user.user.field_user_photos.yml new file mode 100644 index 0000000..de7f746 --- /dev/null +++ b/config/optional/field.field.user.user.field_user_photos.yml @@ -0,0 +1,30 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.user.field_user_photos + - media.type.image + module: + - media + - user +id: user.user.field_user_photos +field_name: field_user_photos +entity_type: user +bundle: user +label: Fotos +description: 'Fotos do usuário.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:media' + handler_settings: + target_bundles: + image: image + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/config/optional/field.storage.user.field_user_bio.yml b/config/optional/field.storage.user.field_user_bio.yml new file mode 100644 index 0000000..9525d2f --- /dev/null +++ b/config/optional/field.storage.user.field_user_bio.yml @@ -0,0 +1,18 @@ +langcode: pt-br +status: true +dependencies: + module: + - text + - user +id: user.field_user_bio +field_name: field_user_bio +entity_type: user +type: text_long +settings: { } +module: text +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/optional/field.storage.user.field_user_category.yml b/config/optional/field.storage.user.field_user_category.yml new file mode 100644 index 0000000..f1c0057 --- /dev/null +++ b/config/optional/field.storage.user.field_user_category.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + module: + - user +id: user.field_user_category +field_name: field_user_category +entity_type: user +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/optional/field.storage.user.field_user_default_photo.yml b/config/optional/field.storage.user.field_user_default_photo.yml new file mode 100644 index 0000000..2f6b818 --- /dev/null +++ b/config/optional/field.storage.user.field_user_default_photo.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + module: + - media + - user +id: user.field_user_default_photo +field_name: field_user_default_photo +entity_type: user +type: entity_reference +settings: + target_type: media +module: core +locked: false +cardinality: 1 +translatable: false +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/optional/field.storage.user.field_user_dept_code.yml b/config/optional/field.storage.user.field_user_dept_code.yml new file mode 100644 index 0000000..c3e3f8b --- /dev/null +++ b/config/optional/field.storage.user.field_user_dept_code.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + module: + - user +id: user.field_user_dept_code +field_name: field_user_dept_code +entity_type: user +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/optional/field.storage.user.field_user_name.yml b/config/optional/field.storage.user.field_user_name.yml new file mode 100644 index 0000000..b031ddc --- /dev/null +++ b/config/optional/field.storage.user.field_user_name.yml @@ -0,0 +1,20 @@ +langcode: pt-br +status: true +dependencies: + module: + - user +id: user.field_user_name +field_name: field_user_name +entity_type: user +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/optional/field.storage.user.field_user_phone.yml b/config/optional/field.storage.user.field_user_phone.yml new file mode 100644 index 0000000..cba4f1c --- /dev/null +++ b/config/optional/field.storage.user.field_user_phone.yml @@ -0,0 +1,18 @@ +langcode: pt-br +status: true +dependencies: + module: + - telephone + - user +id: user.field_user_phone +field_name: field_user_phone +entity_type: user +type: telephone +settings: { } +module: telephone +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/optional/field.storage.user.field_user_photos.yml b/config/optional/field.storage.user.field_user_photos.yml new file mode 100644 index 0000000..60c8168 --- /dev/null +++ b/config/optional/field.storage.user.field_user_photos.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + module: + - media + - user +id: user.field_user_photos +field_name: field_user_photos +entity_type: user +type: entity_reference +settings: + target_type: media +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/css/site-user-info-block.css b/css/site-user-info-block.css new file mode 100644 index 0000000..996c959 --- /dev/null +++ b/css/site-user-info-block.css @@ -0,0 +1,97 @@ +/** + * @file + * Estilos para o bloco de informações do usuário. + */ + +.site-user-info-block { + display: flex; + gap: 1.5rem; + padding: 1.5rem; + background-color: #f8f9fa; + border-radius: 8px; + margin-bottom: 1.5rem; +} + +.site-user-info-block__photo { + flex-shrink: 0; +} + +.site-user-info-block__image { + width: 120px; + height: 120px; + border-radius: 50%; + object-fit: cover; +} + +.site-user-info-block__no-photo { + width: 120px; + height: 120px; + border-radius: 50%; + background-color: #6c757d; + display: flex; + align-items: center; + justify-content: center; +} + +.site-user-info-block__initials { + font-size: 3rem; + font-weight: bold; + color: #fff; +} + +.site-user-info-block__details { + flex-grow: 1; +} + +.site-user-info-block__name { + margin: 0 0 0.5rem 0; + font-size: 1.5rem; + font-weight: 600; + color: #212529; +} + +.site-user-info-block__category { + font-size: 1.1rem; + color: #495057; + margin-bottom: 0.75rem; +} + +.site-user-info-block__dept, +.site-user-info-block__phone { + font-size: 0.95rem; + color: #6c757d; + margin-bottom: 0.25rem; +} + +.site-user-info-block__label { + font-weight: 500; +} + +.site-user-info-block__phone a { + color: #0d6efd; + text-decoration: none; +} + +.site-user-info-block__phone a:hover { + text-decoration: underline; +} + +.site-user-info-block__bio { + margin-top: 1rem; + font-size: 0.95rem; + color: #495057; + line-height: 1.5; +} + +/* Responsivo */ +@media (max-width: 576px) { + .site-user-info-block { + flex-direction: column; + align-items: center; + text-align: center; + } + + .site-user-info-block__details { + width: 100%; + } +} diff --git a/site_users.info.yml b/site_users.info.yml new file mode 100644 index 0000000..27cd3f4 --- /dev/null +++ b/site_users.info.yml @@ -0,0 +1,11 @@ +name: Site Users +type: module +description: 'Customizações de usuários do site, incluindo campos e templates.' +core_version_requirement: ^10 || ^11 +package: Custom +dependencies: + - drupal:user + - drupal:telephone + - drupal:text + - drupal:media + - drupal:media_library diff --git a/site_users.install b/site_users.install new file mode 100644 index 0000000..8df61ad --- /dev/null +++ b/site_users.install @@ -0,0 +1,266 @@ +getComponent('field_user_name')) { + $form_display->setComponent('field_user_name', [ + 'type' => 'string_textfield', + 'weight' => 10, + 'settings' => [ + 'size' => 60, + 'placeholder' => '', + ], + 'region' => 'content', + ]); + } + + // Campo Telefone. + if (!$form_display->getComponent('field_user_phone')) { + $form_display->setComponent('field_user_phone', [ + 'type' => 'telephone_default', + 'weight' => 11, + 'settings' => [ + 'placeholder' => '', + ], + 'region' => 'content', + ]); + } + + // Campo Categoria. + if (!$form_display->getComponent('field_user_category')) { + $form_display->setComponent('field_user_category', [ + 'type' => 'string_textfield', + 'weight' => 12, + 'settings' => [ + 'size' => 60, + 'placeholder' => '', + ], + 'region' => 'content', + ]); + } + + // Campo Código do Departamento. + if (!$form_display->getComponent('field_user_dept_code')) { + $form_display->setComponent('field_user_dept_code', [ + 'type' => 'string_textfield', + 'weight' => 13, + 'settings' => [ + 'size' => 60, + 'placeholder' => '', + ], + 'region' => 'content', + ]); + } + + // Campo Fotos - Media Library widget. + if (!$form_display->getComponent('field_user_photos')) { + $form_display->setComponent('field_user_photos', [ + 'type' => 'media_library_widget', + 'weight' => 14, + 'settings' => [ + 'media_types' => [], + ], + 'region' => 'content', + ]); + } + + // Campo Biografia. + if (!$form_display->getComponent('field_user_bio')) { + $form_display->setComponent('field_user_bio', [ + 'type' => 'text_textarea', + 'weight' => 15, + 'settings' => [ + 'rows' => 5, + 'placeholder' => '', + ], + 'region' => 'content', + ]); + } + + // Campo Foto Padrão - oculto no form (será gerenciado via hook_form_alter). + $form_display->removeComponent('field_user_default_photo'); + + $form_display->save(); + } + + // Configurar view display para os campos de usuário. + $view_display = EntityViewDisplay::load('user.user.default'); + if ($view_display) { + // Campo Nome. + if (!$view_display->getComponent('field_user_name')) { + $view_display->setComponent('field_user_name', [ + 'type' => 'string', + 'weight' => 10, + 'label' => 'above', + 'settings' => [ + 'link_to_entity' => FALSE, + ], + 'region' => 'content', + ]); + } + + // Campo Telefone. + if (!$view_display->getComponent('field_user_phone')) { + $view_display->setComponent('field_user_phone', [ + 'type' => 'telephone_link', + 'weight' => 11, + 'label' => 'above', + 'settings' => [ + 'title' => '', + ], + 'region' => 'content', + ]); + } + + // Campo Categoria. + if (!$view_display->getComponent('field_user_category')) { + $view_display->setComponent('field_user_category', [ + 'type' => 'string', + 'weight' => 12, + 'label' => 'above', + 'settings' => [ + 'link_to_entity' => FALSE, + ], + 'region' => 'content', + ]); + } + + // Campo Código do Departamento. + if (!$view_display->getComponent('field_user_dept_code')) { + $view_display->setComponent('field_user_dept_code', [ + 'type' => 'string', + 'weight' => 13, + 'label' => 'above', + 'settings' => [ + 'link_to_entity' => FALSE, + ], + 'region' => 'content', + ]); + } + + // Campo Fotos. + if (!$view_display->getComponent('field_user_photos')) { + $view_display->setComponent('field_user_photos', [ + 'type' => 'entity_reference_entity_view', + 'weight' => 14, + 'label' => 'above', + 'settings' => [ + 'view_mode' => 'default', + 'link' => FALSE, + ], + 'region' => 'content', + ]); + } + + // Campo Biografia. + if (!$view_display->getComponent('field_user_bio')) { + $view_display->setComponent('field_user_bio', [ + 'type' => 'text_default', + 'weight' => 15, + 'label' => 'above', + 'settings' => [], + 'region' => 'content', + ]); + } + + // Campo Foto Padrão. + if (!$view_display->getComponent('field_user_default_photo')) { + $view_display->setComponent('field_user_default_photo', [ + 'type' => 'entity_reference_entity_view', + 'weight' => 5, + 'label' => 'hidden', + 'settings' => [ + 'view_mode' => 'default', + 'link' => FALSE, + ], + 'region' => 'content', + ]); + } + + $view_display->save(); + } +} + +/** + * Adiciona o campo field_user_default_photo para seleção de foto padrão. + */ +function site_users_update_10001() { + // Criar field storage se não existir. + if (!FieldStorageConfig::loadByName('user', 'field_user_default_photo')) { + FieldStorageConfig::create([ + 'field_name' => 'field_user_default_photo', + 'entity_type' => 'user', + 'type' => 'entity_reference', + 'settings' => [ + 'target_type' => 'media', + ], + 'cardinality' => 1, + 'translatable' => FALSE, + ])->save(); + } + + // Criar field instance se não existir. + if (!FieldConfig::loadByName('user', 'user', 'field_user_default_photo')) { + FieldConfig::create([ + 'field_name' => 'field_user_default_photo', + 'entity_type' => 'user', + 'bundle' => 'user', + 'label' => 'Foto Padrão', + 'description' => 'Selecione a foto principal do perfil.', + 'required' => FALSE, + 'settings' => [ + 'handler' => 'default:media', + 'handler_settings' => [ + 'target_bundles' => [ + 'image' => 'image', + ], + 'sort' => [ + 'field' => '_none', + 'direction' => 'ASC', + ], + 'auto_create' => FALSE, + 'auto_create_bundle' => '', + ], + ], + ])->save(); + } + + // Configurar view display. + $view_display = EntityViewDisplay::load('user.user.default'); + if ($view_display && !$view_display->getComponent('field_user_default_photo')) { + $view_display->setComponent('field_user_default_photo', [ + 'type' => 'entity_reference_entity_view', + 'weight' => 5, + 'label' => 'hidden', + 'settings' => [ + 'view_mode' => 'default', + 'link' => FALSE, + ], + 'region' => 'content', + ])->save(); + } + + // Ocultar do form display (será gerenciado via hook_form_alter). + $form_display = EntityFormDisplay::load('user.user.default'); + if ($form_display) { + $form_display->removeComponent('field_user_default_photo')->save(); + } + + return t('Campo de foto padrão criado com sucesso.'); +} diff --git a/site_users.libraries.yml b/site_users.libraries.yml new file mode 100644 index 0000000..5c8e96d --- /dev/null +++ b/site_users.libraries.yml @@ -0,0 +1,5 @@ +user-info-block: + version: 1.x + css: + component: + css/site-user-info-block.css: {} diff --git a/site_users.links.menu.yml b/site_users.links.menu.yml new file mode 100644 index 0000000..7b3c349 --- /dev/null +++ b/site_users.links.menu.yml @@ -0,0 +1,6 @@ +site_users.settings: + title: 'Site Users' + description: 'Configurações de campos e fotos de usuários.' + route_name: site_users.settings + parent: site_tools.admin_config + weight: 10 diff --git a/site_users.module b/site_users.module new file mode 100644 index 0000000..207b14f --- /dev/null +++ b/site_users.module @@ -0,0 +1,350 @@ + [ + 'template' => 'user--full', + 'base hook' => 'user', + ], + 'site_users_info_block' => [ + 'template' => 'site-user-info-block', + 'variables' => [ + 'user_info' => [], + 'user' => NULL, + ], + ], + ]; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter() for user templates. + */ +function site_users_theme_suggestions_user_alter(array &$suggestions, array $variables) { + $view_mode = $variables['elements']['#view_mode'] ?? 'default'; + $suggestions[] = 'user__' . $view_mode; +} + +/** + * Implements hook_entity_field_access(). + */ +function site_users_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) { + // Apenas para entidade user. + if ($field_definition->getTargetEntityTypeId() !== 'user') { + return AccessResult::neutral(); + } + + // Lista de campos controlados pelo módulo. + $profile_fields = [ + 'field_user_name', + 'field_user_phone', + 'field_user_category', + 'field_user_dept_code', + 'field_user_bio', + ]; + + $photo_fields = [ + 'field_user_photos', + 'field_user_default_photo', + ]; + $field_name = $field_definition->getName(); + + // Verificar se é um campo de perfil. + if (in_array($field_name, $profile_fields)) { + return site_users_check_profile_field_access($operation, $account, $items); + } + + // Verificar se é um campo de fotos. + if (in_array($field_name, $photo_fields)) { + return site_users_check_photo_field_access($operation, $account, $items); + } + + return AccessResult::neutral(); +} + +/** + * Verifica acesso aos campos de perfil. + */ +function site_users_check_profile_field_access($operation, AccountInterface $account, ?FieldItemListInterface $items = NULL) { + // Administradores têm acesso total. + if ($account->hasPermission('administer site_users settings')) { + return AccessResult::allowed()->cachePerPermissions(); + } + + // Determinar se é o próprio usuário. + $is_own = FALSE; + if ($items) { + $entity = $items->getEntity(); + $is_own = ($entity->id() == $account->id()); + } + + if ($operation === 'view') { + // Pode ver qualquer perfil. + if ($account->hasPermission('view any user profile fields')) { + return AccessResult::allowed()->cachePerPermissions(); + } + // Pode ver apenas o próprio perfil. + if ($is_own && $account->hasPermission('view own user profile fields')) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser(); + } + return AccessResult::forbidden()->cachePerPermissions()->cachePerUser(); + } + + if ($operation === 'edit') { + // Pode editar qualquer perfil. + if ($account->hasPermission('edit any user profile fields')) { + return AccessResult::allowed()->cachePerPermissions(); + } + // Pode editar apenas o próprio perfil. + if ($is_own && $account->hasPermission('edit own user profile fields')) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser(); + } + return AccessResult::forbidden()->cachePerPermissions()->cachePerUser(); + } + + return AccessResult::neutral(); +} + +/** + * Verifica acesso ao campo de fotos. + */ +function site_users_check_photo_field_access($operation, AccountInterface $account, ?FieldItemListInterface $items = NULL) { + // Administradores têm acesso total. + if ($account->hasPermission('administer site_users settings')) { + return AccessResult::allowed()->cachePerPermissions(); + } + + // Determinar se é o próprio usuário. + $is_own = FALSE; + if ($items) { + $entity = $items->getEntity(); + $is_own = ($entity->id() == $account->id()); + } + + if ($operation === 'view') { + // Fotos seguem a mesma regra dos campos de perfil para visualização. + if ($account->hasPermission('view any user profile fields')) { + return AccessResult::allowed()->cachePerPermissions(); + } + if ($is_own && $account->hasPermission('view own user profile fields')) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser(); + } + return AccessResult::forbidden()->cachePerPermissions()->cachePerUser(); + } + + if ($operation === 'edit') { + // Pode gerenciar fotos de qualquer usuário. + if ($account->hasPermission('manage user photos')) { + return AccessResult::allowed()->cachePerPermissions(); + } + // Pode gerenciar apenas as próprias fotos. + if ($is_own && $account->hasPermission('manage own user photos')) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser(); + } + return AccessResult::forbidden()->cachePerPermissions()->cachePerUser(); + } + + return AccessResult::neutral(); +} + +/** + * Implements hook_form_FORM_ID_alter() for user_form. + */ +function site_users_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) { + if (isset($form['field_user_photos'])) { + $form['#validate'][] = 'site_users_validate_photos_count'; + _site_users_add_default_photo_selector($form, $form_state); + } +} + +/** + * Implements hook_form_FORM_ID_alter() for user_register_form. + */ +function site_users_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) { + if (isset($form['field_user_photos'])) { + $form['#validate'][] = 'site_users_validate_photos_count'; + _site_users_add_default_photo_selector($form, $form_state); + } +} + +/** + * Adiciona o seletor de foto padrão ao formulário. + */ +function _site_users_add_default_photo_selector(&$form, FormStateInterface $form_state) { + /** @var \Drupal\user\UserInterface $user */ + $user = $form_state->getFormObject()->getEntity(); + + // Obter fotos atuais do usuário. + $photos = []; + if ($user->hasField('field_user_photos') && !$user->get('field_user_photos')->isEmpty()) { + foreach ($user->get('field_user_photos')->referencedEntities() as $media) { + if ($media instanceof MediaInterface) { + $photos[$media->id()] = $media->label(); + } + } + } + + // Obter foto padrão atual. + $default_photo_id = NULL; + if ($user->hasField('field_user_default_photo') && !$user->get('field_user_default_photo')->isEmpty()) { + $default_photo_id = $user->get('field_user_default_photo')->target_id; + } + + // Adicionar seletor de foto padrão após o campo de fotos. + $form['default_photo_selector'] = [ + '#type' => 'container', + '#weight' => $form['field_user_photos']['#weight'] + 0.5 ?? 15, + '#states' => [ + 'visible' => [ + ':input[name="field_user_photos[selection]"]' => ['filled' => TRUE], + ], + ], + ]; + + if (!empty($photos)) { + $form['default_photo_selector']['field_user_default_photo_select'] = [ + '#type' => 'radios', + '#title' => t('Foto padrão'), + '#description' => t('Selecione a foto principal do perfil.'), + '#options' => $photos, + '#default_value' => $default_photo_id, + ]; + } + else { + $form['default_photo_selector']['field_user_default_photo_select'] = [ + '#type' => 'item', + '#title' => t('Foto padrão'), + '#markup' => t('Adicione fotos para selecionar uma como padrão.'), + ]; + } + + // Adicionar submit handler para salvar a foto padrão. + $form['actions']['submit']['#submit'][] = '_site_users_save_default_photo'; +} + +/** + * Submit handler para salvar a foto padrão selecionada. + */ +function _site_users_save_default_photo(&$form, FormStateInterface $form_state) { + $selected_photo = $form_state->getValue('field_user_default_photo_select'); + + /** @var \Drupal\user\UserInterface $user */ + $user = $form_state->getFormObject()->getEntity(); + + if ($user->hasField('field_user_default_photo')) { + $user->set('field_user_default_photo', $selected_photo); + $user->save(); + } +} + +/** + * Validação customizada para quantidade máxima de fotos. + */ +function site_users_validate_photos_count(&$form, FormStateInterface $form_state) { + $photos = $form_state->getValue('field_user_photos'); + + if (empty($photos)) { + return; + } + + // Contar fotos selecionadas (ignorar valores vazios). + $count = 0; + foreach ($photos as $delta => $photo) { + if (is_numeric($delta) && !empty($photo['target_id'])) { + $count++; + } + } + + // Obter limite configurado. + $config = \Drupal::config('site_users.settings'); + $max_count = $config->get('photos.max_count') ?? 5; + + if ($count > $max_count) { + $form_state->setErrorByName('field_user_photos', t('Você pode adicionar no máximo @max fotos. Atualmente há @count fotos selecionadas.', [ + '@max' => $max_count, + '@count' => $count, + ])); + } +} + +/** + * Implements hook_ENTITY_TYPE_presave() for user entities. + */ +function site_users_user_presave(UserInterface $user) { + // Garantir consistência da foto padrão. + if (!$user->hasField('field_user_photos') || !$user->hasField('field_user_default_photo')) { + return; + } + + // Obter IDs das fotos atuais. + $photo_ids = []; + foreach ($user->get('field_user_photos')->getValue() as $item) { + if (!empty($item['target_id'])) { + $photo_ids[] = $item['target_id']; + } + } + + // Obter foto padrão atual. + $default_photo_id = $user->get('field_user_default_photo')->target_id; + + // Se não há fotos, limpar foto padrão. + if (empty($photo_ids)) { + $user->set('field_user_default_photo', NULL); + return; + } + + // Se a foto padrão não está entre as fotos, selecionar a primeira. + if (empty($default_photo_id) || !in_array($default_photo_id, $photo_ids)) { + $user->set('field_user_default_photo', $photo_ids[0]); + } +} + +/** + * Obtém a foto padrão de um usuário. + * + * @param \Drupal\user\UserInterface $user + * O usuário. + * + * @return \Drupal\media\MediaInterface|null + * A entidade de mídia da foto padrão, ou NULL se não houver. + */ +function site_users_get_default_photo(UserInterface $user): ?MediaInterface { + if (!$user->hasField('field_user_default_photo') || !$user->hasField('field_user_photos')) { + return NULL; + } + + // Tentar obter a foto padrão configurada. + if (!$user->get('field_user_default_photo')->isEmpty()) { + $default_photo = $user->get('field_user_default_photo')->entity; + if ($default_photo instanceof MediaInterface) { + return $default_photo; + } + } + + // Fallback: retornar a primeira foto disponível. + if (!$user->get('field_user_photos')->isEmpty()) { + $first_photo = $user->get('field_user_photos')->first()->entity; + if ($first_photo instanceof MediaInterface) { + return $first_photo; + } + } + + return NULL; +} diff --git a/site_users.permissions.yml b/site_users.permissions.yml new file mode 100644 index 0000000..6ddbf8d --- /dev/null +++ b/site_users.permissions.yml @@ -0,0 +1,30 @@ +administer site_users settings: + title: 'Administrar configurações do Site Users' + description: 'Permite administrar as configurações do módulo Site Users.' + restrict access: true + +view any user profile fields: + title: 'Visualizar campos de perfil de qualquer usuário' + description: 'Permite visualizar os campos customizados (nome, telefone, categoria, etc.) de qualquer usuário.' + +view own user profile fields: + title: 'Visualizar campos do próprio perfil' + description: 'Permite visualizar os próprios campos customizados de perfil.' + +edit any user profile fields: + title: 'Editar campos de perfil de qualquer usuário' + description: 'Permite editar os campos customizados de qualquer usuário.' + restrict access: true + +edit own user profile fields: + title: 'Editar campos do próprio perfil' + description: 'Permite editar os próprios campos customizados de perfil.' + +manage user photos: + title: 'Gerenciar fotos de usuários' + description: 'Permite adicionar, editar e remover fotos de qualquer usuário.' + restrict access: true + +manage own user photos: + title: 'Gerenciar próprias fotos' + description: 'Permite adicionar, editar e remover as próprias fotos de perfil.' diff --git a/site_users.routing.yml b/site_users.routing.yml new file mode 100644 index 0000000..f65f7da --- /dev/null +++ b/site_users.routing.yml @@ -0,0 +1,7 @@ +site_users.settings: + path: '/admin/config/local-modules/site-users' + defaults: + _form: '\Drupal\site_users\Form\SiteUsersSettingsForm' + _title: 'Configurações do Site Users' + requirements: + _permission: 'administer site_users settings' diff --git a/src/Form/SiteUsersSettingsForm.php b/src/Form/SiteUsersSettingsForm.php new file mode 100644 index 0000000..3912562 --- /dev/null +++ b/src/Form/SiteUsersSettingsForm.php @@ -0,0 +1,73 @@ +config('site_users.settings'); + + // Fieldset para configurações de fotos. + $form['photos'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Configurações de Fotos'), + '#collapsible' => FALSE, + ]; + + $form['photos']['photos_max_count'] = [ + '#type' => 'number', + '#title' => $this->t('Quantidade de fotos permitidas'), + '#description' => $this->t('Número máximo de fotos que um usuário pode adicionar ao perfil.'), + '#default_value' => $config->get('photos.max_count') ?? 5, + '#min' => 1, + '#max' => 100, + '#required' => TRUE, + ]; + + $form['photos']['photos_ldap_attribute'] = [ + '#type' => 'textfield', + '#title' => $this->t('Atributo LDAP da foto'), + '#description' => $this->t('Se LDAP estiver habilitado, informe o nome do atributo que contém a foto do usuário (ex: thumbnailPhoto, jpegPhoto).'), + '#default_value' => $config->get('photos.ldap_attribute') ?? 'jpegPhoto', + '#maxlength' => 255, + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('site_users.settings') + ->set('photos.max_count', $form_state->getValue('photos_max_count')) + ->set('photos.ldap_attribute', $form_state->getValue('photos_ldap_attribute')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/src/Plugin/Block/UserInfoBlock.php b/src/Plugin/Block/UserInfoBlock.php new file mode 100644 index 0000000..95542b7 --- /dev/null +++ b/src/Plugin/Block/UserInfoBlock.php @@ -0,0 +1,192 @@ +routeMatch = $route_match; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function build() { + $user = $this->getUserFromContext(); + + if (!$user instanceof UserInterface) { + return []; + } + + // Obter foto padrão. + $default_photo = NULL; + $default_photo_url = NULL; + if (function_exists('site_users_get_default_photo')) { + $default_photo = site_users_get_default_photo($user); + if ($default_photo) { + // Usar o source field do media (forma padrão do Drupal). + $source_field = $default_photo->getSource()->getConfiguration()['source_field']; + if ($default_photo->hasField($source_field) && !$default_photo->get($source_field)->isEmpty()) { + $file = $default_photo->get($source_field)->entity; + if ($file) { + $default_photo_url = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri()); + } + } + } + } + + // Coletar informações do usuário. + $user_info = [ + 'uid' => $user->id(), + 'username' => $user->getDisplayName(), + 'name' => $this->getFieldValue($user, 'field_user_name'), + 'phone' => $this->getFieldValue($user, 'field_user_phone'), + 'category' => $this->getFieldValue($user, 'field_user_category'), + 'dept_code' => $this->getFieldValue($user, 'field_user_dept_code'), + 'bio' => $this->getFieldValue($user, 'field_user_bio'), + 'photo_url' => $default_photo_url, + 'photo_alt' => $default_photo ? $default_photo->label() : '', + ]; + + return [ + '#theme' => 'site_users_info_block', + '#user_info' => $user_info, + '#user' => $user, + '#attached' => [ + 'library' => [ + 'site_users/user-info-block', + ], + ], + ]; + } + + /** + * Obtém o usuário da rota atual. + * + * @return \Drupal\user\UserInterface|null + * O usuário ou NULL se não estiver em uma página de usuário. + */ + protected function getUserFromContext(): ?UserInterface { + // Obter da rota /user/{user}. + $user = $this->routeMatch->getParameter('user'); + + if ($user instanceof UserInterface) { + return $user; + } + + // Se for apenas o ID, carregar o usuário. + if (is_numeric($user)) { + return $this->entityTypeManager->getStorage('user')->load($user); + } + + return NULL; + } + + /** + * Obtém o valor de um campo do usuário. + * + * @param \Drupal\user\UserInterface $user + * O usuário. + * @param string $field_name + * O nome do campo. + * + * @return string|null + * O valor do campo ou NULL. + */ + protected function getFieldValue(UserInterface $user, string $field_name): ?string { + if ($user->hasField($field_name) && !$user->get($field_name)->isEmpty()) { + return $user->get($field_name)->value; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + $tags = parent::getCacheTags(); + + $user = $this->getUserFromContext(); + if ($user instanceof UserInterface) { + $tags = Cache::mergeTags($tags, $user->getCacheTags()); + } + + return $tags; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return Cache::mergeContexts(parent::getCacheContexts(), ['route', 'user']); + } + +} diff --git a/templates/site-user-info-block.html.twig b/templates/site-user-info-block.html.twig new file mode 100644 index 0000000..d32aad4 --- /dev/null +++ b/templates/site-user-info-block.html.twig @@ -0,0 +1,64 @@ +{# +/** + * @file + * Template para o bloco de informações do usuário. + * + * Variáveis disponíveis: + * - user_info: Array com informações do usuário: + * - uid: ID do usuário + * - username: Nome de usuário (display name) + * - name: Nome completo + * - phone: Telefone + * - category: Categoria + * - dept_code: Código do departamento + * - bio: Biografia + * - photo_url: URL da foto padrão + * - photo_alt: Texto alternativo da foto + * - user: Entidade do usuário. + */ +#} + diff --git a/templates/user--full.html.twig b/templates/user--full.html.twig new file mode 100644 index 0000000..0bdd028 --- /dev/null +++ b/templates/user--full.html.twig @@ -0,0 +1,18 @@ +{# +/** + * @file + * Template para página de perfil de usuário (view mode: full). + * + * Variáveis disponíveis: + * - content: Campos do usuário renderizados. + * - user: Objeto do usuário. + * - attributes: Atributos HTML do elemento wrapper. + * + * @see template_preprocess_user() + */ +#} + + {% if content %} + {{ content }} + {% endif %} +