Lashtw commited on
Commit
8f2336a
·
verified ·
1 Parent(s): e96828c

Upload 11 files

Browse files
Files changed (2) hide show
  1. src/main.js +10 -0
  2. src/views/InstructorAdminView.js +193 -0
src/main.js CHANGED
@@ -3,6 +3,8 @@ import { renderInstructorView, setupInstructorEvents } from './views/InstructorV
3
  import { renderStudentView, setupStudentEvents } from './views/StudentView.js';
4
  import { renderAdminView, setupAdminEvents } from './views/AdminView.js';
5
 
 
 
6
  const app = document.querySelector('#app');
7
 
8
  function navigateTo(view) {
@@ -20,6 +22,10 @@ function navigateTo(view) {
20
  setupInstructorEvents();
21
  });
22
  break;
 
 
 
 
23
  case 'student':
24
  app.innerHTML = '載入中...';
25
  // Async render because Student view fetches challenges
@@ -56,6 +62,10 @@ function handleRoute() {
56
  navigateTo('instructor');
57
  return;
58
  }
 
 
 
 
59
 
60
 
61
 
 
3
  import { renderStudentView, setupStudentEvents } from './views/StudentView.js';
4
  import { renderAdminView, setupAdminEvents } from './views/AdminView.js';
5
 
6
+ import { renderInstructorAdminView, setupInstructorAdminEvents } from './views/InstructorAdminView.js';
7
+
8
  const app = document.querySelector('#app');
9
 
10
  function navigateTo(view) {
 
22
  setupInstructorEvents();
23
  });
24
  break;
25
+ case 'instructors': // Manage Instructors
26
+ app.innerHTML = renderInstructorAdminView();
27
+ setupInstructorAdminEvents();
28
+ break;
29
  case 'student':
30
  app.innerHTML = '載入中...';
31
  // Async render because Student view fetches challenges
 
62
  navigateTo('instructor');
63
  return;
64
  }
65
+ if (hash === 'instructors') {
66
+ navigateTo('instructors');
67
+ return;
68
+ }
69
 
70
 
71
 
