userRepo->findByMobile($mobile); if ($user === null) { $user = new User($mobile); if (!empty($password)) { $user->setPasswordHash($this->hasher->hashPassword($user, $password)); } } if (!empty($name)) { $user->setRealName(trim($name)); } $roles = $user->getRoles(); if (!in_array('ROLE_SECRETARY', $roles, true)) { $roles[] = 'ROLE_SECRETARY'; $user->setRoles(array_values(array_unique($roles))); } $this->userRepo->save($user); return $user; } public function doctorAtSecretaryLimit(Doctor $doctor): bool { $limit = $this->subscriptionService->getSecretaryLimit('doctor', $doctor->getId()); return $this->secretaryRepo->countActiveByDoctor($doctor) >= $limit; } /** * Assign a secretary (by mobile) to several clinic doctors atomically. * * @param string[] $doctorUuids * @return array{secretary: User, created: DoctorSecretary[], skipped_duplicate: string[], skipped_limit: string[], skipped_not_in_clinic: string[]} */ public function assignToClinicDoctors(Clinic $clinic, string $mobile, array $doctorUuids, array $meta = []): array { $secretary = $this->resolveSecretaryUser($mobile, $meta['name'] ?? null, $meta['password'] ?? null); $created = $skippedDup = $skippedLimit = $skippedNotInClinic = []; foreach (array_unique($doctorUuids) as $doctorUuid) { $doctor = $this->doctorRepo->findByUuid($doctorUuid); if ($doctor === null || !$this->secretaryRepo->isDoctorInClinic($doctor, $clinic)) { $skippedNotInClinic[] = $doctorUuid; continue; } $existing = $this->secretaryRepo->findOneBy([ 'doctor' => $doctor, 'secretary' => $secretary, 'entityType' => DoctorSecretary::OWNER_CLINIC, ]); if ($existing !== null) { if ($existing->isActive()) { $skippedDup[] = $doctorUuid; continue; } $existing->setActive(true); $this->applyMeta($existing, $meta); $created[] = $existing; continue; } if ($this->doctorAtSecretaryLimit($doctor)) { $skippedLimit[] = $doctorUuid; continue; } $row = new DoctorSecretary($doctor, $secretary, $clinic); $this->applyMeta($row, $meta); $this->secretaryRepo->save($row, false); $created[] = $row; } $this->secretaryRepo->getEntityManager()->flush(); // flush the batch of new rows if (!empty($created)) { $this->sendWelcomeSms($mobile, $clinic->getName() ?? 'کلینیک'); } return [ 'secretary' => $secretary, 'created' => $created, 'skipped_duplicate' => $skippedDup, 'skipped_limit' => $skippedLimit, 'skipped_not_in_clinic' => $skippedNotInClinic, ]; } /** * Re-sync a clinic secretary's assigned doctors to exactly $doctorUuids: * activate/create the wanted set, deactivate the rest. * * @param string[] $doctorUuids * @return array{added: DoctorSecretary[], removed: DoctorSecretary[], skipped_limit: string[], skipped_not_in_clinic: string[]} */ public function syncClinicDoctors(Clinic $clinic, User $secretary, array $doctorUuids): array { $wanted = array_values(array_unique($doctorUuids)); $existing = $this->secretaryRepo->findByClinicAndSecretary($clinic, $secretary); $byDoctorUuid = []; foreach ($existing as $row) { $byDoctorUuid[$row->getDoctor()->getUuid()] = $row; } $added = $removed = $skippedLimit = $skippedNotInClinic = []; // New links inherit the person's existing profile/permissions so that a // multi-doctor secretary stays a single consistent record. $template = $existing[0] ?? null; foreach ($wanted as $doctorUuid) { $doctor = $this->doctorRepo->findByUuid($doctorUuid); if ($doctor === null || !$this->secretaryRepo->isDoctorInClinic($doctor, $clinic)) { $skippedNotInClinic[] = $doctorUuid; continue; } $current = $byDoctorUuid[$doctorUuid] ?? null; if ($current !== null) { if (!$current->isActive()) { $current->setActive(true); $added[] = $current; } continue; } if ($this->doctorAtSecretaryLimit($doctor)) { $skippedLimit[] = $doctorUuid; continue; } $row = new DoctorSecretary($doctor, $secretary, $clinic); if ($template !== null) { $row->setNationalCode($template->getNationalCode()) ->setAddress($template->getAddress()) ->setPermissions($template->getPermissions()); } $this->secretaryRepo->save($row, false); $added[] = $row; } foreach ($existing as $row) { if ($row->isActive() && !in_array($row->getDoctor()->getUuid(), $wanted, true)) { $row->setActive(false); $removed[] = $row; } } $this->secretaryRepo->getEntityManager()->flush(); return [ 'added' => $added, 'removed' => $removed, 'skipped_limit' => $skippedLimit, 'skipped_not_in_clinic' => $skippedNotInClinic, ]; } private function applyMeta(DoctorSecretary $row, array $meta): void { if (array_key_exists('national_code', $meta)) { $row->setNationalCode($meta['national_code'] !== null ? trim((string) $meta['national_code']) : null); } if (array_key_exists('address', $meta)) { $row->setAddress($meta['address'] !== null ? trim((string) $meta['address']) : null); } if (!empty($meta['permissions'])) { $row->mergePermissions($meta['permissions']); } } private function sendWelcomeSms(string $mobile, string $ownerName): void { $this->smsService->dispatchTemplate(SmsLog::TAG_SECRETARY, $mobile, [ 'owner' => $ownerName, 'username' => $mobile, 'link' => rtrim($this->appUrl, '/') . '/login', ]); } }