key0303 commited on
Commit
eaaf09c
·
verified ·
1 Parent(s): 624965c

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +628 -19
index.html CHANGED
@@ -1,19 +1,628 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ServiceDesk Enterprise</title>
7
+ <!-- Иконки FontAwesome -->
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <!-- Шрифт Inter -->
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+
12
+ <style>
13
+ /* --- CSS VARIABLES & THEME --- */
14
+ :root {
15
+ --primary: #2563eb;
16
+ --primary-hover: #1d4ed8;
17
+ --secondary: #64748b;
18
+ --success: #10b981;
19
+ --warning: #f59e0b;
20
+ --danger: #ef4444;
21
+ --bg-body: #f1f5f9;
22
+ --bg-card: #ffffff;
23
+ --text-main: #0f172a;
24
+ --text-muted: #64748b;
25
+ --border: #e2e8f0;
26
+ --radius: 12px;
27
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
28
+ --font: 'Inter', sans-serif;
29
+ --nav-width: 260px;
30
+ }
31
+
32
+ [data-theme="dark"] {
33
+ --bg-body: #0f172a;
34
+ --bg-card: #1e293b;
35
+ --text-main: #f8fafc;
36
+ --text-muted: #94a3b8;
37
+ --border: #334155;
38
+ }
39
+
40
+ /* RESET & BASE */
41
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
42
+ body { font-family: var(--font); background: var(--bg-body); color: var(--text-main); height: 100vh; overflow: hidden; display: flex; flex-direction: column; transition: background 0.3s, color 0.3s; }
43
+ a { text-decoration: none; color: inherit; cursor: pointer; }
44
+ ul { list-style: none; }
45
+ button { cursor: pointer; font-family: inherit; border: none; background: none; }
46
+
47
+ /* UTILITIES */
48
+ .hidden { display: none !important; }
49
+ .flex { display: flex; }
50
+ .flex-col { flex-direction: column; }
51
+ .items-center { align-items: center; }
52
+ .justify-between { justify-content: space-between; }
53
+ .gap-2 { gap: 0.5rem; }
54
+ .gap-4 { gap: 1rem; }
55
+ .p-4 { padding: 1rem; }
56
+ .p-6 { padding: 1.5rem; }
57
+ .text-sm { font-size: 0.875rem; }
58
+ .text-lg { font-size: 1.125rem; font-weight: 600; }
59
+ .text-xl { font-size: 1.25rem; font-weight: 700; }
60
+ .text-danger { color: var(--danger); }
61
+ .text-success { color: var(--success); }
62
+ .text-muted { color: var(--text-muted); }
63
+ .w-full { width: 100%; }
64
+ .h-full { height: 100%; }
65
+
66
+ /* --- COMPONENTS --- */
67
+
68
+ /* Buttons */
69
+ .btn { padding: 0.6rem 1.2rem; border-radius: 8px; font-weight: 500; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; justify-content: center; }
70
+ .btn-primary { background: var(--primary); color: white; }
71
+ .btn-primary:hover { background: var(--primary-hover); transform: translateY(-1px); }
72
+ .btn-secondary { background: var(--bg-body); border: 1px solid var(--border); color: var(--text-main); }
73
+ .btn-secondary:hover { background: var(--border); }
74
+ .btn-danger { background: var(--danger); color: white; }
75
+ .btn-icon { padding: 0.5rem; border-radius: 50%; }
76
+
77
+ /* Inputs */
78
+ .form-group { margin-bottom: 1rem; position: relative; }
79
+ .form-label { display: block; margin-bottom: 0.4rem; font-size: 0.9rem; font-weight: 500; color: var(--text-muted); }
80
+ .form-input, .form-select, .form-textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); color: var(--text-main); font-family: inherit; transition: border 0.2s; }
81
+ .form-input:focus, .form-textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
82
+ .form-textarea { resize: vertical; min-height: 100px; }
83
+
84
+ /* Cards */
85
+ .card { background: var(--bg-card); border-radius: var(--radius); padding: 1.5rem; box-shadow: var(--shadow); border: 1px solid var(--border); }
86
+
87
+ /* Badges */
88
+ .badge { padding: 0.25rem 0.6rem; border-radius: 99px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
89
+ .badge-new { background: #e0f2fe; color: #0284c7; }
90
+ .badge-in_progress { background: #fef3c7; color: #d97706; }
91
+ .badge-resolved { background: #dcfce7; color: #16a34a; }
92
+ .badge-high { background: #fee2e2; color: #dc2626; }
93
+ .badge-medium { background: #fef9c3; color: #ca8a04; }
94
+
95
+ /* --- LAYOUT --- */
96
+
97
+ /* Header */
98
+ .app-header { height: 64px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; z-index: 10; }
99
+ .brand-area { display: flex; align-items: center; gap: 12px; }
100
+ .brand-logo { height: 40px; width: 40px; object-fit: contain; }
101
+ .brand-text h1 { font-size: 1.2rem; font-weight: 700; line-height: 1.2; }
102
+ .typewriter { overflow: hidden; white-space: nowrap; border-right: 2px solid var(--primary); animation: blink 0.75s step-end infinite; max-width: 300px; }
103
+ @keyframes blink { from, to { border-color: transparent } 50% { border-color: var(--primary) } }
104
+
105
+ /* Sidebar */
106
+ .app-layout { display: flex; height: calc(100vh - 64px); }
107
+ .sidebar { width: var(--nav-width); background: var(--bg-card); border-right: 1px solid var(--border); padding: 1.5rem 1rem; display: flex; flex-direction: column; gap: 0.5rem; overflow-y: auto; }
108
+ .nav-item { display: flex; align-items: center; gap: 12px; padding: 0.8rem 1rem; border-radius: 8px; color: var(--text-muted); font-weight: 500; transition: all 0.2s; }
109
+ .nav-item:hover, .nav-item.active { background: rgba(37, 99, 235, 0.1); color: var(--primary); }
110
+ .nav-item i { width: 20px; text-align: center; }
111
+
112
+ /* Main Content */
113
+ .main-content { flex: 1; padding: 2rem; overflow-y: auto; position: relative; }
114
+ .page { display: none; animation: fadeIn 0.3s ease-out; }
115
+ .page.active { display: block; }
116
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
117
+
118
+ /* --- SPECIFIC VIEWS --- */
119
+
120
+ /* Login */
121
+ .login-container { display: flex; align-items: center; justify-content: center; height: 100vh; background: linear-gradient(135deg, var(--bg-body) 0%, #dbeafe 100%); }
122
+ .login-card { width: 100%; max-width: 400px; text-align: center; }
123
+
124
+ /* Ticket List */
125
+ .ticket-item { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin-bottom: 0.8rem; display: flex; align-items: center; justify-content: space-between; cursor: pointer; transition: transform 0.1s; }
126
+ .ticket-item:hover { transform: translateX(4px); border-color: var(--primary); }
127
+ .ticket-meta { display: flex; gap: 1rem; font-size: 0.85rem; color: var(--text-muted); margin-top: 4px; }
128
+
129
+ /* Chat */
130
+ .chat-container { display: flex; flex-direction: column; height: calc(100vh - 180px); background: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); }
131
+ .chat-header { padding: 1rem; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.5); }
132
+ .chat-messages { flex: 1; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem; background: var(--bg-body); }
133
+ .chat-input-area { padding: 1rem; border-top: 1px solid var(--border); display: flex; gap: 0.8rem; background: var(--bg-card); border-radius: 0 0 var(--radius) var(--radius); }
134
+
135
+ .message { max-width: 80%; padding: 0.8rem 1rem; border-radius: 12px; position: relative; font-size: 0.95rem; line-height: 1.4; }
136
+ .message.mine { align-self: flex-end; background: var(--primary); color: white; border-bottom-right-radius: 2px; }
137
+ .message.theirs { align-self: flex-start; background: var(--bg-card); border: 1px solid var(--border); border-bottom-left-radius: 2px; }
138
+ .message-time { font-size: 0.7rem; opacity: 0.7; margin-top: 4px; display: block; text-align: right; }
139
+ .message-sys { align-self: center; background: transparent; color: var(--text-muted); font-size: 0.8rem; font-style: italic; border: 1px dashed var(--border); }
140
+
141
+ /* Autocomplete */
142
+ .autocomplete-suggestions { position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-card); border: 1px solid var(--border); border-radius: 0 0 8px 8px; max-height: 200px; overflow-y: auto; z-index: 50; box-shadow: var(--shadow); display: none; }
143
+ .autocomplete-item { padding: 0.6rem 1rem; cursor: pointer; font-size: 0.9rem; }
144
+ .autocomplete-item:hover { background: var(--bg-body); color: var(--primary); }
145
+
146
+ /* Priority Toggles */
147
+ .priority-group { display: flex; gap: 0.5rem; background: var(--bg-body); padding: 4px; border-radius: 8px; width: fit-content; }
148
+ .priority-btn { padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; font-weight: 500; color: var(--text-muted); transition: all 0.2s; }
149
+ .priority-btn.selected { color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
150
+ .p-high.selected { background: var(--danger); }
151
+ .p-medium.selected { background: var(--warning); }
152
+ .p-low.selected { background: var(--secondary); }
153
+
154
+ /* Toast */
155
+ .toast-container { position: fixed; bottom: 2rem; right: 2rem; z-index: 100; display: flex; flex-direction: column; gap: 0.5rem; }
156
+ .toast { padding: 1rem 1.5rem; background: var(--text-main); color: var(--bg-card); border-radius: 8px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); animation: slideIn 0.3s ease-out; display: flex; align-items: center; gap: 10px; }
157
+ @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
158
+
159
+ /* Responsive */
160
+ @media (max-width: 768px) {
161
+ .app-layout { flex-direction: column; }
162
+ .sidebar { width: 100%; height: auto; border-right: none; border-bottom: 1px solid var(--border); padding: 0.5rem; flex-direction: row; overflow-x: auto; }
163
+ .nav-item span { display: none; }
164
+ .nav-item { justify-content: center; padding: 0.8rem; }
165
+ .main-content { padding: 1rem; }
166
+ .typewriter { max-width: 150px; }
167
+ }
168
+ </style>
169
+ </head>
170
+ <body>
171
+
172
+ <!-- APP CONTAINER -->
173
+ <div id="app"></div>
174
+
175
+ <!-- TOAST CONTAINER -->
176
+ <div class="toast-container" id="toastContainer"></div>
177
+
178
+ <script>
179
+ /**
180
+ * SERVICE DESK ENTERPRISE - Single File HTML Implementation
181
+ * Uses LocalStorage to simulate a real MySQL database.
182
+ */
183
+
184
+ // --- 1. CONFIG & UTILS ---
185
+ const Utils = {
186
+ id: () => Math.random().toString(36).substr(2, 9),
187
+ date: (ts = Date.now()) => new Date(ts).toLocaleString('ru-RU'),
188
+ relativeTime: (ts) => {
189
+ const diff = Date.now() - ts;
190
+ const hours = Math.floor(diff / (1000 * 60 * 60));
191
+ if (hours < 1) return "Сейчас";
192
+ if (hours < 24) return `${hours} ч. назад`;
193
+ return new Date(ts).toLocaleDateString('ru-RU');
194
+ },
195
+ sanitize: (str) => {
196
+ const temp = document.createElement('div');
197
+ temp.textContent = str;
198
+ return temp.innerHTML;
199
+ }
200
+ };
201
+
202
+ const Translations = {
203
+ ru: {
204
+ appName: "ServiceDesk",
205
+ slogan: "Мы решаем проблемы. Вы работаете.",
206
+ login: "Войти через SSO",
207
+ logout: "Выход",
208
+ dashboard: "Рабочий стол",
209
+ tickets: "Заявки",
210
+ newTicket: "Новая заявка",
211
+ knowledge: "База знаний",
212
+ admin: "Админка",
213
+ profile: "Профиль",
214
+ title: "Тема",
215
+ description: "Описание",
216
+ priority: "Приоритет",
217
+ status: "Статус",
218
+ submit: "Отправить",
219
+ cancel: "Отмена",
220
+ save: "Сохранить",
221
+ typeMsg: "Введите сообщение...",
222
+ attach: "Вложить",
223
+ send: "Отправить",
224
+ myTickets: "Мои заявки",
225
+ assignedToMe: "Назначены мне",
226
+ overdue: "Просрочено",
227
+ closed: "Закрытые",
228
+ open: "Открытые",
229
+ high: "Срочная",
230
+ medium: "Средняя",
231
+ low: "Обычная",
232
+ searchPlaceholder: "Поиск по номеру или теме...",
233
+ welcome: "Добро пожаловать",
234
+ aiHelper: "AI Помощник",
235
+ aiThinking: "Думаю...",
236
+ createDraft: "Создать черновик заявки",
237
+ sync1C: "Синхронизация 1С",
238
+ export: "Экспортировать",
239
+ settings: "Настройки",
240
+ branding: "Брендинг",
241
+ integration: "Интеграции",
242
+ news: "Новости"
243
+ },
244
+ en: {
245
+ appName: "ServiceDesk",
246
+ slogan: "We fix problems. You work.",
247
+ login: "Login via SSO",
248
+ logout: "Logout",
249
+ dashboard: "Dashboard",
250
+ tickets: "Tickets",
251
+ newTicket: "New Ticket",
252
+ knowledge: "Knowledge Base",
253
+ admin: "Admin Panel",
254
+ profile: "Profile",
255
+ title: "Subject",
256
+ description: "Description",
257
+ priority: "Priority",
258
+ status: "Status",
259
+ submit: "Submit",
260
+ cancel: "Cancel",
261
+ save: "Save",
262
+ typeMsg: "Type a message...",
263
+ attach: "Attach",
264
+ send: "Send",
265
+ myTickets: "My Tickets",
266
+ assignedToMe: "Assigned to me",
267
+ overdue: "Overdue",
268
+ closed: "Closed",
269
+ open: "Open",
270
+ high: "High",
271
+ medium: "Medium",
272
+ low: "Low",
273
+ searchPlaceholder: "Search by ID or subject...",
274
+ welcome: "Welcome",
275
+ aiHelper: "AI Assistant",
276
+ aiThinking: "Thinking...",
277
+ createDraft: "Create Ticket Draft",
278
+ sync1C: "1C Sync",
279
+ export: "Export",
280
+ settings: "Settings",
281
+ branding: "Branding",
282
+ integration: "Integrations",
283
+ news: "News"
284
+ }
285
+ };
286
+
287
+ // --- 2. DATA LAYER (Simulating MySQL) ---
288
+ class DB {
289
+ constructor() {
290
+ this.prefix = "sd_";
291
+ this.init();
292
+ }
293
+
294
+ init() {
295
+ if (!localStorage.getItem(this.prefix + 'users')) {
296
+ // Seed Initial Data
297
+ const users = [
298
+ { id: 'u1', role: 'admin', name: 'Admin User', email: 'admin@jinr.ru', dept: 'IT', avatar: 'https://ui-avatars.com/api/?name=Admin+User&background=0D8ABC&color=fff' },
299
+ { id: 'u2', role: 'agent', name: 'Ivan Petrov', email: 'ivan@jinr.ru', dept: 'Support', avatar: 'https://ui-avatars.com/api/?name=Ivan+Petrov&background=10b981&color=fff' },
300
+ { id: 'u3', role: 'user', name: 'Anna Smith', email: 'anna@jinr.ru', dept: 'HR', building: '1', room: '202', avatar: 'https://ui-avatars.com/api/?name=Anna+Smith&background=f59e0b&color=fff' }
301
+ ];
302
+ const tickets = [
303
+ {
304
+ id: 't1', authorId: 'u3', assigneeId: 'u2',
305
+ title: 'Не работает принтер в 202 кабинете',
306
+ description: 'Принтер HP LaserJet не включается, мигает оранжевый индикатор.',
307
+ priority: 'medium', status: 'new',
308
+ createdAt: Date.now() - (1000 * 60 * 60 * 25), // 25 hours ago (Overdue simulation)
309
+ building: '1', room: '202'
310
+ }
311
+ ];
312
+ const messages = [
313
+ { id: 'm1', ticketId: 't1', authorId: 'u3', body: 'Добрый день, помогите пожалуйста с принтером.', createdAt: Date.now() - (1000 * 60 * 60 * 25) }
314
+ ];
315
+
316
+ const kb = [
317
+ { id: 'k1', titleRU: 'Как настроить почту', titleEN: 'How to setup email', bodyRU: 'Откройте Outlook и введите логин...', bodyEN: 'Open Outlook and enter login...', tags: ['mail', 'outlook'] }
318
+ ];
319
+
320
+ this.save('users', users);
321
+ this.save('tickets', tickets);
322
+ this.save('messages', messages);
323
+ this.save('kb', kb);
324
+ this.save('branding', { primaryColor: '#2563eb', name: 'JINR ServiceDesk' });
325
+ }
326
+ }
327
+
328
+ get(table) { return JSON.parse(localStorage.getItem(this.prefix + table) || '[]'); }
329
+ save(table, data) { localStorage.setItem(this.prefix + table, JSON.stringify(data)); }
330
+
331
+ add(table, item) {
332
+ const data = this.get(table);
333
+ data.push(item);
334
+ this.save(table, data);
335
+ return item;
336
+ }
337
+
338
+ update(table, id, updates) {
339
+ const data = this.get(table);
340
+ const idx = data.findIndex(i => i.id === id);
341
+ if (idx !== -1) {
342
+ data[idx] = { ...data[idx], ...updates, updatedAt: Date.now() };
343
+ this.save(table, data);
344
+ return data[idx];
345
+ }
346
+ return null;
347
+ }
348
+
349
+ findUser(id) { return this.get('users').find(u => u.id === id); }
350
+ getTickets(filter = {}) {
351
+ let tickets = this.get('tickets');
352
+ // Logic for filters can be extended here
353
+ return tickets.sort((a, b) => b.createdAt - a.createdAt);
354
+ }
355
+ }
356
+
357
+ const db = new DB();
358
+
359
+ // --- 3. STATE MANAGEMENT ---
360
+ const State = {
361
+ user: null, // Current logged in user
362
+ lang: 'ru',
363
+
364
+ login(user) {
365
+ this.user = user;
366
+ Render.app();
367
+
368
+ // Typewriter effect after render
369
+ setTimeout(() => {
370
+ const el = document.getElementById('typewriter-text');
371
+ if (el) startTypewriter(el, Translations[this.lang].slogan);
372
+ }, 500);
373
+ },
374
+
375
+ logout() {
376
+ this.user = null;
377
+ Render.app();
378
+ },
379
+
380
+ setLang(l) {
381
+ this.lang = l;
382
+ Render.app(); // Re-render to apply translations
383
+ }
384
+ };
385
+
386
+ // --- 4. UI RENDERER ---
387
+ const Render = {
388
+ app() {
389
+ const app = document.getElementById('app');
390
+ app.innerHTML = '';
391
+
392
+ if (!State.user) {
393
+ app.appendChild(Views.Login());
394
+ } else {
395
+ app.appendChild(Views.Layout());
396
+ Router.init();
397
+ }
398
+ }
399
+ };
400
+
401
+ const Views = {
402
+ Login() {
403
+ const div = document.createElement('div');
404
+ div.className = 'login-container';
405
+ div.innerHTML = `
406
+ <div class="card login-card">
407
+ <img src="https://upload.wikimedia.org/wikipedia/commons/2/23/JINR_logo.png" alt="Logo" style="height: 60px; margin-bottom: 1rem;">
408
+ <h2 class="text-xl" style="margin-bottom: 0.5rem;">${Translations[State.lang].appName}</h2>
409
+ <p class="text-muted" style="margin-bottom: 2rem;">Enterprise Service Desk</p>
410
+
411
+ <button class="btn btn-primary w-full" onclick="Actions.mockSSO()">
412
+ <i class="fa-solid fa-right-to-bracket"></i> ${Translations[State.lang].login}
413
+ </button>
414
+
415
+ <div style="margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1rem;">
416
+ <small class="text-muted">Demo Users:</small><br>
417
+ <a onclick="Actions.quickLogin('u1')" class="text-sm text-primary">Admin</a> |
418
+ <a onclick="Actions.quickLogin('u2')" class="text-sm text-primary">Agent</a> |
419
+ <a onclick="Actions.quickLogin('u3')" class="text-sm text-primary">User</a>
420
+ </div>
421
+ <div style="margin-top: 10px;">
422
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="font-size: 0.7rem; color: var(--text-muted);">Built with anycoder</a>
423
+ </div>
424
+ </div>
425
+ `;
426
+ return div;
427
+ },
428
+
429
+ Layout() {
430
+ const div = document.createElement('div');
431
+ div.className = 'h-full flex-col';
432
+
433
+ const branding = db.get('branding');
434
+ const navItems = [
435
+ { id: 'dashboard', icon: 'fa-table-columns', label: Translations[State.lang].dashboard },
436
+ { id: 'tickets', icon: 'fa-ticket', label: Translations[State.lang].tickets },
437
+ { id: 'knowledge', icon: 'fa-book-open', label: Translations[State.lang].knowledge },
438
+ { id: 'news', icon: 'fa-newspaper', label: Translations[State.lang].news },
439
+ ];
440
+
441
+ if (State.user.role === 'admin') {
442
+ navItems.push({ id: 'admin', icon: 'fa-screwdriver-wrench', label: Translations[State.lang].admin });
443
+ }
444
+
445
+ div.innerHTML = `
446
+ <header class="app-header">
447
+ <div class="brand-area">
448
+ <img src="https://upload.wikimedia.org/wikipedia/commons/2/23/JINR_logo.png" class="brand-logo" alt="Logo">
449
+ <div class="brand-text">
450
+ <h1>${branding.name}</h1>
451
+ <div class="typewriter text-sm text-muted" id="typewriter-text"></div>
452
+ </div>
453
+ </div>
454
+ <div class="flex items-center gap-4">
455
+ <button class="btn btn-secondary text-sm" onclick="Actions.toggleLang()">
456
+ <i class="fa-solid fa-globe"></i> ${State.lang.toUpperCase()}
457
+ </button>
458
+ <div class="flex items-center gap-2">
459
+ <img src="${State.user.avatar}" class="btn-icon" style="background:#ddd; border-radius:50%;">
460
+ <span class="text-sm font-medium">${State.user.name}</span>
461
+ <button class="text-muted hover:text-danger" onclick="State.logout()"><i class="fa-solid fa-power-off"></i></button>
462
+ </div>
463
+ </div>
464
+ </header>
465
+
466
+ <div class="app-layout">
467
+ <nav class="sidebar">
468
+ ${navItems.map(item => `
469
+ <a class="nav-item" data-page="${item.id}">
470
+ <i class="fa-solid ${item.icon}"></i>
471
+ <span>${item.label}</span>
472
+ </a>
473
+ `).join('')}
474
+
475
+ <div style="margin-top: auto; padding: 1rem 0;">
476
+ <div class="card p-4" style="background: linear-gradient(135deg, var(--primary), var(--primary-hover)); color: white; border: none;">
477
+ <div class="text-sm font-bold mb-1"><i class="fa-solid fa-robot"></i> AI Helper</div>
478
+ <div class="text-xs opacity-80 mb-2">Нужна помощь?</div>
479
+ <button class="btn w-full" style="background: rgba(255,255,255,0.2); color: white; font-size: 0.8rem;" onclick="Actions.openAIChat()">Спросить</button>
480
+ </div>
481
+ </div>
482
+ </nav>
483
+
484
+ <main class="main-content" id="main-content">
485
+ <!-- Dynamic Content Here -->
486
+ </main>
487
+ </div>
488
+ `;
489
+
490
+ // Bind Nav Clicks
491
+ setTimeout(() => {
492
+ div.querySelectorAll('.nav-item').forEach(el => {
493
+ el.addEventListener('click', () => {
494
+ div.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
495
+ el.classList.add('active');
496
+ Router.navigate(el.dataset.page);
497
+ });
498
+ });
499
+ // Set default active
500
+ div.querySelector('.nav-item').classList.add('active');
501
+ Router.navigate('dashboard');
502
+ }, 0);
503
+
504
+ return div;
505
+ },
506
+
507
+ Dashboard() {
508
+ const tickets = db.getTickets();
509
+ const userTickets = tickets.filter(t => t.authorId === State.user.id || t.assigneeId === State.user.id);
510
+ const openCount = userTickets.filter(t => t.status !== 'closed').length;
511
+
512
+ return `
513
+ <div class="page active">
514
+ <h2 class="text-xl mb-4">${Translations[State.lang].dashboard}, ${State.user.name.split(' ')[0]}!</h2>
515
+
516
+ <div class="flex gap-4 mb-6" style="flex-wrap: wrap;">
517
+ <div class="card" style="flex:1; min-width: 200px;">
518
+ <div class="text-muted text-sm">Открытых заявок</div>
519
+ <div class="text-xl" style="color: var(--primary);">${openCount}</div>
520
+ </div>
521
+ <div class="card" style="flex:1; min-width: 200px;">
522
+ <div class="text-muted text-sm">Ваш статус</div>
523
+ <div class="text-xl text-success"><i class="fa-solid fa-circle-check"></i> Online</div>
524
+ </div>
525
+ </div>
526
+
527
+ <h3 class="text-lg mb-3">Последние действия</h3>
528
+ <div class="card">
529
+ ${userTickets.length === 0 ? '<p class="text-muted">Нет активных заявок</p>' :
530
+ userTickets.slice(0, 3).map(t => Components.TicketRow(t)).join('')}
531
+ <button class="btn btn-primary w-full mt-3" onclick="Router.navigate('new-ticket')">+ ${Translations[State.lang].newTicket}</button>
532
+ </div>
533
+ </div>
534
+ `;
535
+ },
536
+
537
+ TicketList() {
538
+ const tickets = db.getTickets();
539
+ // Filter logic based on role would go here
540
+ const myTickets = tickets.filter(t => t.authorId === State.user.id);
541
+
542
+ return `
543
+ <div class="page active">
544
+ <div class="flex justify-between items-center mb-4">
545
+ <h2 class="text-xl">${Translations[State.lang].tickets}</h2>
546
+ <button class="btn btn-primary" onclick="Router.navigate('new-ticket')">
547
+ <i class="fa-solid fa-plus"></i> ${Translations[State.lang].newTicket}
548
+ </button>
549
+ </div>
550
+
551
+ <div class="flex gap-2 mb-4" style="overflow-x:auto;">
552
+ <button class="btn btn-secondary text-sm active">Мои</button>
553
+ <button class="btn btn-secondary text-sm">Открытые</button>
554
+ <button class="btn btn-secondary text-sm">Просрочено</button>
555
+ </div>
556
+
557
+ <div class="form-group">
558
+ <input type="text" class="form-input" placeholder="${Translations[State.lang].searchPlaceholder}">
559
+ </div>
560
+
561
+ <div>
562
+ ${myTickets.map(t => Components.TicketRow(t)).join('')}
563
+ </div>
564
+ </div>
565
+ `;
566
+ },
567
+
568
+ NewTicket() {
569
+ return `
570
+ <div class="page active">
571
+ <h2 class="text-xl mb-4">${Translations[State.lang].newTicket}</h2>
572
+ <div class="card">
573
+ <div class="form-group">
574
+ <label class="form-label">${Translations[State.lang].title}</label>
575
+ <input type="text" id="t-title" class="form-input" placeholder="Кратко опишите проблему">
576
+ </div>
577
+
578
+ <div class="form-group" style="z-index: 20;">
579
+ <label class="form-label">${Translations[State.lang].description}</label>
580
+ <textarea id="t-desc" class="form-textarea" placeholder="Подробности..." oninput="Actions.autocomplete(this)"></textarea>
581
+ <div id="autocomplete-box" class="autocomplete-suggestions"></div>
582
+ </div>
583
+
584
+ <div class="flex gap-4 mb-4">
585
+ <div class="form-group flex-1">
586
+ <label class="form-label">Корпус / Кабинет</label>
587
+ <div class="flex gap-2">
588
+ <input type="text" class="form-input" value="${State.user.building || ''}" placeholder="Корп." id="t-build">
589
+ <input type="text" class="form-input" value="${State.user.room || ''}" placeholder="Каб." id="t-room">
590
+ </div>
591
+ </div>
592
+ <div class="form-group flex-1">
593
+ <label class="form-label">Желаемое время визита</label>
594
+ <input type="datetime-local" class="form-input" id="t-date">
595
+ </div>
596
+ </div>
597
+
598
+ <div class="form-group">
599
+ <label class="form-label">${Translations[State.lang].priority}</label>
600
+ <div class="priority-group">
601
+ <div class="priority-btn p-low selected" onclick="Actions.selectPriority(this, 'low')">${Translations[State.lang].low}</div>
602
+ <div class="priority-btn p-medium" onclick="Actions.selectPriority(this, 'medium')">${Translations[State.lang].medium}</div>
603
+ <div class="priority-btn p-high" onclick="Actions.selectPriority(this, 'high')">${Translations[State.lang].high}</div>
604
+ </div>
605
+ <input type="hidden" id="t-priority" value="low">
606
+ </div>
607
+
608
+ <div class="flex justify-end gap-2 mt-6">
609
+ <button class="btn btn-secondary" onclick="Router.navigate('tickets')">${Translations[State.lang].cancel}</button>
610
+ <button class="btn btn-primary" onclick="Actions.submitTicket()">${Translations[State.lang].submit}</button>
611
+ </div>
612
+ </div>
613
+ </div>
614
+ `;
615
+ },
616
+
617
+ TicketDetail(id) {
618
+ const t = db.getTickets().find(x => x.id === id);
619
+ if (!t) return `<div>Ticket not found</div>`;
620
+
621
+ const msgs = db.get('messages').filter(m => m.ticketId === id).sort((a,b) => a.createdAt - b.createdAt);
622
+
623
+ // Check Overdue
624
+ const isOverdue = (t.status === 'new' || t.status === 'in_progress') && (Date.now() - t.createdAt > 24 * 60 * 60 * 1000);
625
+
626
+ return `
627
+ <div class="page active">
628
+ <button class="btn btn-secondary text-sm mb-4" onclick="Router.navigate('tickets')"><i class="fa-solid fa-arrow-left"></i> Назад</button