src/views/InstructorAdminView.js ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getInstructors, addInstructor, removeInstructor, checkInstructorPermission } from "../services/auth.js";
2
+ import { auth } from "../services/firebase.js";
3
+
4
+ export function renderInstructorAdminView() {
5
+ return `
6
+ <div class="min-h-screen p-6 pb-20">
7
+ <header class="flex justify-between items-center mb-10 bg-gray-800 bg-opacity-50 p-4 rounded-xl border border-gray-700 backdrop-blur-sm">
8
+ <div class="flex items-center space-x-4">
9
+ <button id="back-instructor-btn" class="bg-gray-700 hover:bg-gray-600 text-white p-2 rounded-lg transition-all">
10
+ ← 回講師端
11
+ </button>
12
+ <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-600">
13
+ 講師權限管理
14
+ </h1>
15
+ </div>
16
+ <button id="add-instructor-btn" class="bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2 px-6 rounded-lg transition-all shadow-lg">
17
+ + 新增講師
18
+ </button>
19
+ </header>
20
+
21
+ <div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden shadow-2xl">
22
+ <table class="w-full text-left border-collapse">
23
+ <thead>
24
+ <tr class="bg-gray-900/50">
25
+ <th class="p-4 border-b border-gray-700 font-bold text-gray-400">Email (帳號)</th>
26
+ <th class="p-4 border-b border-gray-700 font-bold text-gray-400">姓名</th>
27
+ <th class="p-4 border-b border-gray-700 font-bold text-gray-400">權限</th>
28
+ <th class="p-4 border-b border-gray-700 font-bold text-gray-400 text-right">操作</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody id="instructors-list">
32
+ <tr><td colspan="4" class="p-8 text-center text-gray-500">載入中...</td></tr>
33
+ </tbody>
34
+ </table>
35
+ </div>
36
+
37
+ <!-- Add/Edit Modal -->
38
+ <div id="instructor-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4">
39
+ <div class="bg-gray-800 rounded-xl w-full max-w-lg border border-gray-700 shadow-2xl">
40
+ <div class="p-6 border-b border-gray-700">
41
+ <h3 class="text-xl font-bold text-white">新增講師</h3>
42
+ </div>
43
+ <div class="p-6 space-y-4">
44
+ <div>
45
+ <label class="block text-gray-400 mb-1">Email (Google 帳號)</label>
46
+ <input type="email" id="input-email" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white" placeholder="example@gmail.com">
47
+ <p class="text-xs text-yellow-500 mt-1">* 必須是有效的 Google 帳號 Email</p>
48
+ </div>
49
+ <div>
50
+ <label class="block text-gray-400 mb-1">顯示名稱</label>
51
+ <input type="text" id="input-name" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white" placeholder="王小明">
52
+ </div>
53
+ <div>
54
+ <label class="block text-gray-400 mb-2">權限設定</label>
55
+ <div class="space-y-2">
56
+ <label class="flex items-center space-x-2">
57
+ <input type="checkbox" class="perm-check rounded bg-gray-700 border-gray-600 text-indigo-500" value="create_room" checked>
58
+ <span class="text-gray-300">建立教室 (Create Room)</span>
59
+ </label>
60
+ <label class="flex items-center space-x-2">
61
+ <input type="checkbox" class="perm-check rounded bg-gray-700 border-gray-600 text-indigo-500" value="manage_instructors">
62
+ <span class="text-gray-300">管理講師 (Manage Instructors)</span>
63
+ </label>
64
+ <label class="flex items-center space-x-2">
65
+ <input type="checkbox" class="perm-check rounded bg-gray-700 border-gray-600 text-indigo-500" value="add_question">
66
+ <span class="text-gray-300">管理題目 (Manage Questions)</span>
67
+ </label>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ <div class="p-6 border-t border-gray-700 flex justify-end space-x-3">
72
+ <button onclick="document.getElementById('instructor-modal').classList.add('hidden')" class="px-4 py-2 text-gray-400 hover:text-white">取消</button>
73
+ <button id="save-instructor-btn" class="bg-indigo-600 hover:bg-indigo-500 text-white px-6 py-2 rounded font-bold">新增</button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ export function setupInstructorAdminEvents() {
82
+ // Permission Check
83
+ const user = auth.currentUser;
84
+ if (!user) {
85
+ alert("請先登入");
86
+ window.location.hash = ''; // Back to Landing
87
+ return;
88
+ }
89
+
90
+ checkInstructorPermission(user).then(inst => {
91
+ if (!inst || !inst.permissions?.includes('manage_instructors')) {
92
+ alert("您沒有權限管理講師");
93
+ window.location.hash = 'instructor';
94
+ return;
95
+ }
96
+ loadInstructorList();
97
+ }).catch(e => {
98
+ console.error(e);
99
+ alert("權限驗證失敗");
100
+ window.location.hash = 'instructor';
101
+ });
102
+
103
+
104
+ // Navigation
105
+ document.getElementById('back-instructor-btn').addEventListener('click', () => {
106
+ window.location.hash = 'instructor';
107
+ });
108
+
109
+ // Modal Handling
110
+ document.getElementById('add-instructor-btn').addEventListener('click', () => {
111
+ document.getElementById('input-email').value = '';
112
+ document.getElementById('input-name').value = '';
113
+ document.querySelectorAll('.perm-check').forEach(c => c.checked = true);
114
+ document.getElementById('instructor-modal').classList.remove('hidden');
115
+ });
116
+
117
+ // Save Logic
118
+ document.getElementById('save-instructor-btn').addEventListener('click', async () => {
119
+ const email = document.getElementById('input-email').value.trim();
120
+ const name = document.getElementById('input-name').value.trim();
121
+ const permissions = Array.from(document.querySelectorAll('.perm-check:checked')).map(c => c.value);
122
+
123
+ if (!email || !name) {
124
+ alert("請填寫完整資訊");
125
+ return;
126
+ }
127
+
128
+ try {
129
+ await addInstructor(email, name, permissions);
130
+ alert("新增成功");
131
+ document.getElementById('instructor-modal').classList.add('hidden');
132
+ loadInstructorList();
133
+ } catch (e) {
134
+ console.error(e);
135
+ alert("新增失敗: " + e.message);
136
+ }
137
+ });
138
+
139
+ // Global Helpers
140
+ window.removeInstructorConfirm = async (email) => {
141
+ if (confirm(`確定要移除講師權限 (${email}) 嗎?`)) {
142
+ try {
143
+ await removeInstructor(email);
144
+ loadInstructorList();
145
+ } catch (e) {
146
+ console.error(e);
147
+ alert("移除失敗: " + e.message);
148
+ }
149
+ }
150
+ };
151
+ }
152
+
153
+ async function loadInstructorList() {
154
+ const list = document.getElementById('instructors-list');
155
+ list.innerHTML = '<tr><td colspan="4" class="p-8 text-center text-gray-500">載入中...</td></tr>';
156
+
157
+ try {
158
+ const instructors = await getInstructors();
159
+
160
+ if (instructors.length === 0) {
161
+ list.innerHTML = '<tr><td colspan="4" class="p-8 text-center text-gray-500">目前沒有其他講師</td></tr>';
162
+ return;
163
+ }
164
+
165
+ list.innerHTML = instructors.map(inst => `
166
+ <tr class="hover:bg-gray-700/50 transition-colors border-b border-gray-800">
167
+ <td class="p-4 font-mono text-gray-300">
168
+ ${inst.email}
169
+ ${inst.role === 'admin' ? '<span class="ml-2 text-xs bg-red-900 text-red-300 px-2 py-0.5 rounded border border-red-700">Admin</span>' : ''}
170
+ </td>
171
+ <td class="p-4 font-bold text-white">${inst.name}</td>
172
+ <td class="p-4">
173
+ <div class="flex flex-wrap gap-1">
174
+ ${(inst.permissions || []).map(p => `
175
+ <span class="text-xs bg-indigo-900/50 text-indigo-300 px-2 py-0.5 rounded border border-indigo-700">${p}</span>
176
+ `).join('')}
177
+ </div>
178
+ </td>
179
+ <td class="p-4 text-right">
180
+ ${inst.role !== 'admin' ? `
181
+ <button onclick="window.removeInstructorConfirm('${inst.email}')" class="text-red-400 hover:text-white bg-red-900/20 hover:bg-red-600 px-3 py-1 rounded transition-colors text-sm">
182
+ 移除
183
+ </button>
184
+ ` : '<span class="text-gray-600 text-sm">不可變更</span>'}
185
+ </td>
186
+ </tr>
187
+ `).join('');
188
+
189
+ } catch (e) {
190
+ console.error(e);
191
+ list.innerHTML = '<tr><td colspan="4" class="p-8 text-center text-red-500">載入失敗</td></tr>';
192
+ }
193
+ }