From 7f51e16f6b6505d2dd128a7c9c77fa2f791e4fe0 Mon Sep 17 00:00:00 2001 From: Nordi98 Date: Mon, 4 Aug 2025 06:53:42 +0200 Subject: [PATCH] ed --- .../[tools]/nordi_license/html/script.js | 463 +++++++++++++++--- .../[tools]/nordi_license/html/style.css | 131 +++++ 2 files changed, 527 insertions(+), 67 deletions(-) diff --git a/resources/[tools]/nordi_license/html/script.js b/resources/[tools]/nordi_license/html/script.js index 66d984978..eca5c8326 100644 --- a/resources/[tools]/nordi_license/html/script.js +++ b/resources/[tools]/nordi_license/html/script.js @@ -1,4 +1,6 @@ let currentLicense = null; +let isCardFlipped = false; +let cameraStream = null; // Event Listener für Nachrichten von FiveM window.addEventListener('message', function(event) { @@ -8,6 +10,12 @@ window.addEventListener('message', function(event) { case 'showLicense': showLicense(data.data); break; + case 'hideLicense': + closeLicense(); + break; + case 'openCamera': + openCamera(); + break; } }); @@ -17,75 +25,301 @@ function showLicense(data) { const container = document.getElementById('license-container'); const card = document.getElementById('license-card'); - // Lizenztyp-spezifische Klasse hinzufügen - card.className = 'license-card ' + data.license.license_type; + // Loading anzeigen + showLoading(); - // Header befüllen - document.querySelector('.license-title').textContent = data.config.label; - document.querySelector('.license-icon').className = 'license-icon ' + data.config.icon; - - // Lizenzinformationen befüllen - document.getElementById('license-name').textContent = data.license.name || 'N/A'; - document.getElementById('license-birthday').textContent = data.license.birthday || 'N/A'; - document.getElementById('license-gender').textContent = formatGender(data.license.gender) || 'N/A'; - document.getElementById('license-issue').textContent = data.license.issue_date || 'N/A'; - document.getElementById('license-expire').textContent = data.license.expire_date || 'N/A'; - - // Klassen anzeigen (nur bei Führerschein) - const classesRow = document.getElementById('license-classes-row'); - if (data.license.classes && data.license.classes !== '[]') { - try { - const classes = JSON.parse(data.license.classes); - if (classes && classes.length > 0) { - document.getElementById('license-classes').textContent = classes.join(', '); - classesRow.style.display = 'flex'; - } else { - classesRow.style.display = 'none'; - } - } catch (e) { - classesRow.style.display = 'none'; - } - } else { - classesRow.style.display = 'none'; - } - - // Status anzeigen - const statusElement = document.getElementById('license-status'); - if (data.license.is_active) { - statusElement.textContent = '✅ Gültig'; - statusElement.className = 'license-status active'; - } else { - statusElement.textContent = '❌ Ungültig'; - statusElement.className = 'license-status inactive'; - } - - // Container anzeigen - container.classList.remove('hidden'); + setTimeout(() => { + // Lizenztyp-spezifische Klasse hinzufügen + card.className = 'license-card ' + data.license.license_type; + + // Header befüllen + document.getElementById('license-title').textContent = data.config.label; + document.getElementById('license-icon').className = 'license-icon ' + data.config.icon; + + // Persönliche Daten + document.getElementById('license-name').textContent = data.license.name || 'N/A'; + document.getElementById('license-birthday').textContent = formatDate(data.license.birthday) || 'N/A'; + document.getElementById('license-gender').textContent = formatGender(data.license.gender) || 'N/A'; + + // Dokument-Informationen + document.getElementById('license-issue').textContent = formatDate(data.license.issue_date) || 'N/A'; + document.getElementById('license-expire').textContent = formatDate(data.license.expire_date) || 'N/A'; + document.getElementById('license-id').textContent = '#' + (data.license.id || '000000').toString().padStart(6, '0'); + document.getElementById('license-issuer').textContent = data.license.issued_by_name || 'Behörde'; + + // Foto anzeigen + displayPlayerPhoto(data.license); + + // Klassen anzeigen (nur bei Führerschein) + displayLicenseClasses(data.license); + + // Status und Gültigkeit + displayLicenseStatus(data.license); + displayValidityIndicator(data.license); + + // Rückseite vorbereiten + prepareBackSide(data.license); + + // Container anzeigen + hideLoading(); + container.classList.remove('hidden'); + + // Sound-Effekt + playSound('card-flip-sound'); + + // Notification + showNotification('Lizenz geladen', 'success'); + + }, 500); } - // Foto anzeigen +// Spieler-Foto anzeigen +function displayPlayerPhoto(license) { const photoImg = document.getElementById('player-photo'); const photoPlaceholder = document.getElementById('photo-placeholder'); - if (data.license.photo_url && data.license.photo_url !== '') { - // Echtes Foto anzeigen - photoImg.src = data.license.photo_url; - photoImg.classList.remove('hidden'); - photoPlaceholder.classList.add('hidden'); + if (license.photo_url && license.photo_url !== '') { + photoImg.src = license.photo_url; + photoImg.onload = function() { + photoImg.classList.remove('hidden'); + photoPlaceholder.classList.add('hidden'); + }; + photoImg.onerror = function() { + photoImg.classList.add('hidden'); + photoPlaceholder.classList.remove('hidden'); + }; } else { - // Platzhalter anzeigen photoImg.classList.add('hidden'); photoPlaceholder.classList.remove('hidden'); } +} -// Geschlecht formatieren -function formatGender(gender) { - const genderMap = { - 'male': 'Männlich', - 'female': 'Weiblich', - 'other': 'Divers' - }; - return genderMap[gender] || gender; +// Lizenz-Klassen anzeigen +function displayLicenseClasses(license) { + const classesRow = document.getElementById('license-classes-row'); + const classesElement = document.getElementById('license-classes'); + + if (license.license_type === 'drivers_license' && license.classes && license.classes !== '[]') { + try { + const classes = JSON.parse(license.classes); + if (classes && classes.length > 0) { + classesElement.textContent = classes.join(', '); + classesRow.style.display = 'flex'; + return; + } + } catch (e) { + console.error('Fehler beim Parsen der Klassen:', e); + } + } + classesRow.style.display = 'none'; +} + +// Lizenz-Status anzeigen +function displayLicenseStatus(license) { + const statusElement = document.getElementById('license-status'); + const statusIcon = statusElement.querySelector('.status-icon'); + const statusText = statusElement.querySelector('.status-text'); + + // Ablaufdatum prüfen + let isExpired = false; + let isExpiringSoon = false; + + if (license.expire_date) { + const expireDate = new Date(license.expire_date); + const today = new Date(); + const daysUntilExpire = Math.ceil((expireDate - today) / (1000 * 60 * 60 * 24)); + + isExpired = daysUntilExpire < 0; + isExpiringSoon = daysUntilExpire <= 30 && daysUntilExpire >= 0; + } + + // Status setzen + if (!license.is_active || isExpired) { + statusElement.className = 'license-status inactive'; + statusIcon.className = 'status-icon fas fa-times-circle'; + statusText.textContent = isExpired ? 'Abgelaufen' : 'Ungültig'; + } else if (isExpiringSoon) { + statusElement.className = 'license-status warning'; + statusIcon.className = 'status-icon fas fa-exclamation-triangle'; + statusText.textContent = 'Läuft bald ab'; + } else { + statusElement.className = 'license-status active'; + statusIcon.className = 'status-icon fas fa-check-circle'; + statusText.textContent = 'Gültig'; + } +} + +// Gültigkeits-Indikator anzeigen +function displayValidityIndicator(license) { + const validityFill = document.getElementById('validity-fill'); + const validityText = document.getElementById('validity-text'); + + if (!license.expire_date) { + validityText.textContent = 'Unbegrenzt gültig'; + validityFill.style.width = '100%'; + validityFill.style.backgroundColor = '#4CAF50'; + return; + } + + const issueDate = new Date(license.issue_date); + const expireDate = new Date(license.expire_date); + const today = new Date(); + + const totalDays = Math.ceil((expireDate - issueDate) / (1000 * 60 * 60 * 24)); + const remainingDays = Math.ceil((expireDate - today) / (1000 * 60 * 60 * 24)); + const percentage = Math.max(0, Math.min(100, (remainingDays / totalDays) * 100)); + + validityFill.style.width = percentage + '%'; + + if (remainingDays < 0) { + validityText.textContent = 'Abgelaufen'; + validityFill.style.backgroundColor = '#f44336'; + } else if (remainingDays <= 30) { + validityText.textContent = `Noch ${remainingDays} Tage gültig`; + validityFill.style.backgroundColor = '#ff9800'; + } else { + validityText.textContent = `Noch ${remainingDays} Tage gültig`; + validityFill.style.backgroundColor = '#4CAF50'; + } +} + +// Rückseite vorbereiten +function prepareBackSide(license) { + const classesGrid = document.getElementById('classes-grid'); + const restrictionsList = document.getElementById('restrictions-list'); + const notesText = document.getElementById('notes-text'); + + // Klassen-Grid für Führerschein + if (license.license_type === 'drivers_license' && license.classes) { + try { + const classes = JSON.parse(license.classes); + classesGrid.innerHTML = ''; + + const classDescriptions = { + 'A': 'Motorräder', + 'A1': 'Leichte Motorräder', + 'A2': 'Mittlere Motorräder', + 'B': 'PKW', + 'BE': 'PKW mit Anhänger', + 'C': 'LKW', + 'CE': 'LKW mit Anhänger', + 'D': 'Bus', + 'DE': 'Bus mit Anhänger' + }; + + classes.forEach(cls => { + const classItem = document.createElement('div'); + classItem.className = 'class-item'; + classItem.innerHTML = ` +
${cls}
+
${classDescriptions[cls] || 'Unbekannt'}
+ `; + classesGrid.appendChild(classItem); + }); + } catch (e) { + classesGrid.innerHTML = '

Keine Klassen verfügbar

'; + } + } else { + classesGrid.innerHTML = '

Nicht zutreffend

'; + } + + // Beschränkungen (Beispiel) + restrictionsList.innerHTML = '
  • Keine besonderen Beschränkungen
  • '; + + // Bemerkungen + notesText.textContent = license.notes || 'Keine besonderen Bemerkungen'; +} + +// Karte drehen +function flipCard() { + const frontSide = document.querySelector('.license-content, .license-footer'); + const backSide = document.getElementById('license-back'); + + isCardFlipped = !isCardFlipped; + + if (isCardFlipped) { + // Zur Rückseite + document.querySelector('.license-content').classList.add('hidden'); + document.querySelector('.license-footer').classList.add('hidden'); + backSide.classList.remove('hidden'); + } else { + // Zur Vorderseite + document.querySelector('.license-content').classList.remove('hidden'); + document.querySelector('.license-footer').classList.remove('hidden'); + backSide.classList.add('hidden'); + } + + playSound('card-flip-sound'); +} + +// Kamera öffnen +async function openCamera() { + const container = document.getElementById('camera-container'); + const video = document.getElementById('camera-video'); + + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { + width: 640, + height: 480, + facingMode: 'user' + } + }); + + video.srcObject = cameraStream; + container.classList.remove('hidden'); + + showNotification('Kamera geöffnet', 'info'); + } catch (err) { + console.error('Kamera-Zugriff fehlgeschlagen:', err); + showNotification('Kamera-Zugriff fehlgeschlagen', 'error'); + } +} + +// Foto aufnehmen +function takePhoto() { + const video = document.getElementById('camera-video'); + const canvas = document.getElementById('camera-canvas'); + const ctx = canvas.getContext('2d'); + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + ctx.drawImage(video, 0, 0); + + const photoData = canvas.toDataURL('image/jpeg', 0.8); + + // Sound-Effekt + playSound('camera-shutter-sound'); + + // An FiveM senden + fetch(`https://${GetParentResourceName()}/savePhoto`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + photo: photoData, + citizenid: currentLicense?.license?.citizenid + }) + }).then(() => { + showNotification('Foto gespeichert', 'success'); + closeCamera(); + }).catch(err => { + console.error('Fehler beim Speichern:', err); + showNotification('Fehler beim Speichern', 'error'); + }); +} + +// Kamera schließen +function closeCamera() { + const container = document.getElementById('camera-container'); + + if (cameraStream) { + cameraStream.getTracks().forEach(track => track.stop()); + cameraStream = null; + } + + container.classList.add('hidden'); } // Lizenz schließen @@ -93,26 +327,121 @@ function closeLicense() { const container = document.getElementById('license-container'); container.classList.add('hidden'); - // Callback an FiveM senden + // Karte zurücksetzen + isCardFlipped = false; + document.querySelector('.license-content').classList.remove('hidden'); + document.querySelector('.license-footer').classList.remove('hidden'); + document.getElementById('license-back').classList.add('hidden'); + + // Callback an FiveM fetch(`https://${GetParentResourceName()}/closeLicense`, { method: 'POST', headers: { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json', }, body: JSON.stringify({}) - }); + }).catch(() => {}); // Fehler ignorieren } -// ESC-Taste zum Schließen +// Hilfsfunktionen +function formatGender(gender) { + const genderMap = { + 'male': 'Männlich', + 'female': 'Weiblich', + 'other': 'Divers', + 'm': 'Männlich', + 'f': 'Weiblich' + }; + return genderMap[gender?.toLowerCase()] || gender || 'Unbekannt'; +} + +function formatDate(dateString) { + if (!dateString) return null; + + try { + const date = new Date(dateString); + return date.toLocaleDateString('de-DE'); + } catch (e) { + return dateString; + } +} + +function showLoading() { + document.getElementById('loading-overlay').classList.remove('hidden'); +} + +function hideLoading() { + document.getElementById('loading-overlay').classList.add('hidden'); +} + +function playSound(soundId) { + const audio = document.getElementById(soundId); + if (audio) { + audio.volume = 0.3; + audio.play().catch(() => {}); // Fehler ignorieren + } +} + +function showNotification(message, type = 'info') { + const container = document.getElementById('notification-container'); + const notification = document.createElement('div'); + + notification.className = `notification ${type}`; + notification.innerHTML = ` +
    + + ${message} +
    + + `; + + container.appendChild(notification); + + // Auto-remove nach 5 Sekunden + setTimeout(() => { + if (notification.parentElement) { + notification.remove(); + } + }, 5000); +} + +function getNotificationIcon(type) { + const icons = { + 'success': 'fa-check-circle', + 'error': 'fa-exclamation-circle', + 'warning': 'fa-exclamation-triangle', + 'info': 'fa-info-circle' + }; + return icons[type] || icons.info; +} + +// Event Listeners document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { + if (!document.getElementById('camera-container').classList.contains('hidden')) { + closeCamera(); + } else if (!document.getElementById('license-container').classList.contains('hidden')) { + closeLicense(); + } + } + + if (event.key === 'f' || event.key === 'F') { + if (!document.getElementById('license-container').classList.contains('hidden')) { + flipCard(); + } + } +}); + +// Klick außerhalb zum Schließen +document.getElementById('license-container').addEventListener('click', function(event) { + if (event.target.classList.contains('license-overlay')) { closeLicense(); } }); -// Klick außerhalb der Karte zum Schließen -document.getElementById('license-container').addEventListener('click', function(event) { - if (event.target === this) { - closeLicense(); - } +// Initialisierung +document.addEventListener('DOMContentLoaded', function() { + console.log('License System UI geladen'); }); diff --git a/resources/[tools]/nordi_license/html/style.css b/resources/[tools]/nordi_license/html/style.css index 5948277ca..f631638fb 100644 --- a/resources/[tools]/nordi_license/html/style.css +++ b/resources/[tools]/nordi_license/html/style.css @@ -235,3 +235,134 @@ body { .license-card { animation: slideIn 0.3s ease-out; } + +/* Notification System */ +#notification-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + display: flex; + flex-direction: column; + gap: 10px; +} + +.notification { + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 15px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; + min-width: 300px; + animation: slideInRight 0.3s ease; +} + +.notification.success { border-left: 4px solid #4CAF50; } +.notification.error { border-left: 4px solid #f44336; } +.notification.warning { border-left: 4px solid #ff9800; } +.notification.info { border-left: 4px solid #2196F3; } + +.notification-content { + display: flex; + align-items: center; + gap: 10px; +} + +.notification-close { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 5px; +} + +/* Loading Overlay */ +#loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-spinner { + text-align: center; + color: white; +} + +.spinner { + width: 50px; + height: 50px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top: 3px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +/* Kamera Interface */ +#camera-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9998; +} + +.camera-interface { + background: white; + border-radius: 15px; + padding: 20px; + max-width: 600px; + width: 90%; +} + +.camera-preview { + position: relative; + margin: 20px 0; +} + +#camera-video { + width: 100%; + border-radius: 10px; +} + +.face-guide { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + color: white; +} + +.guide-circle { + width: 200px; + height: 200px; + border: 3px solid rgba(255, 255, 255, 0.8); + border-radius: 50%; + margin: 0 auto 10px; +} + +/* Animationen */ +@keyframes slideInRight { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +}