feat: implement admin API for user and representation management

- Updated UsersPage to fetch users from the new admin endpoint.
- Enhanced user data structure to include 'name' and modified rendering logic.
- Added RepresentationDetailPage for detailed representation management.
- Created AdminApiController to handle user and representation CRUD operations.
- Implemented pagination and search functionality for users and representations.
- Updated user and representation data models to reflect new API structure.
This commit is contained in:
hamed
2026-06-09 23:41:44 +03:30
parent f619449167
commit 147a2a894e
20 changed files with 1067 additions and 228 deletions
+522
View File
@@ -0,0 +1,522 @@
<?php
namespace App\Admin\Controller;
use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Payment\Entity\Payment;
use App\Rating\Entity\Comment;
use App\Rating\Entity\Rate;
use App\Representation\Entity\Representation;
use App\Secretary\Entity\DoctorSecretary;
use App\Settlement\Entity\Settlement;
use App\Sms\Entity\SmsLog;
use App\Sms\Entity\SmsTemplate;
use App\Shared\Controller\BaseController;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')]
class AdminApiController extends BaseController
{
public function __construct(
private readonly EntityManagerInterface $em,
) {}
// ── Users ─────────────────────────────────────────────────────────────────
#[Route('/api/v1/admin/users', methods: ['GET'])]
public function users(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$qb = $this->em->createQueryBuilder()
->select('u.uuid, u.id, u.mobileNumber as mobile, u.realName as name, u.email, u.roles, u.status, u.createdAt')
->from(User::class, 'u')
->orderBy('u.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('u.mobileNumber LIKE :s OR u.realName LIKE :s OR u.email LIKE :s')
->setParameter('s', '%' . $search . '%');
}
$total = (clone $qb)->select('COUNT(u.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $u) => [
'uuid' => $u['uuid'],
'id' => $u['id'],
'mobile_number' => $u['mobile'],
'name' => $u['name'],
'email' => $u['email'],
'roles' => $u['roles'],
'is_active' => $u['status'] === 1,
'created_at' => date('c', (int) $u['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Appointments ──────────────────────────────────────────────────────────
#[Route('/api/v1/admin/appointments', methods: ['GET'])]
public function appointments(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$status = trim((string) $request->query->get('status', ''));
$qb = $this->em->createQueryBuilder()
->select(
'a.uuid, a.slotStart, a.slotEnd, a.status, a.createdAt',
'd.uuid as doctor_uuid, d.name as doctor_name',
'u.mobileNumber as patient_mobile, u.realName as patient_name',
)
->from(Appointment::class, 'a')
->join('a.doctor', 'd')
->join('a.user', 'u')
->orderBy('a.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('u.mobileNumber LIKE :s OR d.name LIKE :s')
->setParameter('s', '%' . $search . '%');
}
if ($status !== '') {
$qb->andWhere('a.status = :status')->setParameter('status', $status);
}
$total = (clone $qb)->select('COUNT(a.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $a) => [
'uuid' => $a['uuid'],
'patient_name' => $a['patient_name'] ?? '',
'patient_mobile' => $a['patient_mobile'],
'doctor_name' => $a['doctor_name'],
'appointment_date' => date('Y-m-d', (int) $a['slotStart']),
'appointment_time' => date('H:i', (int) $a['slotStart']),
'status' => $a['status'],
'amount' => 0,
'created_at' => date('c', (int) $a['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Payments ──────────────────────────────────────────────────────────────
#[Route('/api/v1/admin/payments', methods: ['GET'])]
public function payments(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$status = trim((string) $request->query->get('status', ''));
$qb = $this->em->createQueryBuilder()
->select(
'p.uuid, p.amountRials, p.status, p.gateway, p.referenceId, p.createdAt, p.updatedAt',
'u.mobileNumber as patient_mobile',
)
->from(Payment::class, 'p')
->join('p.user', 'u')
->orderBy('p.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('u.mobileNumber LIKE :s OR p.referenceId LIKE :s OR p.orderId LIKE :s')
->setParameter('s', '%' . $search . '%');
}
if ($status !== '') {
$qb->andWhere('p.status = :status')->setParameter('status', $status);
}
$total = (clone $qb)->select('COUNT(p.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $p) => [
'uuid' => $p['uuid'],
'amount' => (int) $p['amountRials'],
'status' => $p['status'],
'gateway' => $p['gateway'],
'ref_id' => $p['referenceId'],
'patient_mobile' => $p['patient_mobile'],
'paid_at' => $p['status'] === 'success' ? date('c', (int) $p['updatedAt']) : null,
'created_at' => date('c', (int) $p['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Representations ───────────────────────────────────────────────────────
#[Route('/api/v1/admin/representations', methods: ['GET'])]
public function representations(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$qb = $this->em->createQueryBuilder()
->select('r.uuid, r.fullName, r.mobileNumber, r.city, r.commissionPercent, r.active, r.createdAt')
->from(Representation::class, 'r')
->orderBy('r.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('r.fullName LIKE :s OR r.city LIKE :s')
->setParameter('s', '%' . $search . '%');
}
$total = (clone $qb)->select('COUNT(r.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $r) => [
'uuid' => $r['uuid'],
'domain' => $r['fullName'],
'full_name' => $r['fullName'],
'mobile_number' => $r['mobileNumber'],
'city' => $r['city'] ?? '',
'commission_percent' => (float) $r['commissionPercent'],
'wallet_balance' => 0,
'is_active' => (bool) $r['active'],
'created_at' => date('c', (int) $r['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Secretaries ───────────────────────────────────────────────────────────
#[Route('/api/v1/admin/secretaries', methods: ['GET'])]
public function secretaries(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$qb = $this->em->createQueryBuilder()
->select(
'ds.uuid, ds.permissions, ds.active, ds.createdAt',
'u.mobileNumber as mobile, u.realName as user_name',
'd.name as doctor_name, d.uuid as doctor_uuid',
)
->from(DoctorSecretary::class, 'ds')
->join('ds.doctor', 'd')
->join('ds.secretary', 'u')
->orderBy('ds.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('u.mobileNumber LIKE :s OR u.realName LIKE :s OR d.name LIKE :s')
->setParameter('s', '%' . $search . '%');
}
$total = (clone $qb)->select('COUNT(ds.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $ds) => [
'uuid' => $ds['uuid'],
'user_name' => $ds['user_name'] ?? $ds['mobile'],
'mobile_number' => $ds['mobile'],
'doctor_name' => $ds['doctor_name'],
'doctor_uuid' => $ds['doctor_uuid'],
'is_active' => (bool) $ds['active'],
'permissions' => $ds['permissions'] ?? DoctorSecretary::DEFAULT_PERMISSIONS,
'created_at' => date('c', (int) $ds['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Ratings ───────────────────────────────────────────────────────────────
#[Route('/api/v1/admin/rates', methods: ['GET'])]
public function rates(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$qb = $this->em->createQueryBuilder()
->select(
'r.uuid, r.score, r.createdAt',
'u.realName as patient_name, u.mobileNumber as patient_mobile',
'd.name as doctor_name',
)
->from(Rate::class, 'r')
->join('r.doctor', 'd')
->join('r.user', 'u')
->orderBy('r.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('d.name LIKE :s OR u.realName LIKE :s')
->setParameter('s', '%' . $search . '%');
}
$total = (clone $qb)->select('COUNT(r.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $r) => [
'uuid' => $r['uuid'],
'patient_name' => $r['patient_name'] ?? $r['patient_mobile'],
'doctor_name' => $r['doctor_name'],
'overall' => (int) $r['score'],
'score' => (int) $r['score'],
'created_at' => date('c', (int) $r['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Comments ──────────────────────────────────────────────────────────────
#[Route('/api/v1/admin/comments', methods: ['GET'])]
public function comments(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$status = trim((string) $request->query->get('status', ''));
$qb = $this->em->createQueryBuilder()
->select(
'c.uuid, c.body, c.status, c.createdAt',
'u.realName as patient_name',
'd.name as doctor_name',
)
->from(Comment::class, 'c')
->join('c.doctor', 'd')
->join('c.user', 'u')
->orderBy('c.createdAt', 'DESC');
if ($search !== '') {
$qb->andWhere('d.name LIKE :s OR c.body LIKE :s OR u.realName LIKE :s')
->setParameter('s', '%' . $search . '%');
}
if ($status !== '') {
$qb->andWhere('c.status = :status')->setParameter('status', $status);
}
$total = (clone $qb)->select('COUNT(c.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $c) => [
'uuid' => $c['uuid'],
'patient_name' => $c['patient_name'] ?? '',
'doctor_name' => $c['doctor_name'],
'title' => mb_substr($c['body'], 0, 50),
'body' => $c['body'],
'is_approved' => $c['status'] === 'approved',
'status' => $c['status'],
'created_at' => date('c', (int) $c['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── SMS Logs ──────────────────────────────────────────────────────────────
#[Route('/api/v1/admin/sms/logs', methods: ['GET'])]
public function smsLogs(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$qb = $this->em->createQueryBuilder()
->select('s.uuid, s.mobile, s.message, s.provider, s.success, s.createdAt')
->from(SmsLog::class, 's')
->orderBy('s.createdAt', 'DESC');
$total = (clone $qb)->select('COUNT(s.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $l) => [
'uuid' => $l['uuid'],
'recipient' => $l['mobile'],
'message' => $l['message'],
'status' => $l['success'] ? 'sent' : 'failed',
'provider' => $l['provider'],
'sent_at' => date('c', (int) $l['createdAt']),
'created_at' => date('c', (int) $l['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Settlements ───────────────────────────────────────────────────────────
#[Route('/api/v1/admin/settlements', methods: ['GET'])]
public function settlements(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$status = trim((string) $request->query->get('status', ''));
$qb = $this->em->createQueryBuilder()
->select(
's.uuid, s.amountRials, s.status, s.bankAccount, s.adminNote, s.reviewedAt, s.createdAt',
'u.realName as user_name, u.mobileNumber as user_mobile',
)
->from(Settlement::class, 's')
->join('s.user', 'u')
->orderBy('s.createdAt', 'DESC');
if ($status !== '') {
$qb->andWhere('s.status = :status')->setParameter('status', $status);
}
$total = (clone $qb)->select('COUNT(s.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $s) => [
'uuid' => $s['uuid'],
'representation_name' => $s['user_name'] ?? $s['user_mobile'],
'amount' => (int) $s['amountRials'],
'status' => $s['status'],
'bank_card' => $s['bankAccount']['card'] ?? null,
'bank_name' => $s['bankAccount']['bank_name'] ?? null,
'reject_reason' => $s['adminNote'],
'requested_at' => date('c', (int) $s['createdAt']),
'processed_at' => $s['reviewedAt'] ? date('c', (int) $s['reviewedAt']) : null,
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── SMS Templates (paginated list with optional status filter) ────────────
#[Route('/api/v1/admin/sms/sample-templates', methods: ['GET'])]
public function smsSampleTemplates(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$status = trim((string) $request->query->get('status', 'approved'));
$qb = $this->em->createQueryBuilder()
->select('t.uuid, t.name, t.body, t.providerCode, t.status, t.adminNote, t.createdAt')
->from(SmsTemplate::class, 't')
->orderBy('t.createdAt', 'DESC');
if ($status !== '') {
$qb->where('t.status = :status')->setParameter('status', $status);
}
$total = (clone $qb)->select('COUNT(t.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $t) => [
'uuid' => $t['uuid'],
'name' => $t['name'],
'body' => $t['body'],
'provider_code' => $t['providerCode'],
'status' => $t['status'],
'admin_note' => $t['adminNote'],
'created_at' => date('c', (int) $t['createdAt']),
], $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Dashboard Stats ───────────────────────────────────────────────────────
#[Route('/api/v1/admin/dashboard/stats', methods: ['GET'])]
public function dashboardStats(): JsonResponse
{
$em = $this->em;
$todayStart = mktime(0, 0, 0);
$todayEnd = mktime(23, 59, 59);
$totalUsers = (int) $em->createQueryBuilder()
->select('COUNT(u.id)')->from(User::class, 'u')
->getQuery()->getSingleScalarResult();
$activeDoctors = (int) $em->createQueryBuilder()
->select('COUNT(d.id)')->from(Doctor::class, 'd')
->where('d.activeDoctorAppointment = true')
->getQuery()->getSingleScalarResult();
$totalDoctors = (int) $em->createQueryBuilder()
->select('COUNT(d.id)')->from(Doctor::class, 'd')
->getQuery()->getSingleScalarResult();
$totalClinics = (int) $em->createQueryBuilder()
->select('COUNT(c.id)')->from(Clinic::class, 'c')
->getQuery()->getSingleScalarResult();
$todayAppointments = (int) $em->createQueryBuilder()
->select('COUNT(a.id)')->from(Appointment::class, 'a')
->where('a.slotStart BETWEEN :s AND :e')
->setParameter('s', $todayStart)->setParameter('e', $todayEnd)
->getQuery()->getSingleScalarResult();
$totalAppointments = (int) $em->createQueryBuilder()
->select('COUNT(a.id)')->from(Appointment::class, 'a')
->getQuery()->getSingleScalarResult();
$todayPaymentsResult = $em->createQueryBuilder()
->select('COUNT(p.id) as cnt, COALESCE(SUM(p.amountRials), 0) as total')
->from(Payment::class, 'p')
->where('p.status = :s AND p.createdAt BETWEEN :ts AND :te')
->setParameter('s', Payment::STATUS_SUCCESS)
->setParameter('ts', $todayStart)->setParameter('te', $todayEnd)
->getQuery()->getSingleResult();
$totalPaymentsResult = $em->createQueryBuilder()
->select('COUNT(p.id) as cnt, COALESCE(SUM(p.amountRials), 0) as total')
->from(Payment::class, 'p')
->where('p.status = :s')
->setParameter('s', Payment::STATUS_SUCCESS)
->getQuery()->getSingleResult();
$pendingComments = (int) $em->createQueryBuilder()
->select('COUNT(c.id)')->from(Comment::class, 'c')
->where('c.status = :s')->setParameter('s', Comment::STATUS_PENDING)
->getQuery()->getSingleScalarResult();
$pendingSettlements = (int) $em->createQueryBuilder()
->select('COUNT(s.id)')->from(Settlement::class, 's')
->where('s.status = :s')->setParameter('s', Settlement::STATUS_PENDING)
->getQuery()->getSingleScalarResult();
return $this->success([
'total_users' => $totalUsers,
'active_doctors' => $activeDoctors,
'total_doctors' => $totalDoctors,
'total_clinics' => $totalClinics,
'today_appointments' => $todayAppointments,
'total_appointments' => $totalAppointments,
'today_payments_count' => (int) $todayPaymentsResult['cnt'],
'today_payments_amount' => (int) $todayPaymentsResult['total'],
'total_payments_amount' => (int) $totalPaymentsResult['total'],
'pending_comments' => $pendingComments,
'pending_settlements' => $pendingSettlements,
]);
}
}