@tencent-rtc/trtc-agent-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/README.zh.md +173 -0
  4. package/bin/cli.js +434 -0
  5. package/knowledge-base/index.yaml +454 -0
  6. package/knowledge-base/platform-slice-template.md +233 -0
  7. package/knowledge-base/scenario-spec.md +350 -0
  8. package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
  9. package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
  10. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
  11. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
  12. package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
  13. package/knowledge-base/slice-spec.md +546 -0
  14. package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
  15. package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
  16. package/knowledge-base/slices/conference/web/device-control.md +338 -0
  17. package/knowledge-base/slices/conference/web/login-auth.md +261 -0
  18. package/knowledge-base/slices/conference/web/network-quality.md +190 -0
  19. package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
  20. package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
  21. package/knowledge-base/slices/conference/web/participant-list.md +238 -0
  22. package/knowledge-base/slices/conference/web/participant-management.md +718 -0
  23. package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
  24. package/knowledge-base/slices/conference/web/room-call.md +213 -0
  25. package/knowledge-base/slices/conference/web/room-chat.md +426 -0
  26. package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
  27. package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
  28. package/knowledge-base/slices/conference/web/screen-share.md +211 -0
  29. package/knowledge-base/slices/conference/web/video-layout.md +675 -0
  30. package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
  31. package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
  32. package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
  33. package/knowledge-base/slices/live/anchor-preview.md +90 -0
  34. package/knowledge-base/slices/live/anchor-room-config.md +104 -0
  35. package/knowledge-base/slices/live/audience-list.md +86 -0
  36. package/knowledge-base/slices/live/audience-manage.md +92 -0
  37. package/knowledge-base/slices/live/audience-watch.md +85 -0
  38. package/knowledge-base/slices/live/audio.md +116 -0
  39. package/knowledge-base/slices/live/barrage.md +88 -0
  40. package/knowledge-base/slices/live/beauty.md +99 -0
  41. package/knowledge-base/slices/live/coguest-apply.md +105 -0
  42. package/knowledge-base/slices/live/device-control.md +91 -0
  43. package/knowledge-base/slices/live/error-codes.md +167 -0
  44. package/knowledge-base/slices/live/gift.md +84 -0
  45. package/knowledge-base/slices/live/ios/.gitkeep +0 -0
  46. package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
  47. package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
  48. package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
  49. package/knowledge-base/slices/live/ios/audience-list.md +353 -0
  50. package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
  51. package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
  52. package/knowledge-base/slices/live/ios/audio.md +373 -0
  53. package/knowledge-base/slices/live/ios/barrage.md +285 -0
  54. package/knowledge-base/slices/live/ios/beauty.md +323 -0
  55. package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
  56. package/knowledge-base/slices/live/ios/device-control.md +286 -0
  57. package/knowledge-base/slices/live/ios/error-codes.md +270 -0
  58. package/knowledge-base/slices/live/ios/gift.md +315 -0
  59. package/knowledge-base/slices/live/ios/live-list.md +269 -0
  60. package/knowledge-base/slices/live/ios/login-auth.md +247 -0
  61. package/knowledge-base/slices/live/live-list.md +82 -0
  62. package/knowledge-base/slices/live/login-auth.md +78 -0
  63. package/package.json +34 -0
  64. package/skills/trtc/SKILL.md +326 -0
  65. package/skills/trtc/room-builder/SKILL.md +138 -0
  66. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
  67. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
  68. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
  69. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
  70. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
  71. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
  72. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
  73. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
  74. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
  75. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
  76. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
  77. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
  78. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
  79. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
  80. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
  81. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
  82. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
  83. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
  84. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
  85. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  86. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  87. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
  88. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
  89. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
  90. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
  91. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
  92. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
  93. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
  94. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
  95. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
  96. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
  97. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
  98. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
  99. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
  100. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
  101. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
  102. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
  103. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
  104. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
  105. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
  106. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
  107. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
  108. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
  109. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
  110. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
  111. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
  112. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
  113. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
  114. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
  115. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
  116. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
  117. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
  118. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
  119. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
  120. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
  121. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
  122. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
  123. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
  124. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
  125. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
  126. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
  127. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
  128. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
  129. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
  130. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
  131. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
  132. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
  133. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
  134. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
  135. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
  136. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
  137. package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
  138. package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
  139. package/skills/trtc-apply/SKILL.md +97 -0
  140. package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
  141. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  142. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
  143. package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
  144. package/skills/trtc-docs/SKILL.md +207 -0
  145. package/skills/trtc-onboarding/SKILL.md +839 -0
  146. package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
  147. package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
  148. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
  149. package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
  150. package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
  151. package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
  152. package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
  153. package/skills/trtc-search/SKILL.md +221 -0
  154. package/skills/trtc-topic/SKILL.md +638 -0
  155. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
  156. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
  157. package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
  158. package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
  159. package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
  160. package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
  161. package/skills/trtc-topic/references/execution-units.yaml +58 -0
  162. package/skills/trtc-topic/runtime/README.md +50 -0
  163. package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
  164. package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
  165. package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
  166. package/skills/trtc-topic/runtime/package-lock.json +1211 -0
  167. package/skills/trtc-topic/runtime/package.json +13 -0
  168. package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
  169. package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
  170. package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
  171. package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
  172. package/skills/trtc-topic/scripts/apply.py +581 -0
  173. package/skills/trtc-topic/scripts/finalize_session.py +113 -0
  174. package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
  175. package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
  176. package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
  177. package/skills/trtc-topic/scripts/next_slice.py +137 -0
  178. package/skills/trtc-topic/tests/README.md +70 -0
  179. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
  180. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  181. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
  182. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  183. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
  184. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
  185. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
  186. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
  187. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
  188. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
  189. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
  190. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
  191. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
  192. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
  193. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
  194. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
  195. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
  196. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
  197. package/skills/trtc-topic/tests/conftest.py +72 -0
  198. package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
  199. package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
  200. package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
  201. package/skills/trtc-topic/tests/test_gates.py +316 -0
  202. package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
  203. package/skills/trtc-topic/tests/test_state_machine.py +414 -0
  204. package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
  205. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
