feat: import users` real names from bpn API

This commit is contained in:
Björn Fromme
2026-08-17 14:24:56 +02:00
parent bf69fcceff
commit 77de1643f0
11 changed files with 96 additions and 7 deletions
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260817090000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Store the BusPro first and last name on the user';
}
public function up(Schema $schema): void
{
// Left empty on purpose: the names are synced from BPN on the next login of each account.
$this->addSql('ALTER TABLE user ADD first_name VARCHAR(100) DEFAULT NULL, ADD last_name VARCHAR(100) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE user DROP first_name, DROP last_name');
}
}
@@ -129,7 +129,13 @@ class PersonalDataController extends AbstractController
// Update profile completeness flag on user entity
$isComplete = $this->completenessChecker->isComplete($personalData);
$user->setProfileComplete($isComplete);
$user
->setProfileComplete($isComplete)
// Otherwise a rename here would stay invisible in the backend until the next
// login, which is the only other place the name is synced from BPN.
->setFirstName($personalData->firstName)
->setLastName($personalData->name)
;
$this->entityManager->flush();
$this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert');
+46
View File
@@ -27,6 +27,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $addressId = null;
#[ORM\Column(type: 'string', length: 100, nullable: true)]
private ?string $firstName = null;
#[ORM\Column(type: 'string', length: 100, nullable: true)]
private ?string $lastName = null;
/**
* @var array<string>
*/
@@ -91,6 +97,46 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(?string $firstName): static
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(?string $lastName): static
{
$this->lastName = $lastName;
return $this;
}
public function getFullName(): ?string
{
$fullName = trim(sprintf('%s %s', $this->firstName, $this->lastName));
return '' !== $fullName ? $fullName : null;
}
/**
* The name to identify this account by in the UI. BusPro only fills the name in on login, so
* accounts that have not signed in since the columns were added fall back to their e-mail.
*/
public function getDisplayName(): string
{
return $this->getFullName() ?? (string) $this->email;
}
public function getPassword(): ?string
{
return base64_decode($this->password);
@@ -57,7 +57,7 @@ final readonly class AccommodationBookingFilterOptionsProvider
$managers[(int) $manager->getId()] = $manager;
}
uasort($managers, static fn (User $a, User $b) => strcasecmp((string) $a->getEmail(), (string) $b->getEmail()));
uasort($managers, static fn (User $a, User $b) => strcasecmp($a->getDisplayName(), $b->getDisplayName()));
return array_values($managers);
}
@@ -63,7 +63,7 @@ class AccommodationBookingFilterType extends AbstractListFilterType
'label' => 'Betreuer:in',
'class' => User::class,
'choices' => $options['managers'],
'choice_label' => 'email',
'choice_label' => 'displayName',
'placeholder' => 'alle',
'required' => false,
]];
@@ -130,7 +130,7 @@ class AccommodationBookingType extends AbstractType
'class' => User::class,
'required' => false,
'choices' => $options['assignable_managers'],
'choice_label' => 'email',
'choice_label' => 'displayName',
'placeholder' => 'keine Zuordnung',
'label' => 'Bearbeiter:in',
]);
@@ -81,7 +81,7 @@ class AccommodationBookingFilterDto extends AbstractListFilterDto
} elseif (null !== $this->managedBy) {
$chips[] = new ListFilterChip(
'Betreuer:in',
(string) $this->managedBy->getEmail(),
$this->managedBy->getDisplayName(),
$this->managedByLocked ? [] : ['managedBy'],
);
}
+2
View File
@@ -29,6 +29,8 @@ class UserRepository extends ServiceEntityRepository
$this->applySearchTerm($qb, $filter->searchTerm(), [
'user.email',
'user.firstName',
'user.lastName',
'user.addressId',
'user.personId',
]);
+2
View File
@@ -119,6 +119,8 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
->setPassword($encryptedPassword)
->setPersonId($personalData->personId)
->setAddressId($personalData->addressId)
->setFirstName($personalData->firstName)
->setLastName($personalData->name)
->setLastLoginAt(new \DateTimeImmutable())
->setProfileComplete($this->completenessChecker->isComplete($personalData))
;
@@ -60,7 +60,7 @@
</td>
<td>
{% if booking.managedBy is not null %}
{{ booking.managedBy.email }}
{{ booking.managedBy.displayName }}
{% else %}
keine Zuordnung
{% endif %}
+7 -1
View File
@@ -13,6 +13,9 @@
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Name', 'user.lastName') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'E-Mail', 'user.email') }}
</th>
@@ -37,6 +40,9 @@
<tbody>
{% for user in pagination %}
<tr>
<td>
{{ user.fullName | default('-') }}
</td>
<td>
{{ user.email }}
</td>
@@ -65,7 +71,7 @@
</tr>
{% else %}
<tr>
<td colspan="7">
<td colspan="8">
{{ filter.isActive ? 'Keine Treffer für diesen Filter.' : 'Keine Daten...' }}
</td>
</tr>