index.html (15629B)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>LibEuFin EbiSync - File Submission Portal</title> 7 <link rel="preconnect" href="https://fonts.googleapis.com"> 8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet"> 10 <style> 11 :root { 12 --cream: #FAF8F3; 13 --charcoal: #2B2B2B; 14 --rust: #C1503D; 15 --sage: #8B9A7E; 16 --gold: #D4AF37; 17 --shadow: rgba(43, 43, 43, 0.08); 18 } 19 20 * { 21 margin: 0; 22 padding: 0; 23 box-sizing: border-box; 24 } 25 26 body { 27 font-family: 'Montserrat', sans-serif; 28 background: var(--cream); 29 color: var(--charcoal); 30 line-height: 1.7; 31 overflow-x: hidden; 32 } 33 34 /* Animated background texture */ 35 body::before { 36 content: ''; 37 position: fixed; 38 top: 0; 39 left: 0; 40 width: 100%; 41 height: 100%; 42 background-image: 43 repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(43, 43, 43, 0.015) 2px, rgba(43, 43, 43, 0.015) 4px), 44 repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(43, 43, 43, 0.015) 2px, rgba(43, 43, 43, 0.015) 4px); 45 pointer-events: none; 46 z-index: 0; 47 } 48 49 .container { 50 max-width: 1200px; 51 margin: 0 auto; 52 padding: 0 2rem; 53 position: relative; 54 z-index: 1; 55 } 56 57 header { 58 padding: 2rem 0 2rem; 59 } 60 61 h1 { 62 font-family: 'Cormorant Garamond', serif; 63 font-size: 4.5rem; 64 font-weight: 300; 65 letter-spacing: -0.02em; 66 line-height: 1.1; 67 margin-bottom: 1rem; 68 } 69 70 .subtitle { 71 font-size: 1rem; 72 letter-spacing: 0.15em; 73 text-transform: uppercase; 74 color: var(--rust); 75 font-weight: 500; 76 } 77 78 .version-text { 79 margin-left: auto; 80 font-size: 0.75rem; 81 color: rgba(43, 43, 43, 0.5); 82 font-family: 'Courier New', monospace; 83 } 84 85 main { 86 display: grid; 87 grid-template-columns: 1fr 1fr; 88 gap: 3rem; 89 margin-bottom: 6rem; 90 } 91 92 section { 93 background: white; 94 padding: 1rem 2rem; 95 border: 1px solid rgba(43, 43, 43, 0.1); 96 position: relative; 97 transition: all 0.4s ease; 98 } 99 100 section::before { 101 content: ''; 102 position: absolute; 103 top: 0; 104 left: 0; 105 width: 4px; 106 height: 100%; 107 background: var(--rust); 108 transform: scaleY(0); 109 transition: transform 0.4s ease; 110 } 111 112 section:hover::before { 113 transform: scaleY(1); 114 } 115 116 h2 { 117 font-family: 'Cormorant Garamond', serif; 118 font-size: 2.5rem; 119 font-weight: 400; 120 margin-bottom: 0.5rem; 121 letter-spacing: -0.01em; 122 } 123 124 .order-card { 125 padding: 1.5rem; 126 margin-bottom: 1rem; 127 border: 1px solid rgba(43, 43, 43, 0.1); 128 cursor: pointer; 129 transition: all 0.3s ease; 130 position: relative; 131 background: var(--cream); 132 } 133 134 .order-card:hover { 135 transform: translateX(8px); 136 border-color: var(--rust); 137 box-shadow: -8px 0 0 var(--rust); 138 } 139 140 .order-card.selected { 141 background: var(--charcoal); 142 color: var(--cream); 143 border-color: var(--charcoal); 144 } 145 146 .order-card.selected .order-id { 147 color: var(--gold); 148 } 149 150 .order-id { 151 font-family: 'Courier New', monospace; 152 color: var(--rust); 153 margin-bottom: 0.5rem; 154 font-weight: bold; 155 } 156 157 .order-description { 158 font-size: 0.875rem; 159 line-height: 1.6; 160 } 161 162 .file-upload-zone { 163 padding: 3rem; 164 border: 2px dashed rgba(43, 43, 43, 0.2); 165 text-align: center; 166 cursor: pointer; 167 position: relative; 168 } 169 170 .file-upload-zone:hover { 171 border-color: var(--rust); 172 background: rgba(193, 80, 61, 0.03); 173 } 174 175 .file-upload-zone.drag-over { 176 border-color: var(--sage); 177 background: rgba(139, 154, 126, 0.1); 178 transform: scale(1.02); 179 } 180 181 .upload-icon { 182 font-size: 3rem; 183 margin-bottom: 1rem; 184 opacity: 0.3; 185 } 186 187 .upload-text { 188 font-size: 1rem; 189 margin-bottom: 0.5rem; 190 } 191 192 .upload-hint { 193 font-size: 0.875rem; 194 opacity: 0.6; 195 } 196 197 input[type="file"] { 198 display: none; 199 } 200 201 .selected-file { 202 margin-top: 1.5rem; 203 padding: 1.5rem; 204 background: var(--cream); 205 border-left: 4px solid var(--sage); 206 } 207 208 .selected-file-name { 209 font-weight: 600; 210 margin-bottom: 0.5rem; 211 } 212 213 .selected-file-size { 214 font-size: 0.875rem; 215 opacity: 0.7; 216 } 217 218 .submit-button { 219 width: 100%; 220 margin-top: 2rem; 221 padding: 1.25rem 2rem; 222 background: var(--charcoal); 223 color: var(--cream); 224 border: none; 225 font-size: 1rem; 226 font-weight: 600; 227 letter-spacing: 0.1em; 228 text-transform: uppercase; 229 cursor: pointer; 230 transition: all 0.3s ease; 231 position: relative; 232 overflow: hidden; 233 } 234 235 .submit-button::before { 236 content: ''; 237 position: absolute; 238 top: 50%; 239 left: 50%; 240 width: 0; 241 height: 0; 242 background: var(--rust); 243 border-radius: 50%; 244 transform: translate(-50%, -50%); 245 transition: width 0.6s ease, height 0.6s ease; 246 } 247 248 .submit-button:hover::before { 249 width: 300%; 250 height: 300%; 251 } 252 253 .submit-button:hover { 254 color: white; 255 } 256 257 .submit-button span { 258 position: relative; 259 z-index: 1; 260 } 261 262 .submit-button:disabled { 263 opacity: 0.4; 264 cursor: not-allowed; 265 } 266 267 .message { 268 margin-top: 2rem; 269 padding: 1.5rem; 270 border-left: 4px solid var(--sage); 271 background: rgba(139, 154, 126, 0.1); 272 animation: fadeIn 0.5s ease forwards; 273 } 274 275 .message.error { 276 border-left-color: var(--rust); 277 background: rgba(193, 80, 61, 0.1); 278 } 279 280 .message.success { 281 border-left-color: var(--sage); 282 background: rgba(139, 154, 126, 0.1); 283 } 284 285 .loading { 286 display: inline-block; 287 width: 20px; 288 height: 20px; 289 border: 2px solid rgba(43, 43, 43, 0.1); 290 border-radius: 50%; 291 border-top-color: var(--charcoal); 292 animation: spin 1s linear infinite; 293 } 294 295 @keyframes spin { 296 to { 297 transform: rotate(360deg); 298 } 299 } 300 301 @media (max-width: 768px) { 302 main { 303 grid-template-columns: 1fr; 304 } 305 306 h1 { 307 font-size: 3rem; 308 } 309 } 310 </style> 311 </head> 312 <body> 313 <div class="container"> 314 <header> 315 <div class="subtitle">LibEuFin EbiSync</div> 316 <h1>File Submission Portal</h1> 317 <div class="version-text" id="versionText">Initializing...</div> 318 </header> 319 320 <main> 321 <section> 322 <h2>Choose Order</h2> 323 <div class="orders-list" id="ordersList"> 324 <div style="text-align: center; padding: 2rem; opacity: 0.5;"> 325 <div class="loading"></div> 326 <p style="margin-top: 1rem;">Loading orders...</p> 327 </div> 328 </div> 329 </section> 330 331 <section> 332 <h2>Submit File</h2> 333 <div class="file-upload-zone" id="uploadZone"> 334 <div class="upload-icon">📄</div> 335 <div class="upload-text">Drop XML file here or click to browse</div> 336 <div class="upload-hint">Accepts .xml files only</div> 337 <input type="file" id="fileInput" accept=".xml,application/xml,text/xml"> 338 </div> 339 <div id="selectedFileInfo"></div> 340 <button class="submit-button" id="submitButton" disabled> 341 <span>Submit Document</span> 342 </button> 343 <div id="messageArea"></div> 344 </section> 345 </main> 346 </div> 347 348 <script> 349 let selectedOrder = null; 350 let selectedFile = null; 351 352 // Initialize 353 function init() { 354 loadConfig() 355 loadOrders(); 356 setupEventListeners(); 357 } 358 359 async function loadConfig() { 360 try { 361 const response = await fetch('/config'); 362 const data = await response.json(); 363 document.getElementById('versionText').textContent = `${data.spa_version} (${data.version})`; 364 } catch (error) { 365 document.getElementById('versionText').textContent = 'Error'; 366 showMessage('Unable to connect to backend server', 'error'); 367 } 368 } 369 370 async function loadOrders() { 371 try { 372 const response = await fetch('/submit'); 373 const data = await response.json(); 374 375 const ordersList = document.getElementById('ordersList'); 376 ordersList.innerHTML = ''; 377 378 data.orders.forEach(order => { 379 const orderCard = document.createElement('div'); 380 orderCard.className = 'order-card'; 381 orderCard.innerHTML = ` 382 <div class="order-id">${order.id}</div> 383 <div class="order-description">${order.description}</div> 384 `; 385 orderCard.onclick = () => selectOrder(order, orderCard); 386 ordersList.appendChild(orderCard); 387 }); 388 } catch (error) { 389 document.getElementById('ordersList').innerHTML = 390 '<p style="text-align: center; opacity: 0.5;">Failed to load orders</p>'; 391 } 392 } 393 394 function selectOrder(order, element) { 395 document.querySelectorAll('.order-card').forEach(card => { 396 card.classList.remove('selected'); 397 }); 398 element.classList.add('selected'); 399 selectedOrder = order; 400 updateSubmitButton(); 401 } 402 403 function setupEventListeners() { 404 const uploadZone = document.getElementById('uploadZone'); 405 const fileInput = document.getElementById('fileInput'); 406 407 uploadZone.addEventListener('click', () => fileInput.click()); 408 409 uploadZone.addEventListener('dragover', (e) => { 410 e.preventDefault(); 411 uploadZone.classList.add('drag-over'); 412 }); 413 414 uploadZone.addEventListener('dragleave', () => { 415 uploadZone.classList.remove('drag-over'); 416 }); 417 418 uploadZone.addEventListener('drop', (e) => { 419 e.preventDefault(); 420 uploadZone.classList.remove('drag-over'); 421 const files = e.dataTransfer.files; 422 if (files.length > 0) { 423 handleFileSelect(files[0]); 424 } 425 }); 426 427 fileInput.addEventListener('change', (e) => { 428 if (e.target.files.length > 0) { 429 handleFileSelect(e.target.files[0]); 430 } 431 }); 432 433 document.getElementById('submitButton').addEventListener('click', submitFile); 434 } 435 436 function handleFileSelect(file) { 437 if (!file.name.endsWith('.xml')) { 438 showMessage('Please select an XML file', 'error'); 439 return; 440 } 441 442 selectedFile = file; 443 const infoDiv = document.getElementById('selectedFileInfo'); 444 infoDiv.innerHTML = ` 445 <div class="selected-file"> 446 <div class="selected-file-name">📎 ${file.name}</div> 447 <div class="selected-file-size">${(file.size / 1024).toFixed(2)} KB</div> 448 </div> 449 `; 450 updateSubmitButton(); 451 } 452 453 function updateSubmitButton() { 454 const button = document.getElementById('submitButton'); 455 button.disabled = !(selectedOrder && selectedFile); 456 } 457 458 async function submitFile() { 459 const button = document.getElementById('submitButton'); 460 button.disabled = true; 461 button.innerHTML = '<span><div class="loading" style="display: inline-block; vertical-align: middle; margin-right: 10px;"></div>Submitting...</span>'; 462 463 const formData = new FormData(); 464 formData.append('order', selectedOrder.id); 465 formData.append('file', selectedFile); 466 467 clearMessage() 468 469 try { 470 const response = await fetch('/submit', { 471 method: 'POST', 472 body: formData 473 }); 474 const data = await response.json(); 475 if (response.ok) { 476 showMessage(`Successfully submitted ${selectedFile.name} with order ${selectedOrder.id} as ${data.order}`, 'success'); 477 // Reset form 478 selectedFile = null; 479 document.getElementById('selectedFileInfo').innerHTML = ''; 480 document.getElementById('fileInput').value = ''; 481 } else { 482 showMessage(`${data.code} - ${data.hint ?? 'Submission failed'}`, 'error'); 483 } 484 } catch (error) { 485 showMessage('Network error: ' + error.message, 'error'); 486 } finally { 487 button.disabled = false; 488 button.innerHTML = '<span>Submit Document</span>'; 489 updateSubmitButton(); 490 } 491 } 492 493 function showMessage(text, type = 'info') { 494 const messageArea = document.getElementById('messageArea'); 495 messageArea.innerHTML = ` 496 <div class="message ${type}"> 497 ${text} 498 </div> 499 `; 500 } 501 502 function clearMessage() { 503 const messageArea = document.getElementById('messageArea'); 504 messageArea.innerHTML = ''; 505 } 506 507 // Start application 508 init(); 509 </script> 510 </body> 511 </html>