@@ -0,0 +1,678 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
3
+ import { useRouter } from 'vue-router';
4
+ import { useUIKit } from '@tencentcloud/uikit-base-component-vue3';
5
+ import {
6
+ Stethoscope,
7
+ Bell,
8
+ LogOut,
9
+ Users,
10
+ Clock,
11
+ CheckCircle,
12
+ TrendingUp,
13
+ Video,
14
+ FileText,
15
+ X,
16
+ } from '@/shared/icons';
17
+ import {
18
+ useLoginState,
19
+ useRoomState,
20
+ RoomEvent,
21
+ RoomStatus,
22
+ } from 'tuikit-atomicx-vue3';
23
+ import { services, type MedicalAppointment } from '@/services/adapters';
24
+ import type { ConsultationCallEvent } from '@/features/consultation/types';
25
+ import { clearSession, getSessionUser } from '@/utils/session';
26
+ import { formatTimeRange } from '@/utils/format';
27
+ import LoadingSpinner from '@/components/LoadingSpinner.vue';
28
+ import MedicalButton from '@/components/MedicalButton.vue';
29
+ import LanguageSwitch from '@/components/LanguageSwitch.vue';
30
+
31
+ const router = useRouter();
32
+ const loginState = useLoginState();
33
+ const roomState = useRoomState();
34
+ const { t } = useUIKit();
35
+
36
+ const loading = ref(false);
37
+ const syncing = ref(false);
38
+ const startingAppointmentId = ref('');
39
+ const loggingOut = ref(false);
40
+ const consultationInvite = ref<{
41
+ appointment: MedicalAppointment;
42
+ fromDoctor: ReturnType<typeof services.user.getDoctorById>;
43
+ patient: ReturnType<typeof services.user.getPatientById>;
44
+ roomId: string;
45
+ } | null>(null);
46
+ const joiningInvite = ref(false);
47
+ const inviteDetailsVisible = ref(false);
48
+ const actionMessage = ref('');
49
+ type AppointmentFilter = 'all' | 'waiting' | 'running';
50
+
51
+ const activeFilter = ref<AppointmentFilter>('all');
52
+ const filters = computed<Array<{ value: AppointmentFilter; label: string }>>(() => [
53
+ { value: 'all', label: t('Medical.DoctorDashboard.All') },
54
+ { value: 'waiting', label: t('Medical.DoctorDashboard.Waiting') },
55
+ { value: 'running', label: t('Medical.DoctorDashboard.Running') },
56
+ ]);
57
+
58
+ const doctor = computed(() => getSessionUser());
59
+ const doctorAppointments = computed(() =>
60
+ doctor.value
61
+ ? services.appointment.getAppointmentsByDoctor(doctor.value.userId)
62
+ : []
63
+ );
64
+
65
+ const appointmentCards = computed(() =>
66
+ doctorAppointments.value.map(item => {
67
+ const scheduled = roomState.scheduledRoomList.value.find(
68
+ room => room.roomId === item.roomId
69
+ );
70
+ const patient = services.user.getPatientById(item.patientId);
71
+ return {
72
+ ...item,
73
+ scheduled,
74
+ patient,
75
+ };
76
+ })
77
+ );
78
+
79
+ const filteredAppointmentCards = computed(() => {
80
+ if (activeFilter.value === 'all') {
81
+ return appointmentCards.value;
82
+ }
83
+ if (activeFilter.value === 'waiting') {
84
+ return appointmentCards.value.filter(
85
+ item => item.scheduled?.roomStatus !== RoomStatus.Running
86
+ );
87
+ }
88
+ return appointmentCards.value.filter(
89
+ item => item.scheduled?.roomStatus === RoomStatus.Running
90
+ );
91
+ });
92
+
93
+ const stats = computed(() => {
94
+ const total = appointmentCards.value.length;
95
+ const running = appointmentCards.value.filter(
96
+ item => item.scheduled?.roomStatus === RoomStatus.Running
97
+ ).length;
98
+ const scheduled = appointmentCards.value.filter(
99
+ item => item.scheduled?.roomStatus !== RoomStatus.Running
100
+ ).length;
101
+ const syncedRooms = roomState.scheduledRoomList.value.length;
102
+ return [
103
+ {
104
+ title: t('Medical.DoctorDashboard.StatToday'),
105
+ value: String(total),
106
+ icon: Users,
107
+ color: 'bg-gradient-to-br from-blue-500 to-blue-600',
108
+ },
109
+ {
110
+ title: t('Medical.DoctorDashboard.StatWaiting'),
111
+ value: String(scheduled),
112
+ icon: Clock,
113
+ color: 'bg-gradient-to-br from-yellow-500 to-yellow-600',
114
+ },
115
+ {
116
+ title: t('Medical.DoctorDashboard.StatRunning'),
117
+ value: String(running),
118
+ icon: CheckCircle,
119
+ color: 'bg-gradient-to-br from-green-500 to-green-600',
120
+ },
121
+ {
122
+ title: t('Medical.DoctorDashboard.StatSynced'),
123
+ value: String(syncedRooms),
124
+ icon: TrendingUp,
125
+ color: 'bg-gradient-to-br from-purple-500 to-purple-600',
126
+ },
127
+ ];
128
+ });
129
+
130
+ function getSchedulableWindow(appointment: {
131
+ scheduleStartTime: number;
132
+ scheduleEndTime: number;
133
+ }) {
134
+ const nowSeconds = Math.floor(Date.now() / 1000);
135
+ const minimumStartTime = nowSeconds + 5 * 60;
136
+ const originalDuration = Math.max(
137
+ 30 * 60,
138
+ appointment.scheduleEndTime - appointment.scheduleStartTime
139
+ );
140
+
141
+ if (appointment.scheduleEndTime > minimumStartTime) {
142
+ return {
143
+ scheduleStartTime: appointment.scheduleStartTime,
144
+ scheduleEndTime: appointment.scheduleEndTime,
145
+ };
146
+ }
147
+
148
+ return {
149
+ scheduleStartTime: minimumStartTime,
150
+ scheduleEndTime: minimumStartTime + originalDuration,
151
+ };
152
+ }
153
+
154
+ async function loadScheduledRooms() {
155
+ loading.value = true;
156
+ try {
157
+ await roomState.getScheduledRoomList({ cursor: '' });
158
+ } finally {
159
+ loading.value = false;
160
+ }
161
+ }
162
+
163
+ async function syncAppointments() {
164
+ if (!doctor.value) {
165
+ return;
166
+ }
167
+ syncing.value = true;
168
+ actionMessage.value = '';
169
+ try {
170
+ for (const appointment of doctorAppointments.value) {
171
+ const existing = roomState.scheduledRoomList.value.find(
172
+ room => room.roomId === appointment.roomId
173
+ );
174
+ if (existing) {
175
+ continue;
176
+ }
177
+ const scheduleWindow = getSchedulableWindow(appointment);
178
+ await roomState.scheduleRoom({
179
+ roomId: appointment.roomId,
180
+ options: {
181
+ roomName: `${doctor.value.userName} · ${appointment.id}`,
182
+ scheduleStartTime: scheduleWindow.scheduleStartTime,
183
+ scheduleEndTime: scheduleWindow.scheduleEndTime,
184
+ scheduleAttendees: [appointment.patientId],
185
+ reminderSecondsBeforeStart: 300,
186
+ },
187
+ });
188
+ }
189
+ await loadScheduledRooms();
190
+ actionMessage.value = t('Medical.DoctorDashboard.RefreshSuccess');
191
+ } catch (error) {
192
+ actionMessage.value =
193
+ error instanceof Error ? error.message : t('Medical.DoctorDashboard.RefreshFailed');
194
+ } finally {
195
+ syncing.value = false;
196
+ }
197
+ }
198
+
199
+ async function startConsultation(appointmentId: string, roomId: string) {
200
+ if (startingAppointmentId.value) {
201
+ return;
202
+ }
203
+ const appointment = services.appointment.getAppointmentById(appointmentId);
204
+ if (!appointment) {
205
+ return;
206
+ }
207
+ startingAppointmentId.value = appointmentId;
208
+ try {
209
+ try {
210
+ await roomState.createAndJoinRoom({
211
+ roomId,
212
+ options: {
213
+ roomName: `${doctor.value?.userName ?? t('Medical.Common.Doctor')} · ${appointment.id}`,
214
+ },
215
+ });
216
+ } catch {
217
+ await roomState.joinRoom({ roomId });
218
+ }
219
+ await router.push(`/doctor/consultation/${appointmentId}`);
220
+ } catch (error) {
221
+ actionMessage.value =
222
+ error instanceof Error ? error.message : t('Medical.DoctorDashboard.EnterFailed');
223
+ } finally {
224
+ startingAppointmentId.value = '';
225
+ }
226
+ }
227
+
228
+ function getInviteDoctorId(
229
+ eventInfo: ConsultationCallEvent,
230
+ appointment: MedicalAppointment
231
+ ) {
232
+ return (
233
+ eventInfo?.call?.inviter?.userId ||
234
+ eventInfo?.call?.caller?.userId ||
235
+ eventInfo?.inviter?.userId ||
236
+ eventInfo?.senderUserId ||
237
+ appointment.doctorId
238
+ );
239
+ }
240
+
241
+ function handleConsultationInviteReceived(eventInfo: ConsultationCallEvent) {
242
+ const roomId = eventInfo?.roomInfo?.roomId || eventInfo?.call?.roomId || '';
243
+ if (!roomId || !doctor.value) {
244
+ return;
245
+ }
246
+ const appointment = services.appointment.getAppointmentByRoomId(roomId);
247
+ if (!appointment || appointment.doctorId === doctor.value.userId) {
248
+ return;
249
+ }
250
+ const fromDoctor = services.user.getDoctorById(
251
+ getInviteDoctorId(eventInfo, appointment)
252
+ );
253
+ consultationInvite.value = {
254
+ appointment,
255
+ fromDoctor,
256
+ patient: services.user.getPatientById(appointment.patientId),
257
+ roomId,
258
+ };
259
+ }
260
+
261
+ function clearConsultationInviteByRoom(eventInfo: ConsultationCallEvent) {
262
+ const roomId = eventInfo?.roomInfo?.roomId || eventInfo?.call?.roomId || '';
263
+ if (roomId && consultationInvite.value?.roomId === roomId) {
264
+ dismissConsultationInvite();
265
+ }
266
+ }
267
+
268
+ function dismissConsultationInvite() {
269
+ consultationInvite.value = null;
270
+ inviteDetailsVisible.value = false;
271
+ }
272
+
273
+ async function joinConsultationInvite() {
274
+ if (!consultationInvite.value || joiningInvite.value) {
275
+ return;
276
+ }
277
+ joiningInvite.value = true;
278
+ actionMessage.value = '';
279
+ try {
280
+ try {
281
+ await roomState.acceptCall({ roomId: consultationInvite.value.roomId });
282
+ } catch {
283
+ await roomState.joinRoom({ roomId: consultationInvite.value.roomId });
284
+ }
285
+ await router.push(
286
+ `/doctor/consultation/${consultationInvite.value.appointment.id}`
287
+ );
288
+ dismissConsultationInvite();
289
+ } catch (error) {
290
+ actionMessage.value =
291
+ error instanceof Error ? error.message : t('Medical.DoctorDashboard.JoinInviteFailed');
292
+ } finally {
293
+ joiningInvite.value = false;
294
+ }
295
+ }
296
+
297
+ async function logout() {
298
+ if (loggingOut.value) {
299
+ return;
300
+ }
301
+ loggingOut.value = true;
302
+ try {
303
+ await loginState.logout();
304
+ clearSession();
305
+ router.replace('/login');
306
+ } finally {
307
+ loggingOut.value = false;
308
+ }
309
+ }
310
+
311
+ onMounted(async () => {
312
+ if (!doctor.value || doctor.value.role !== 'doctor') {
313
+ router.replace('/login');
314
+ return;
315
+ }
316
+ await loadScheduledRooms();
317
+ roomState.subscribeEvent(
318
+ RoomEvent.onCallReceived,
319
+ handleConsultationInviteReceived
320
+ );
321
+ roomState.subscribeEvent(
322
+ RoomEvent.onCallCancelled,
323
+ clearConsultationInviteByRoom
324
+ );
325
+ roomState.subscribeEvent(RoomEvent.onCallTimeout, clearConsultationInviteByRoom);
326
+ });
327
+
328
+ onBeforeUnmount(() => {
329
+ roomState.unsubscribeEvent(
330
+ RoomEvent.onCallReceived,
331
+ handleConsultationInviteReceived
332
+ );
333
+ roomState.unsubscribeEvent(
334
+ RoomEvent.onCallCancelled,
335
+ clearConsultationInviteByRoom
336
+ );
337
+ roomState.unsubscribeEvent(
338
+ RoomEvent.onCallTimeout,
339
+ clearConsultationInviteByRoom
340
+ );
341
+ });
342
+ </script>
343
+
344
+ <template>
345
+ <div class="min-h-screen bg-[#F8FAFB]">
346
+ <header class="bg-white border-b border-gray-200 px-6 py-4">
347
+ <div class="max-w-7xl mx-auto flex items-center justify-between">
348
+ <div class="flex items-center gap-4">
349
+ <div
350
+ class="bg-gradient-to-br from-[#0D9488] to-[#0F766E] p-2 rounded-xl"
351
+ >
352
+ <Stethoscope :size="24" class="text-white" />
353
+ </div>
354
+ <div>
355
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('Medical.DoctorDashboard.Title') }}</h1>
356
+ <p class="text-sm text-gray-500">{{ t('Medical.DoctorDashboard.Subtitle') }}</p>
357
+ </div>
358
+ </div>
359
+
360
+ <div class="flex items-center gap-4">
361
+ <LanguageSwitch />
362
+ <button
363
+ class="p-2 hover:bg-gray-100 rounded-xl transition-colors relative"
364
+ >
365
+ <Bell :size="20" class="text-gray-600" />
366
+ <span
367
+ v-if="consultationInvite"
368
+ class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"
369
+ ></span>
370
+ </button>
371
+ <div class="h-8 w-px bg-gray-200"></div>
372
+ <div class="flex items-center gap-3">
373
+ <div
374
+ class="w-10 h-10 rounded-full bg-gradient-to-br from-[#0D9488] to-[#0F766E] flex items-center justify-center text-white font-medium"
375
+ >
376
+ {{ doctor?.userName?.slice(0, 1) }}
377
+ </div>
378
+ <div>
379
+ <p class="text-sm font-medium text-gray-900">
380
+ {{ doctor?.userName }}
381
+ </p>
382
+ <p class="text-xs text-gray-500">{{ doctor?.title }}</p>
383
+ </div>
384
+ </div>
385
+ <button
386
+ @click="logout"
387
+ :disabled="loggingOut"
388
+ class="p-2 hover:bg-gray-100 rounded-xl transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
389
+ >
390
+ <LoadingSpinner v-if="loggingOut" class="text-gray-600" />
391
+ <LogOut v-else :size="20" class="text-gray-600" />
392
+ </button>
393
+ </div>
394
+ </div>
395
+ </header>
396
+
397
+ <main class="max-w-7xl mx-auto p-6">
398
+ <div class="flex items-end justify-between gap-4 mb-6">
399
+ <div>
400
+ <h2 class="text-3xl font-semibold text-gray-900 mb-2">
401
+ {{ t('Medical.DoctorDashboard.TodayAppointments', { count: stats[0]?.value }) }}
402
+ </h2>
403
+ <p class="text-sm text-gray-500">
404
+ {{ t('Medical.DoctorDashboard.CurrentDoctor', { hospital: doctor?.hospital, doctor: doctor?.userName }) }}
405
+ </p>
406
+ </div>
407
+ <MedicalButton @click="syncAppointments" :loading="syncing">
408
+ {{ syncing ? t('Medical.DoctorDashboard.Refreshing') : t('Medical.DoctorDashboard.RefreshToday') }}
409
+ </MedicalButton>
410
+ </div>
411
+
412
+ <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
413
+ <div
414
+ v-for="stat in stats"
415
+ :key="stat.title"
416
+ class="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm hover:shadow-md transition-shadow"
417
+ >
418
+ <div class="flex items-center justify-between mb-4">
419
+ <div :class="['p-3 rounded-xl', stat.color]">
420
+ <component :is="stat.icon" :size="24" class="text-white" />
421
+ </div>
422
+ </div>
423
+ <h3 class="text-2xl font-semibold text-gray-900 mb-1">
424
+ {{ stat.value }}
425
+ </h3>
426
+ <p class="text-sm text-gray-600">{{ stat.title }}</p>
427
+ </div>
428
+ </div>
429
+
430
+ <p
431
+ v-if="actionMessage"
432
+ class="mb-4 text-sm text-[#0D9488] bg-[#0D9488]/10 px-4 py-3 rounded-xl"
433
+ >
434
+ {{ actionMessage }}
435
+ </p>
436
+
437
+ <div
438
+ v-if="consultationInvite"
439
+ class="mb-6 rounded-3xl border border-indigo-100 bg-white shadow-[0_10px_30px_rgba(79,70,229,0.12)]"
440
+ >
441
+ <div class="p-5 flex items-center justify-between gap-4">
442
+ <div class="flex items-center gap-4 flex-1 min-w-0">
443
+ <div class="relative shrink-0">
444
+ <div
445
+ class="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-indigo-500/30 animate-pulse"
446
+ >
447
+ <Stethoscope class="w-6 h-6 text-white" />
448
+ </div>
449
+ <div
450
+ class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center border-2 border-white"
451
+ >
452
+ <span class="text-white text-xs font-bold">!</span>
453
+ </div>
454
+ </div>
455
+ <div class="flex-1 min-w-0">
456
+ <div class="flex items-center gap-2 mb-1">
457
+ <h4 class="font-semibold text-gray-900">{{ t('Medical.DoctorDashboard.ConsultationInvite') }}</h4>
458
+ <span
459
+ class="bg-indigo-500 text-white rounded-full px-3 py-0.5 text-xs font-medium"
460
+ >
461
+ {{ t('Medical.DoctorDashboard.Pending') }}
462
+ </span>
463
+ </div>
464
+ <p class="text-sm text-gray-700 leading-6">
465
+ {{
466
+ t('Medical.DoctorDashboard.InviteSummary', {
467
+ doctor: consultationInvite.fromDoctor?.userName || t('Medical.Common.Doctor'),
468
+ department: consultationInvite.fromDoctor?.department || t('Medical.Common.UnknownDepartment'),
469
+ patient: consultationInvite.patient?.userName || t('Medical.Common.Patient'),
470
+ })
471
+ }}
472
+ </p>
473
+ </div>
474
+ </div>
475
+ <div class="flex items-center gap-3 shrink-0">
476
+ <button
477
+ type="button"
478
+ @click="inviteDetailsVisible = true"
479
+ class="rounded-xl gap-2 h-11 px-4 border border-indigo-200 text-indigo-600 hover:bg-indigo-50 inline-flex items-center justify-center font-medium transition-colors"
480
+ >
481
+ <FileText class="w-4 h-4" />
482
+ {{ t('Medical.DoctorDashboard.ViewDetails') }}
483
+ </button>
484
+ <button
485
+ type="button"
486
+ @click="joinConsultationInvite"
487
+ :disabled="joiningInvite"
488
+ class="bg-gradient-to-r from-indigo-500 to-indigo-600 hover:from-indigo-600 hover:to-indigo-700 text-white rounded-xl gap-2 h-11 px-4 shadow-lg shadow-indigo-500/30 inline-flex items-center justify-center font-medium transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
489
+ >
490
+ <LoadingSpinner v-if="joiningInvite" />
491
+ <Video v-else class="w-4 h-4" />
492
+ {{ joiningInvite ? t('Medical.DoctorDashboard.Joining') : t('Medical.DoctorDashboard.JoinNow') }}
493
+ </button>
494
+ <button
495
+ type="button"
496
+ @click="dismissConsultationInvite"
497
+ class="w-9 h-9 rounded-lg hover:bg-gray-100 shrink-0 inline-flex items-center justify-center transition-colors"
498
+ >
499
+ <X class="w-4 h-4 text-gray-500" />
500
+ </button>
501
+ </div>
502
+ </div>
503
+ </div>
504
+
505
+ <div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
506
+ <div class="p-6 border-b border-gray-100">
507
+ <div class="flex items-center justify-between gap-4">
508
+ <div>
509
+ <h2 class="text-lg font-semibold text-gray-900">{{ t('Medical.DoctorDashboard.WaitingPatients') }}</h2>
510
+ <p class="text-sm text-gray-500 mt-1">
511
+ {{ loading ? t('Medical.DoctorDashboard.LoadingAppointments') : t('Medical.DoctorDashboard.SortByTime') }}
512
+ </p>
513
+ </div>
514
+ <div class="flex items-center gap-2">
515
+ <button
516
+ v-for="filter in filters"
517
+ :key="filter.value"
518
+ @click="activeFilter = filter.value"
519
+ :class="[
520
+ 'px-4 py-2 rounded-xl text-sm font-medium transition-all',
521
+ activeFilter === filter.value
522
+ ? 'bg-[#0D9488] text-white'
523
+ : 'text-gray-600 hover:bg-gray-100',
524
+ ]"
525
+ >
526
+ {{ filter.label }}
527
+ </button>
528
+ </div>
529
+ </div>
530
+ </div>
531
+
532
+ <div class="divide-y divide-gray-100">
533
+ <div
534
+ v-for="item in filteredAppointmentCards"
535
+ :key="item.id"
536
+ class="p-6 hover:bg-gray-50 transition-colors"
537
+ >
538
+ <div class="flex items-center justify-between gap-4">
539
+ <div class="flex items-center gap-4 flex-1 min-w-0">
540
+ <div
541
+ class="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-medium"
542
+ >
543
+ {{ item.patient?.userName?.charAt(0) }}
544
+ </div>
545
+ <div class="flex-1 min-w-0">
546
+ <div class="flex items-center gap-3 mb-1 flex-wrap">
547
+ <h3 class="font-semibold text-gray-900">
548
+ {{ item.patient?.userName }}
549
+ </h3>
550
+ <span class="text-sm text-gray-500">
551
+ {{ t('Medical.DoctorDashboard.PatientAge', { gender: item.patientGender, age: item.patientAge }) }}
552
+ </span>
553
+ <span
554
+ :class="[
555
+ 'px-2 py-0.5 rounded-full text-xs font-medium',
556
+ item.scheduled?.roomStatus === RoomStatus.Running
557
+ ? 'bg-green-100 text-green-700'
558
+ : 'bg-yellow-100 text-yellow-700',
559
+ ]"
560
+ >
561
+ {{
562
+ item.scheduled?.roomStatus === RoomStatus.Running
563
+ ? t('Medical.DoctorDashboard.ConsultationRunning')
564
+ : t('Medical.DoctorDashboard.Waiting')
565
+ }}
566
+ </span>
567
+ </div>
568
+ <div
569
+ class="flex items-center gap-6 text-sm text-gray-600 flex-wrap"
570
+ >
571
+ <span class="flex items-center gap-1">
572
+ <Clock :size="14" />
573
+ {{
574
+ formatTimeRange(
575
+ item.scheduleStartTime,
576
+ item.scheduleEndTime
577
+ )
578
+ }}
579
+ </span>
580
+ <span class="flex items-center gap-1">
581
+ <FileText :size="14" />
582
+ {{ item.chiefComplaint }}
583
+ </span>
584
+ </div>
585
+ </div>
586
+ </div>
587
+
588
+ <MedicalButton
589
+ @click="startConsultation(item.id, item.roomId)"
590
+ :disabled="!!startingAppointmentId"
591
+ :loading="startingAppointmentId === item.id"
592
+ class="shrink-0"
593
+ >
594
+ <Video v-if="startingAppointmentId !== item.id" :size="16" />
595
+ {{
596
+ startingAppointmentId === item.id ? t('Medical.Common.Entering') : t('Medical.DoctorDashboard.StartConsultation')
597
+ }}
598
+ </MedicalButton>
599
+ </div>
600
+ </div>
601
+ </div>
602
+ </div>
603
+ </main>
604
+
605
+ <div
606
+ v-if="inviteDetailsVisible && consultationInvite"
607
+ class="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 p-4"
608
+ @click.self="inviteDetailsVisible = false"
609
+ >
610
+ <div class="w-full max-w-[460px] rounded-3xl bg-white p-6 shadow-2xl">
611
+ <div class="flex items-start justify-between gap-4">
612
+ <div>
613
+ <h3 class="text-lg font-semibold text-gray-900">{{ t('Medical.DoctorDashboard.InviteDetails') }}</h3>
614
+ <p class="mt-1 text-sm text-gray-500">
615
+ {{ t('Medical.DoctorDashboard.AppointmentId', { id: consultationInvite.appointment.id }) }}
616
+ </p>
617
+ </div>
618
+ <button
619
+ type="button"
620
+ @click="inviteDetailsVisible = false"
621
+ class="w-8 h-8 rounded-lg hover:bg-gray-100 inline-flex items-center justify-center"
622
+ >
623
+ <X class="w-4 h-4 text-gray-500" />
624
+ </button>
625
+ </div>
626
+
627
+ <div class="mt-5 space-y-4 text-sm">
628
+ <div class="rounded-2xl bg-indigo-50 px-4 py-3">
629
+ <p class="text-gray-500">{{ t('Medical.DoctorDashboard.InvitingDoctor') }}</p>
630
+ <p class="mt-1 font-medium text-gray-900">
631
+ {{ consultationInvite.fromDoctor?.userName || t('Medical.Common.Doctor') }}
632
+ · {{ consultationInvite.fromDoctor?.department || t('Medical.Common.UnknownDepartment') }}
633
+ </p>
634
+ </div>
635
+ <div class="rounded-2xl bg-gray-50 px-4 py-3">
636
+ <p class="text-gray-500">{{ t('Medical.DoctorDashboard.PatientInfo') }}</p>
637
+ <p class="mt-1 font-medium text-gray-900">
638
+ {{ consultationInvite.patient?.userName || t('Medical.Common.Patient') }}
639
+ · {{ t('Medical.DoctorDashboard.PatientAge', { gender: consultationInvite.appointment.patientGender, age: consultationInvite.appointment.patientAge }) }}
640
+ </p>
641
+ </div>
642
+ <div class="rounded-2xl bg-gray-50 px-4 py-3">
643
+ <p class="text-gray-500">{{ t('Medical.DoctorDashboard.ChiefComplaint') }}</p>
644
+ <p class="mt-1 font-medium text-gray-900">
645
+ {{ consultationInvite.appointment.chiefComplaint }}
646
+ </p>
647
+ </div>
648
+ <div class="rounded-2xl bg-red-50 px-4 py-3">
649
+ <p class="text-red-500">{{ t('Medical.DoctorDashboard.AllergyHistory') }}</p>
650
+ <p class="mt-1 font-medium text-red-700">
651
+ {{ consultationInvite.appointment.allergyHistory }}
652
+ </p>
653
+ </div>
654
+ </div>
655
+
656
+ <div class="mt-6 flex justify-end gap-3">
657
+ <button
658
+ type="button"
659
+ @click="inviteDetailsVisible = false"
660
+ class="h-11 px-4 rounded-xl border border-gray-200 text-gray-700 font-medium hover:bg-gray-50"
661
+ >
662
+ {{ t('Medical.DoctorDashboard.HandleLater') }}
663
+ </button>
664
+ <button
665
+ type="button"
666
+ @click="joinConsultationInvite"
667
+ :disabled="joiningInvite"
668
+ class="h-11 px-4 rounded-xl bg-indigo-600 text-white font-medium hover:bg-indigo-700 disabled:opacity-60 disabled:cursor-not-allowed inline-flex items-center gap-2"
669
+ >
670
+ <LoadingSpinner v-if="joiningInvite" />
671
+ <Video v-else class="w-4 h-4" />
672
+ {{ joiningInvite ? t('Medical.DoctorDashboard.Joining') : t('Medical.DoctorDashboard.JoinNow') }}
673
+ </button>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </div>
678
+ </template>