feat(settings): complete remaining settings tabs (account, tags, turns)

Fill the three previously-placeholder settings sections so every menu item
is now a real page inside the settings shell:

- حساب کاربری: new authenticated POST /api/v1/user/change-password
  (verifies current password, ≥8 chars, must differ) + account page with a
  profile summary and change-password form.
- برچسب‌ها: new per-tenant TenantTag domain (entity/repo/controller +
  migration) with tenant-scoped CRUD at /api/v1/tenant-tag(s), plus a tags
  management page (list + color + add/edit/delete).
- مدیریت نوبت دهی: export the existing WeeklyScheduleTab from
  DoctorDetailPage and reuse it in a standalone AppointmentSettingsPage
  (current doctor's uuid + addresses).

Wire all three menu entries to their routes. Backend covered by PHPUnit
(change-password, tenant-tag CRUD + ownership); FE covered by Vitest.
API docs updated (auth.md, tag.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-13 14:16:21 +03:30
co-authored by Claude Opus 4.8
parent 19d8560c9e
commit 76f9fbe88f
20 changed files with 1004 additions and 14 deletions
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Tests\Auth;
use App\Auth\Entity\User;
use App\Tests\ApiTestCase;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* POST /api/v1/user/change-password — authenticated password change.
*/
class ChangePasswordTest extends ApiTestCase
{
private function userWithPassword(string $password): User
{
$user = $this->createUser(['ROLE_DOCTOR']);
$hasher = static::getContainer()->get(UserPasswordHasherInterface::class);
$user->setPasswordHash($hasher->hashPassword($user, $password));
$this->em->flush();
return $user;
}
public function testChangesPasswordWithCorrectCurrent(): void
{
$user = $this->userWithPassword('oldpass12');
$this->authJson('POST', '/api/v1/user/change-password', $user, [
'current_password' => 'oldpass12',
'new_password' => 'newpass34',
]);
self::assertSame(200, $this->responseCode());
$hasher = static::getContainer()->get(UserPasswordHasherInterface::class);
$this->em->clear();
$reloaded = $this->em->getRepository(User::class)->find($user->getId());
self::assertTrue($hasher->isPasswordValid($reloaded, 'newpass34'));
}
public function testRejectsWrongCurrentPassword(): void
{
$user = $this->userWithPassword('oldpass12');
$this->authJson('POST', '/api/v1/user/change-password', $user, [
'current_password' => 'wrongpass',
'new_password' => 'newpass34',
]);
self::assertSame(422, $this->responseCode());
}
public function testRejectsShortNewPassword(): void
{
$user = $this->userWithPassword('oldpass12');
$this->authJson('POST', '/api/v1/user/change-password', $user, [
'current_password' => 'oldpass12',
'new_password' => 'short',
]);
self::assertSame(422, $this->responseCode());
}
public function testRejectsSameAsCurrent(): void
{
$user = $this->userWithPassword('oldpass12');
$this->authJson('POST', '/api/v1/user/change-password', $user, [
'current_password' => 'oldpass12',
'new_password' => 'oldpass12',
]);
self::assertSame(422, $this->responseCode());
}
}