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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Tag;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* Per-tenant tag CRUD, scoped to the caller's doctor/clinic entity.
|
||||
*/
|
||||
class TenantTagTest extends ApiTestCase
|
||||
{
|
||||
private function doctorUser(): array
|
||||
{
|
||||
$user = $this->createUser(['ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($user, 'دکتر');
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
return [$user, $doctor];
|
||||
}
|
||||
|
||||
public function testCreateListUpdateDelete(): void
|
||||
{
|
||||
[$user] = $this->doctorUser();
|
||||
|
||||
// create
|
||||
$created = $this->authJson('POST', '/api/v1/tenant-tag', $user, [
|
||||
'name' => 'فوری', 'color' => '#FF0000',
|
||||
]);
|
||||
self::assertSame(201, $this->responseCode());
|
||||
self::assertSame('فوری', $created['data']['name']);
|
||||
self::assertSame('#FF0000', $created['data']['color']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
// list
|
||||
$list = $this->authJson('GET', '/api/v1/tenant-tags', $user);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('فوری', $list['data'][0]['name']);
|
||||
|
||||
// update
|
||||
$this->authJson('PATCH', '/api/v1/tenant-tag/' . $uuid, $user, [
|
||||
'name' => 'مهم', 'color' => '#00AA00', 'active' => false,
|
||||
]);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
// delete
|
||||
$this->authJson('DELETE', '/api/v1/tenant-tag/' . $uuid, $user);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$after = $this->authJson('GET', '/api/v1/tenant-tags', $user);
|
||||
self::assertCount(0, $after['data']);
|
||||
}
|
||||
|
||||
public function testRejectsInvalidNameAndColor(): void
|
||||
{
|
||||
[$user] = $this->doctorUser();
|
||||
|
||||
$this->authJson('POST', '/api/v1/tenant-tag', $user, ['name' => '', 'color' => '#FF0000']);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
|
||||
$this->authJson('POST', '/api/v1/tenant-tag', $user, ['name' => 'ok', 'color' => 'red']);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testCannotTouchAnotherTenantsTag(): void
|
||||
{
|
||||
[$ownerA] = $this->doctorUser();
|
||||
$created = $this->authJson('POST', '/api/v1/tenant-tag', $ownerA, ['name' => 'مال A', 'color' => '#123456']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
[$ownerB] = $this->doctorUser();
|
||||
$this->authJson('PATCH', '/api/v1/tenant-tag/' . $uuid, $ownerB, ['name' => 'دزدی']);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
|
||||
$this->authJson('DELETE', '/api/v1/tenant-tag/' . $uuid, $ownerB);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user