From 4d24104e50f00ba9337171821cee8023ae5c3fff Mon Sep 17 00:00:00 2001 From: Nordi98 Date: Mon, 4 Aug 2025 09:35:37 +0200 Subject: [PATCH] ed --- .../[tools]/nordi_license/client/main.lua | 1797 ++++++++++++++--- resources/[tools]/nordi_license/config.lua | 586 +++--- .../[tools]/nordi_license/html/index.html | 394 ++-- .../[tools]/nordi_license/html/script.js | 1137 ++++------- .../[tools]/nordi_license/html/style.css | 1072 ++++------ .../[tools]/nordi_license/server/main.lua | 486 +++-- 6 files changed, 3089 insertions(+), 2383 deletions(-) diff --git a/resources/[tools]/nordi_license/client/main.lua b/resources/[tools]/nordi_license/client/main.lua index c6f3eba7b..f8b3b634d 100644 --- a/resources/[tools]/nordi_license/client/main.lua +++ b/resources/[tools]/nordi_license/client/main.lua @@ -1,357 +1,1548 @@ local QBCore = exports['qb-core']:GetCoreObject() -local isMenuOpen = false -local currentLicenseData = nil --- Debug-Funktion +-- Lokale Variablen +local isMenuOpen = false +local currentTarget = nil +local nearbyPlayers = {} +local isLicenseShowing = false +local pendingRequests = {} + +-- Hilfsfunktionen local function debugPrint(message) if Config.Debug then - print("^2[License-System Client] " .. message .. "^7") + print("^3[License-System Client] " .. message .. "^7") end end --- Hilfsfunktionen -local function getClosestPlayer() - local players = GetActivePlayers() - local closestDistance = -1 - local closestPlayer = -1 - local ped = PlayerPedId() - local coords = GetEntityCoords(ped) +local function showNotification(message, type) + QBCore.Functions.Notify(message, type or 'primary') +end - for i = 1, #players do - local target = GetPlayerPed(players[i]) - if target ~= ped then - local targetCoords = GetEntityCoords(target) - local distance = #(coords - targetCoords) - if closestDistance == -1 or closestDistance > distance then - closestPlayer = GetPlayerServerId(players[i]) - closestDistance = distance +-- Nearby Players abrufen +local function getNearbyPlayers(radius) + radius = radius or 5.0 + local players = {} + local playerPed = PlayerPedId() + local playerCoords = GetEntityCoords(playerPed) + + for _, playerId in ipairs(GetActivePlayers()) do + local targetPed = GetPlayerPed(playerId) + if targetPed ~= playerPed then + local targetCoords = GetEntityCoords(targetPed) + local distance = #(playerCoords - targetCoords) + + if distance <= radius then + local serverId = GetPlayerServerId(playerId) + local playerName = GetPlayerName(playerId) + + table.insert(players, { + id = serverId, + name = playerName, + distance = math.floor(distance * 100) / 100, + ped = targetPed + }) end end end - - return closestPlayer, closestDistance + + -- Nach Entfernung sortieren + table.sort(players, function(a, b) + return a.distance < b.distance + end) + + debugPrint("Gefundene Spieler in der Nähe: " .. #players) + return players end --- URL-Validierung -local function isValidUrl(url) - if not url or url == "" then return true end -- Optional - return string.match(url, Config.Validation.url_pattern) ~= nil +-- Berechtigung prüfen +local function hasPermission() + local PlayerData = QBCore.Functions.GetPlayerData() + if not PlayerData or not PlayerData.job then return false end + + local hasAuth = Config.AuthorizedJobs[PlayerData.job.name] or false + debugPrint("Berechtigung für Job " .. PlayerData.job.name .. ": " .. tostring(hasAuth)) + return hasAuth end --- Datum-Validierung -local function isValidDate(date) - if not date or date == "" then return false end - if not string.match(date, Config.Validation.date_pattern) then return false end +-- Lizenz anzeigen +local function showLicense(licenseData) + if not licenseData then + showNotification(Config.Notifications.license_not_found.message, Config.Notifications.license_not_found.type) + return + end - local day, month, year = date:match("(%d+)%.(%d+)%.(%d+)") - day, month, year = tonumber(day), tonumber(month), tonumber(year) + debugPrint("Zeige Lizenz: " .. licenseData.license.license_type) - if not day or not month or not year then return false end - if day < 1 or day > 31 then return false end - if month < 1 or month > 12 then return false end - if year < 1900 or year > 2100 then return false end + SendNUIMessage({ + action = 'showLicense', + data = licenseData + }) + + SetNuiFocus(true, true) + isLicenseShowing = true +end + +-- Lizenz schließen +local function closeLicense() + SendNUIMessage({ + action = 'hideLicense' + }) + + SetNuiFocus(false, false) + isLicenseShowing = false + debugPrint("Lizenz geschlossen") +end + +-- Spieler-Lizenz anzeigen +local function showPlayerLicense(targetId) + debugPrint("=== showPlayerLicense START ===") + debugPrint("Sende Event: requestLicense für Spieler: " .. tostring(targetId)) + + TriggerServerEvent('license-system:server:requestLicense', targetId) +end + +-- Eigene Lizenz anzeigen +local function showMyLicense(licenseType) + debugPrint("=== showMyLicense START ===") + debugPrint("Sende Event: requestMyLicense für Typ: " .. tostring(licenseType)) + + TriggerServerEvent('license-system:server:requestMyLicense', licenseType) +end + +-- FORWARD DECLARATIONS (Funktionen die später definiert werden) +local confirmIssueLicense +local openIssueLicenseMenu +local openDriversLicenseClassMenu +local openRevokeLicenseMenu +local openPlayerLicenseMenu +local openLicenseMenu + +-- Lizenz-Ausstellung bestätigen (FRÜH DEFINIERT) +confirmIssueLicense = function(targetId, targetName, licenseType, classes) + local config = Config.LicenseTypes[licenseType] + if not config then + showNotification('Unbekannter Lizenztyp!', 'error') + return + end + + local classText = classes and table.concat(classes, ', ') or 'Keine' + local priceText = config.price and (config.price .. ' $') or 'Kostenlos' + + debugPrint("Bestätige Lizenz-Ausstellung: " .. licenseType .. " für " .. targetName) + + lib.registerContext({ + id = 'confirm_issue_license', + title = 'Lizenz ausstellen bestätigen', + options = { + { + title = 'Spieler: ' .. targetName, + disabled = true, + icon = 'fas fa-user' + }, + { + title = 'Lizenztyp: ' .. config.label, + disabled = true, + icon = config.icon + }, + { + title = 'Klassen: ' .. classText, + disabled = true, + icon = 'fas fa-list' + }, + { + title = 'Kosten: ' .. priceText, + disabled = true, + icon = 'fas fa-dollar-sign' + }, + { + title = '─────────────────', + disabled = true + }, + { + title = '✅ Bestätigen', + description = 'Lizenz jetzt ausstellen', + icon = 'fas fa-check', + onSelect = function() + debugPrint("Sende Lizenz-Ausstellung an Server...") + TriggerServerEvent('license-system:server:issueLicense', targetId, licenseType, classes) + lib.hideContext() + end + }, + { + title = '❌ Abbrechen', + description = 'Vorgang abbrechen', + icon = 'fas fa-times', + onSelect = function() + openIssueLicenseMenu(targetId, targetName) + end + } + } + }) + + lib.showContext('confirm_issue_license') +end + +-- Führerschein-Klassen Menü +openDriversLicenseClassMenu = function(targetId, targetName, licenseType) + local config = Config.LicenseTypes[licenseType] + if not config then + showNotification('Lizenz-Konfiguration nicht gefunden!', 'error') + return + end + + local selectedClasses = {} + + local function updateMenu() + local menuOptions = {} + + if config.classes then + for _, class in ipairs(config.classes) do + local isSelected = false + for _, selected in ipairs(selectedClasses) do + if selected == class then + isSelected = true + break + end + end + + local classDescriptions = { + ['A'] = 'Motorräder', + ['A1'] = 'Leichte Motorräder (bis 125ccm)', + ['A2'] = 'Mittlere Motorräder (bis 35kW)', + ['B'] = 'PKW (bis 3,5t)', + ['BE'] = 'PKW mit Anhänger', + ['C'] = 'LKW (über 3,5t)', + ['CE'] = 'LKW mit Anhänger', + ['D'] = 'Bus (über 8 Personen)', + ['DE'] = 'Bus mit Anhänger' + } + + table.insert(menuOptions, { + title = 'Klasse ' .. class .. (isSelected and ' ✅' or ''), + description = classDescriptions[class] or 'Keine Beschreibung', + icon = isSelected and 'fas fa-check-square' or 'far fa-square', + onSelect = function() + if isSelected then + -- Klasse entfernen + for i, selected in ipairs(selectedClasses) do + if selected == class then + table.remove(selectedClasses, i) + break + end + end + else + -- Klasse hinzufügen + table.insert(selectedClasses, class) + end + updateMenu() + end + }) + end + end + + table.insert(menuOptions, { + title = '─────────────────', + disabled = true + }) + + table.insert(menuOptions, { + title = 'Bestätigen (' .. #selectedClasses .. ' Klassen)', + description = 'Führerschein mit ausgewählten Klassen ausstellen', + icon = 'fas fa-check', + disabled = #selectedClasses == 0, + onSelect = function() + confirmIssueLicense(targetId, targetName, licenseType, selectedClasses) + end + }) + + table.insert(menuOptions, { + title = '← Zurück', + icon = 'fas fa-arrow-left', + onSelect = function() + openIssueLicenseMenu(targetId, targetName) + end + }) + + lib.registerContext({ + id = 'drivers_license_classes', + title = 'Führerschein-Klassen: ' .. targetName, + options = menuOptions + }) + + lib.showContext('drivers_license_classes') + end + + updateMenu() +end + +-- Lizenz ausstellen Menü +openIssueLicenseMenu = function(targetId, targetName) + debugPrint("Öffne Lizenz-Ausstellungs-Menü für: " .. targetName) + + local menuOptions = {} + + for licenseType, config in pairs(Config.LicenseTypes) do + local priceText = config.price and (config.price .. ' $') or 'Kostenlos' + local validityText = config.validity_days and (config.validity_days .. ' Tage') or 'Unbegrenzt' + + table.insert(menuOptions, { + title = config.label, + description = config.description or 'Keine Beschreibung verfügbar', + icon = config.icon, + onSelect = function() + if licenseType == 'drivers_license' and config.classes then + openDriversLicenseClassMenu(targetId, targetName, licenseType) + else + confirmIssueLicense(targetId, targetName, licenseType, nil) + end + end, + metadata = { + {label = 'Preis', value = priceText}, + {label = 'Gültigkeitsdauer', value = validityText} + } + }) + end + + table.insert(menuOptions, { + title = '← Zurück', + icon = 'fas fa-arrow-left', + onSelect = function() + openPlayerLicenseMenu(targetId, targetName) + end + }) + + lib.registerContext({ + id = 'issue_license', + title = 'Lizenz ausstellen: ' .. targetName, + options = menuOptions + }) + + lib.showContext('issue_license') +end + +-- Lizenz entziehen Menü +openRevokeLicenseMenu = function(targetId, targetName, licenses) + debugPrint("Öffne Lizenz-Entziehungs-Menü für: " .. targetName) + + local menuOptions = {} + + for _, license in ipairs(licenses) do + if license.is_active == 1 then + local config = Config.LicenseTypes[license.license_type] or { + label = license.license_type, + icon = 'fas fa-id-card' + } + + table.insert(menuOptions, { + title = config.label, + description = 'Diese Lizenz entziehen', + icon = config.icon, + onSelect = function() + lib.registerContext({ + id = 'confirm_revoke_license', + title = 'Lizenz entziehen bestätigen', + options = { + { + title = 'Spieler: ' .. targetName, + disabled = true, + icon = 'fas fa-user' + }, + { + title = 'Lizenztyp: ' .. config.label, + disabled = true, + icon = config.icon + }, + { + title = '─────────────────', + disabled = true + }, + { + title = '✅ Bestätigen', + description = 'Lizenz jetzt entziehen', + icon = 'fas fa-check', + onSelect = function() + debugPrint("Sende Lizenz-Entziehung an Server...") + TriggerServerEvent('license-system:server:revokeLicense', targetId, license.license_type) + lib.hideContext() + end + }, + { + title = '❌ Abbrechen', + description = 'Vorgang abbrechen', + icon = 'fas fa-times', + onSelect = function() + openRevokeLicenseMenu(targetId, targetName, licenses) + end + } + } + }) + + lib.showContext('confirm_revoke_license') + end + }) + end + end + + if #menuOptions == 0 then + table.insert(menuOptions, { + title = 'Keine aktiven Lizenzen', + description = 'Dieser Spieler hat keine aktiven Lizenzen', + icon = 'fas fa-exclamation-triangle', + disabled = true + }) + end + + table.insert(menuOptions, { + title = '← Zurück', + icon = 'fas fa-arrow-left', + onSelect = function() + openPlayerLicenseMenu(targetId, targetName) + end + }) + + lib.registerContext({ + id = 'revoke_license', + title = 'Lizenz entziehen: ' .. targetName, + options = menuOptions + }) + + lib.showContext('revoke_license') +end + +-- Spieler-Lizenz-Menü +openPlayerLicenseMenu = function(targetId, targetName) + debugPrint("=== openPlayerLicenseMenu START ===") + debugPrint("Sende Event: requestPlayerLicenses für: " .. targetName .. " (ID: " .. targetId .. ")") + + TriggerServerEvent('license-system:server:requestPlayerLicenses', targetId) +end + +-- Hauptmenü für Lizenz-System +openLicenseMenu = function() + debugPrint("Öffne Hauptmenü für Lizenz-System") + + if not hasPermission() then + showNotification(Config.Notifications.no_permission.message, Config.Notifications.no_permission.type) + return + end + + nearbyPlayers = getNearbyPlayers(5.0) + + if #nearbyPlayers == 0 then + showNotification(Config.Notifications.no_players_nearby.message, Config.Notifications.no_players_nearby.type) + return + end + + local menuOptions = {} + + for _, player in ipairs(nearbyPlayers) do + table.insert(menuOptions, { + title = player.name, + description = 'Entfernung: ' .. player.distance .. 'm', + icon = 'fas fa-user', + onSelect = function() + openPlayerLicenseMenu(player.id, player.name) + end + }) + end + + lib.registerContext({ + id = 'license_nearby_players', + title = 'Spieler in der Nähe (' .. #nearbyPlayers .. ')', + options = menuOptions + }) + + lib.showContext('license_nearby_players') +end + +-- Eigene Lizenzen anzeigen +local function showMyLicenses() + debugPrint("Öffne Menü für eigene Lizenzen") + + local menuOptions = {} + + for licenseType, config in pairs(Config.LicenseTypes) do + table.insert(menuOptions, { + title = config.label, + description = 'Deine ' .. config.label .. ' anzeigen', + icon = config.icon, + onSelect = function() + showMyLicense(licenseType) + end + }) + end + + lib.registerContext({ + id = 'my_licenses', + title = 'Meine Lizenzen', + options = menuOptions + }) + + lib.showContext('my_licenses') +end + +-- EVENT HANDLER: Einzelne Lizenz erhalten +RegisterNetEvent('license-system:client:receiveLicense', function(licenseData) + debugPrint("=== Event: receiveLicense ===") + debugPrint("LicenseData-Typ: " .. type(licenseData)) + + if licenseData then + debugPrint("Lizenz-Daten erhalten: " .. licenseData.license.license_type) + showLicense(licenseData) + else + debugPrint("Keine Lizenz-Daten erhalten") + showNotification(Config.Notifications.license_not_found.message, Config.Notifications.license_not_found.type) + end +end) + +-- EVENT HANDLER: Eigene Lizenz erhalten +RegisterNetEvent('license-system:client:receiveMyLicense', function(licenseData, licenseType) + debugPrint("=== Event: receiveMyLicense ===") + debugPrint("LicenseType: " .. tostring(licenseType)) + debugPrint("LicenseData-Typ: " .. type(licenseData)) + + if licenseData then + debugPrint("Eigene Lizenz-Daten erhalten: " .. licenseData.license.license_type) + showLicense(licenseData) + else + debugPrint("Keine eigene Lizenz gefunden") + local config = Config.LicenseTypes[licenseType] + local licenseName = config and config.label or licenseType + showNotification('Du hast keine ' .. licenseName .. '!', 'error') + end +end) + +-- EVENT HANDLER: Alle Spieler-Lizenzen erhalten +RegisterNetEvent('license-system:client:receivePlayerLicenses', function(licenses, targetId, targetName) + debugPrint("=== Event: receivePlayerLicenses ===") + debugPrint("Erhaltene Lizenzen: " .. #licenses) + debugPrint("TargetName: " .. tostring(targetName)) + + local menuOptions = {} + + if licenses and #licenses > 0 then + for _, license in ipairs(licenses) do + local licenseConfig = Config.LicenseTypes[license.license_type] or { + label = license.license_type, + icon = 'fas fa-id-card', + color = '#667eea' + } + + local statusIcon = (license.is_active == 1) and '✅' or '❌' + local statusText = (license.is_active == 1) and 'Gültig' or 'Ungültig' + local expireText = license.expire_date or 'Unbegrenzt' + + table.insert(menuOptions, { + title = licenseConfig.label .. ' ' .. statusIcon, + description = 'Status: ' .. statusText .. ' | Gültig bis: ' .. expireText, + icon = licenseConfig.icon, + onSelect = function() + local licenseData = { + license = license, + config = licenseConfig + } + showLicense(licenseData) + end, + metadata = { + {label = 'Status', value = statusText}, + {label = 'Ausgestellt', value = license.issue_date or 'Unbekannt'}, + {label = 'Gültig bis', value = expireText}, + {label = 'Aussteller', value = license.issued_by_name or 'System'} + } + }) + end + else + table.insert(menuOptions, { + title = 'Keine Lizenzen gefunden', + description = 'Dieser Spieler hat keine Lizenzen', + icon = 'fas fa-exclamation-triangle', + disabled = true + }) + end + + -- Aktionen hinzufügen + table.insert(menuOptions, { + title = '─────────────────', + disabled = true + }) + + table.insert(menuOptions, { + title = 'Neue Lizenz ausstellen', + description = 'Eine neue Lizenz für diesen Spieler ausstellen', + icon = 'fas fa-plus', + onSelect = function() + openIssueLicenseMenu(targetId, targetName) + end + }) + + if licenses and #licenses > 0 then + table.insert(menuOptions, { + title = 'Lizenz entziehen', + description = 'Eine bestehende Lizenz entziehen', + icon = 'fas fa-minus', + onSelect = function() + openRevokeLicenseMenu(targetId, targetName, licenses) + end + }) + end + + table.insert(menuOptions, { + title = '← Zurück', + icon = 'fas fa-arrow-left', + onSelect = function() + openLicenseMenu() + end + }) + + lib.registerContext({ + id = 'player_licenses', + title = 'Lizenzen: ' .. targetName, + options = menuOptions + }) + + lib.showContext('player_licenses') +end) + +-- EVENT HANDLER: Berechtigung erhalten +RegisterNetEvent('license-system:client:receivePermission', function(hasAuth, licenseType) + debugPrint("=== Event: receivePermission ===") + debugPrint("Berechtigung für " .. licenseType .. ": " .. tostring(hasAuth)) + + if not hasAuth then + showNotification(Config.Notifications.no_permission.message, Config.Notifications.no_permission.type) + end +end) + +-- EVENT HANDLER: Lizenz erfolgreich ausgestellt +RegisterNetEvent('license-system:client:licenseIssued', function(targetId, licenseType) + debugPrint("=== Event: licenseIssued ===") + debugPrint("Lizenz " .. licenseType .. " für Spieler " .. targetId .. " ausgestellt") + + -- Menü aktualisieren + if lib.getOpenContextMenu() then + lib.hideContext() + Wait(100) + openLicenseMenu() + end +end) + +-- EVENT HANDLER: Lizenz erfolgreich entzogen +RegisterNetEvent('license-system:client:licenseRevoked', function(targetId, licenseType) + debugPrint("=== Event: licenseRevoked ===") + debugPrint("Lizenz " .. licenseType .. " für Spieler " .. targetId .. " entzogen") + + -- Menü aktualisieren + if lib.getOpenContextMenu() then + lib.hideContext() + Wait(100) + openLicenseMenu() + end +end) + +-- EVENT HANDLER: Lizenz anzeigen (von anderen Spielern) +RegisterNetEvent('license-system:client:showLicense', function(targetId) + debugPrint("Event erhalten: showLicense für ID " .. tostring(targetId)) + showPlayerLicense(targetId) +end) + +-- EVENT HANDLER: Eigene Lizenz anzeigen +RegisterNetEvent('license-system:client:showMyLicense', function(licenseType) + debugPrint("Event erhalten: showMyLicense für Typ " .. tostring(licenseType)) + showMyLicense(licenseType) +end) + +-- EVENT HANDLER: Kamera öffnen +RegisterNetEvent('license-system:client:openCamera', function() + debugPrint("Event erhalten: openCamera") + SendNUIMessage({ + action = 'openCamera' + }) +end) + +-- EVENT HANDLER: Menü aktualisieren +RegisterNetEvent('license-system:client:refreshMenu', function() + debugPrint("Event erhalten: refreshMenu") + if lib.getOpenContextMenu() then + lib.hideContext() + Wait(100) + openLicenseMenu() + end +end) + +-- NUI CALLBACKS +RegisterNUICallback('closeLicense', function(data, cb) + debugPrint("NUI Callback: closeLicense") + closeLicense() + + if cb and type(cb) == "function" then + cb('ok') + end +end) + +RegisterNUICallback('savePhoto', function(data, cb) + debugPrint("NUI Callback: savePhoto") + + if data.photo and data.citizenid then + TriggerServerEvent('license-system:server:savePhoto', data.citizenid, data.photo) + + if cb and type(cb) == "function" then + cb('ok') + end + else + debugPrint("^1Fehler: Foto-Daten unvollständig^7") + + if cb and type(cb) == "function" then + cb('error') + end + end +end) + +RegisterNUICallback('takePicture', function(data, cb) + debugPrint("NUI Callback: takePicture") + + -- Hier könnte eine Kamera-Funktion implementiert werden + if cb and type(cb) == "function" then + cb('ok') + end +end) + +-- COMMANDS +RegisterCommand(Config.Commands.license.name, function() + debugPrint("Command ausgeführt: " .. Config.Commands.license.name) + openLicenseMenu() +end, Config.Commands.license.restricted) + +RegisterCommand(Config.Commands.mylicense.name, function() + debugPrint("Command ausgeführt: " .. Config.Commands.mylicense.name) + showMyLicenses() +end, Config.Commands.mylicense.restricted) + +-- Zusätzliche Commands für schnellen Zugriff +RegisterCommand('ausweis', function() + debugPrint("Command ausgeführt: ausweis") + showMyLicense('id_card') +end, false) + +RegisterCommand('führerschein', function() + debugPrint("Command ausgeführt: führerschein") + showMyLicense('drivers_license') +end, false) + +RegisterCommand('waffenschein', function() + debugPrint("Command ausgeführt: waffenschein") + showMyLicense('weapon_license') +end, false) + +RegisterCommand('pass', function() + debugPrint("Command ausgeführt: pass") + showMyLicense('passport') +end, false) + +-- KEYBINDS +if Config.Keybinds and Config.Keybinds.open_license_menu then + RegisterKeyMapping(Config.Commands.license.name, Config.Keybinds.open_license_menu.description, 'keyboard', Config.Keybinds.open_license_menu.key) +end + +if Config.Keybinds and Config.Keybinds.show_my_licenses then + RegisterKeyMapping(Config.Commands.mylicense.name, Config.Keybinds.show_my_licenses.description, 'keyboard', Config.Keybinds.show_my_licenses.key) +end + +-- ESC-Taste zum Schließen der Lizenz +CreateThread(function() + while true do + Wait(0) + + if isLicenseShowing then + if IsControlJustPressed(0, 322) then -- ESC-Taste + closeLicense() + end + else + Wait(500) + end + end +end) + +-- CLEANUP UND INITIALISIERUNG +AddEventHandler('onResourceStop', function(resourceName) + if GetCurrentResourceName() == resourceName then + if isLicenseShowing then + closeLicense() + end + + if lib.getOpenContextMenu() then + lib.hideContext() + end + + debugPrint("License-System Client gestoppt") + end +end) + +AddEventHandler('onResourceStart', function(resourceName) + if GetCurrentResourceName() == resourceName then + debugPrint("License-System Client gestartet (Event-basiert)") + + -- Warten bis QBCore geladen ist + while not QBCore do + Wait(100) + end + + debugPrint("QBCore erfolgreich geladen") + end +end) + +-- Player laden Event +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + debugPrint("Spieler geladen - License-System bereit") +end) + +-- Job Update Event +RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) + debugPrint("Job aktualisiert: " .. JobInfo.name) +end) + +-- Initialisierung +CreateThread(function() + debugPrint("License-System Client Thread gestartet") + + -- Warten bis Spieler gespawnt ist + while not NetworkIsPlayerActive(PlayerId()) do + Wait(100) + end + + debugPrint("Spieler ist aktiv - System bereit") +end) + +-- Zusätzliche Utility-Funktionen +local function requestLicenseWithTimeout(eventName, targetId, timeout) + timeout = timeout or 5000 + local requestId = math.random(1000, 9999) + + pendingRequests[requestId] = { + timestamp = GetGameTimer(), + timeout = timeout + } + + debugPrint("Sende Request mit Timeout: " .. eventName .. " (ID: " .. requestId .. ")") + TriggerServerEvent(eventName, targetId, requestId) + + -- Timeout-Handler + CreateThread(function() + Wait(timeout) + if pendingRequests[requestId] then + pendingRequests[requestId] = nil + debugPrint("^1Request Timeout: " .. eventName .. " (ID: " .. requestId .. ")^7") + showNotification('Anfrage-Timeout! Versuche es erneut.', 'error') + end + end) + + return requestId +end + +-- Erweiterte Error-Handling +local function safeExecute(func, errorMessage) + local success, error = pcall(func) + if not success then + debugPrint("^1Fehler: " .. (errorMessage or "Unbekannter Fehler") .. "^7") + debugPrint("^1Details: " .. tostring(error) .. "^7") + showNotification('Ein Fehler ist aufgetreten!', 'error') + end + return success +end + +-- Performance-Monitoring +local performanceStats = { + menuOpens = 0, + licenseShows = 0, + errors = 0 +} + +CreateThread(function() + while true do + Wait(60000) -- Jede Minute + + if Config.Debug then + debugPrint("=== Performance Stats ===") + debugPrint("Menü-Öffnungen: " .. performanceStats.menuOpens) + debugPrint("Lizenz-Anzeigen: " .. performanceStats.licenseShows) + debugPrint("Fehler: " .. performanceStats.errors) + end + end +end) + +-- Stats aktualisieren +local originalOpenLicenseMenu = openLicenseMenu +openLicenseMenu = function() + performanceStats.menuOpens = performanceStats.menuOpens + 1 + return originalOpenLicenseMenu() +end + +local originalShowLicense = showLicense +showLicense = function(licenseData) + performanceStats.licenseShows = performanceStats.licenseShows + 1 + return originalShowLicense(licenseData) +end + +-- Erweiterte Fehlerbehandlung für Events +local function safeEventHandler(eventName, handler) + RegisterNetEvent(eventName, function(...) + local success, error = pcall(handler, ...) + if not success then + debugPrint("^1Fehler in Event " .. eventName .. ": " .. tostring(error) .. "^7") + performanceStats.errors = performanceStats.errors + 1 + showNotification('Ein Fehler ist aufgetreten!', 'error') + end + end) +end + +-- Zusätzliche Validierungen +local function validateLicenseData(licenseData) + if not licenseData then + debugPrint("^1Validierung fehlgeschlagen: licenseData ist nil^7") + return false + end + + if not licenseData.license then + debugPrint("^1Validierung fehlgeschlagen: license-Objekt fehlt^7") + return false + end + + if not licenseData.license.license_type then + debugPrint("^1Validierung fehlgeschlagen: license_type fehlt^7") + return false + end return true end --- Bild-URL validieren -local function validateImageUrl(url, callback) - if not url or url == "" then - callback(true, Config.UI.default_avatar) +-- Erweiterte showLicense Funktion mit Validierung +local function showLicenseWithValidation(licenseData) + if not validateLicenseData(licenseData) then + showNotification('Ungültige Lizenz-Daten!', 'error') return end - if not isValidUrl(url) then - callback(false, "Ungültige URL") - return + debugPrint("Zeige Lizenz: " .. licenseData.license.license_type) + + -- Zusätzliche Daten für NUI vorbereiten + local nuitData = { + license = licenseData.license, + config = licenseData.config or Config.LicenseTypes[licenseData.license.license_type], + timestamp = GetGameTimer(), + playerData = QBCore.Functions.GetPlayerData() + } + + SendNUIMessage({ + action = 'showLicense', + data = nuitData + }) + + SetNuiFocus(true, true) + isLicenseShowing = true + performanceStats.licenseShows = performanceStats.licenseShows + 1 +end + +-- Überschreibe die ursprüngliche showLicense Funktion +showLicense = showLicenseWithValidation + +-- Erweiterte Menü-Funktionen mit Error-Handling +local function safeMenuAction(action, errorMessage) + return function(...) + local success, error = pcall(action, ...) + if not success then + debugPrint("^1Menü-Fehler: " .. (errorMessage or "Unbekannt") .. "^7") + debugPrint("^1Details: " .. tostring(error) .. "^7") + performanceStats.errors = performanceStats.errors + 1 + showNotification('Menü-Fehler aufgetreten!', 'error') + + -- Menü schließen bei Fehler + if lib.getOpenContextMenu() then + lib.hideContext() + end + end + end +end + +-- Cache für Spieler-Daten +local playerCache = {} +local cacheTimeout = 30000 -- 30 Sekunden + +local function getCachedPlayerData(playerId) + local now = GetGameTimer() + local cached = playerCache[playerId] + + if cached and (now - cached.timestamp) < cacheTimeout then + debugPrint("Verwende gecachte Spieler-Daten für ID: " .. playerId) + return cached.data end - -- Dateiformat prüfen - local extension = string.lower(url:match("%.([^%.]+)$") or "") - local isValidFormat = false + return nil +end + +local function setCachedPlayerData(playerId, data) + playerCache[playerId] = { + data = data, + timestamp = GetGameTimer() + } + debugPrint("Spieler-Daten gecacht für ID: " .. playerId) +end + +-- Cache-Cleanup +CreateThread(function() + while true do + Wait(60000) -- Jede Minute + + local now = GetGameTimer() + local cleaned = 0 + + for playerId, cached in pairs(playerCache) do + if (now - cached.timestamp) > cacheTimeout then + playerCache[playerId] = nil + cleaned = cleaned + 1 + end + end + + if cleaned > 0 then + debugPrint("Cache bereinigt: " .. cleaned .. " Einträge entfernt") + end + end +end) + +-- Erweiterte Nearby-Players Funktion mit Cache +local function getNearbyPlayersWithCache(radius) + radius = radius or 5.0 + local players = {} + local playerPed = PlayerPedId() + local playerCoords = GetEntityCoords(playerPed) - for _, format in ipairs(Config.UI.allowed_image_formats) do - if extension == format then - isValidFormat = true - break + for _, playerId in ipairs(GetActivePlayers()) do + local targetPed = GetPlayerPed(playerId) + if targetPed ~= playerPed then + local targetCoords = GetEntityCoords(targetPed) + local distance = #(playerCoords - targetCoords) + + if distance <= radius then + local serverId = GetPlayerServerId(playerId) + local playerName = GetPlayerName(playerId) + + -- Cache-Daten verwenden wenn verfügbar + local cachedData = getCachedPlayerData(serverId) + local playerData = cachedData or { + id = serverId, + name = playerName, + ped = targetPed + } + + playerData.distance = math.floor(distance * 100) / 100 + + -- Daten cachen wenn nicht bereits gecacht + if not cachedData then + setCachedPlayerData(serverId, playerData) + end + + table.insert(players, playerData) + end end end - if not isValidFormat then - callback(false, "Ungültiges Bildformat. Erlaubt: " .. table.concat(Config.UI.allowed_image_formats, ", ")) - return - end - - callback(true, url) -end - --- Erweiterte Lizenz-Erstellung mit benutzerdefinierten Feldern -local function openCustomLicenseCreation(targetId, licenseType) - debugPrint("Öffne erweiterte Lizenz-Erstellung für: " .. licenseType) - - local config = Config.LicenseTypes[licenseType] - if not config then - QBCore.Functions.Notify("Unbekannter Lizenztyp!", "error") - return - end - - -- NUI-Daten vorbereiten - local formData = { - licenseType = licenseType, - config = config, - targetId = targetId, - validation = Config.Validation, - ui = Config.UI - } - - debugPrint("Sende Daten an NUI: " .. json.encode(formData)) - - -- NUI öffnen - SetNuiFocus(true, true) - SendNUIMessage({ - action = "openCustomLicenseForm", - data = formData - }) - - isMenuOpen = true -end - --- Standard-Lizenz-Erstellung (ohne benutzerdefinierte Felder) -local function openStandardLicenseCreation(targetId, licenseType) - debugPrint("Öffne Standard-Lizenz-Erstellung für: " .. licenseType) - - local config = Config.LicenseTypes[licenseType] - if not config then - QBCore.Functions.Notify("Unbekannter Lizenztyp!", "error") - return - end - - local classes = {} - if config.classes then - for classKey, classLabel in pairs(config.classes) do - table.insert(classes, {key = classKey, label = classLabel}) - end - end - - -- Standard-Erstellung (für Lizenzen ohne custom_fields) - TriggerServerEvent('license-system:server:issueLicense', targetId, licenseType, classes) -end - --- Hauptmenü öffnen -local function openMainMenu() - debugPrint("Öffne Hauptmenü") - - local targetId, distance = getClosestPlayer() - - local menuData = { - licenseTypes = Config.LicenseTypes, - targetId = targetId, - targetDistance = distance, - hasTarget = targetId ~= -1 and distance <= 3.0 - } - - SetNuiFocus(true, true) - SendNUIMessage({ - action = "openMainMenu", - data = menuData - }) - - isMenuOpen = true -end - --- NUI-Callbacks -RegisterNUICallback('closeMenu', function(data, cb) - debugPrint("Schließe Menü") - SetNuiFocus(false, false) - isMenuOpen = false - cb('ok') -end) - -RegisterNUICallback('requestLicense', function(data, cb) - debugPrint("Lizenz angefordert für Spieler: " .. data.targetId) - TriggerServerEvent('license-system:server:requestLicense', data.targetId) - cb('ok') -end) - -RegisterNUICallback('requestMyLicense', function(data, cb) - debugPrint("Eigene Lizenz angefordert: " .. (data.licenseType or "alle")) - TriggerServerEvent('license-system:server:requestMyLicense', data.licenseType) - cb('ok') -end) - -RegisterNUICallback('requestPlayerLicenses', function(data, cb) - debugPrint("Alle Lizenzen angefordert für Spieler: " .. data.targetId) - TriggerServerEvent('license-system:server:requestPlayerLicenses', data.targetId) - cb('ok') -end) - -RegisterNUICallback('openLicenseCreation', function(data, cb) - debugPrint("Lizenz-Erstellung angefordert: " .. data.licenseType) - - local config = Config.LicenseTypes[data.licenseType] - if config and config.custom_fields and #config.custom_fields > 0 then - openCustomLicenseCreation(data.targetId, data.licenseType) - else - openStandardLicenseCreation(data.targetId, data.licenseType) - end - - cb('ok') -end) - -RegisterNUICallback('validateImageUrl', function(data, cb) - debugPrint("Validiere Bild-URL: " .. (data.url or "leer")) - - validateImageUrl(data.url, function(isValid, result) - cb({ - valid = isValid, - url = isValid and result or Config.UI.default_avatar, - error = not isValid and result or nil - }) + -- Nach Entfernung sortieren + table.sort(players, function(a, b) + return a.distance < b.distance end) -end) + + debugPrint("Gefundene Spieler in der Nähe: " .. #players) + return players +end -RegisterNUICallback('submitCustomLicense', function(data, cb) - debugPrint("Benutzerdefinierte Lizenz eingereicht") - debugPrint("Daten: " .. json.encode(data)) +-- Überschreibe die ursprüngliche getNearbyPlayers Funktion +getNearbyPlayers = getNearbyPlayersWithCache + +-- Erweiterte Notification-Funktion +local function showNotificationWithSound(message, type, sound) + QBCore.Functions.Notify(message, type or 'primary') - local licenseType = data.licenseType - local targetId = data.targetId - local customData = data.customData - local classes = data.classes or {} + if sound and Config.Sounds and Config.Sounds[sound] then + PlaySoundFrontend(-1, Config.Sounds[sound].name, Config.Sounds[sound].set, true) + end +end + +-- Erweiterte Event-Handler mit besserer Fehlerbehandlung +RegisterNetEvent('license-system:client:receiveLicenseWithValidation', function(licenseData) + debugPrint("=== Event: receiveLicenseWithValidation ===") - -- Validierung - local config = Config.LicenseTypes[licenseType] - if not config then - cb({success = false, error = "Unbekannter Lizenztyp"}) + if not validateLicenseData(licenseData) then + showNotificationWithSound('Ungültige Lizenz-Daten erhalten!', 'error', 'error') return end - -- Pflichtfelder prüfen - for _, field in ipairs(config.custom_fields or {}) do - if field.required then - local value = customData[field.name] - if not value or value == "" then - cb({success = false, error = "Feld '" .. field.label .. "' ist erforderlich"}) - return - end - - -- Spezielle Validierung - if field.type == "date" and not isValidDate(value) then - cb({success = false, error = "Ungültiges Datum in Feld '" .. field.label .. "'"}) - return - end - - if field.type == "url" and not isValidUrl(value) then - cb({success = false, error = "Ungültige URL in Feld '" .. field.label .. "'"}) - return - end + debugPrint("Gültige Lizenz-Daten erhalten: " .. licenseData.license.license_type) + showLicense(licenseData) +end) + +-- Erweiterte Lizenz-Anzeige mit Animationen +local function showLicenseWithAnimation(licenseData) + if not validateLicenseData(licenseData) then + showNotification('Ungültige Lizenz-Daten!', 'error') + return + end + + -- Animation starten + local playerPed = PlayerPedId() + + -- Lizenz-Animation (falls gewünscht) + if Config.Animations and Config.Animations.show_license then + local anim = Config.Animations.show_license + RequestAnimDict(anim.dict) + + while not HasAnimDictLoaded(anim.dict) do + Wait(10) end - end - - -- An Server senden - TriggerServerEvent('license-system:server:issueCustomLicense', targetId, licenseType, customData, classes) - - cb({success = true}) -end) - -RegisterNUICallback('revokeLicense', function(data, cb) - debugPrint("Lizenz-Entzug angefordert: " .. data.licenseType .. " für Spieler: " .. data.targetId) - TriggerServerEvent('license-system:server:revokeLicense', data.targetId, data.licenseType) - cb('ok') -end) - --- Server-Events -RegisterNetEvent('license-system:client:receiveLicense', function(licenseData) - debugPrint("Lizenz erhalten") - - if licenseData then - debugPrint("Zeige Lizenz: " .. licenseData.license.license_type) - SetNuiFocus(true, true) - SendNUIMessage({ - action = "showLicense", - data = licenseData - }) - else - debugPrint("Keine Lizenz gefunden") - QBCore.Functions.Notify("Keine Lizenz gefunden!", "error") + TaskPlayAnim(playerPed, anim.dict, anim.name, 8.0, -8.0, -1, anim.flag or 49, 0, false, false, false) end -end) - -RegisterNetEvent('license-system:client:receiveMyLicense', function(licenseData, licenseType) - debugPrint("Eigene Lizenz erhalten: " .. (licenseType or "unbekannt")) - if licenseData then - debugPrint("Zeige eigene Lizenz: " .. licenseData.license.license_type) - - SetNuiFocus(true, true) - SendNUIMessage({ - action = "showMyLicense", - data = licenseData - }) - else - debugPrint("Keine eigene Lizenz gefunden") - QBCore.Functions.Notify("Du hast keine " .. (Config.LicenseTypes[licenseType] and Config.LicenseTypes[licenseType].label or "Lizenz") .. "!", "error") - end -end) - -RegisterNetEvent('license-system:client:receivePlayerLicenses', function(licenses, targetId, targetName) - debugPrint("Spieler-Lizenzen erhalten: " .. #licenses .. " für " .. targetName) + -- Lizenz anzeigen + showLicense(licenseData) - SetNuiFocus(true, true) - SendNUIMessage({ - action = "showPlayerLicenses", - data = { - licenses = licenses, - targetId = targetId, - targetName = targetName, - licenseTypes = Config.LicenseTypes - } - }) -end) - -RegisterNetEvent('license-system:client:licenseIssued', function(targetId, licenseType) - debugPrint("Lizenz ausgestellt: " .. licenseType) - QBCore.Functions.Notify("Lizenz erfolgreich ausgestellt!", "success") -end) - -RegisterNetEvent('license-system:client:licenseRevoked', function(targetId, licenseType) - debugPrint("Lizenz entzogen: " .. licenseType) - QBCore.Functions.Notify("Lizenz erfolgreich entzogen!", "success") -end) - -RegisterNetEvent('license-system:client:refreshMenu', function() - debugPrint("Menü-Refresh angefordert") - if isMenuOpen then - -- Menü neu laden falls geöffnet - Wait(500) - openMainMenu() + -- Animation nach kurzer Zeit stoppen + if Config.Animations and Config.Animations.show_license then + CreateThread(function() + Wait(2000) + ClearPedTasks(playerPed) + end) end -end) +end --- Commands -RegisterCommand('lizenz', function() - if not isMenuOpen then - openMainMenu() +-- Erweiterte Menü-Navigation +local menuHistory = {} + +local function pushMenuHistory(menuId) + table.insert(menuHistory, menuId) + debugPrint("Menü-Historie: " .. menuId .. " hinzugefügt") +end + +local function popMenuHistory() + if #menuHistory > 0 then + local lastMenu = table.remove(menuHistory) + debugPrint("Menü-Historie: " .. lastMenu .. " entfernt") + return lastMenu end -end, false) + return nil +end -RegisterCommand('ausweis', function() - TriggerServerEvent('license-system:server:requestMyLicense', 'id_card') -end, false) +local function clearMenuHistory() + menuHistory = {} + debugPrint("Menü-Historie geleert") +end --- Keybinds -RegisterKeyMapping('lizenz', 'Lizenz-System öffnen', 'keyboard', 'F6') -RegisterKeyMapping('ausweis', 'Eigenen Ausweis zeigen', 'keyboard', 'F7') +-- Erweiterte Cleanup-Funktion +local function cleanup() + debugPrint("Führe erweiterte Cleanup-Routine aus...") + + -- NUI schließen + if isLicenseShowing then + closeLicense() + end + + -- Menüs schließen + if lib.getOpenContextMenu() then + lib.hideContext() + end + + -- Cache leeren + playerCache = {} + + -- Historie leeren + clearMenuHistory() + + -- Pending Requests leeren + pendingRequests = {} + + -- Animationen stoppen + local playerPed = PlayerPedId() + if DoesEntityExist(playerPed) then + ClearPedTasks(playerPed) + end + + debugPrint("Cleanup abgeschlossen") +end --- Cleanup +-- Erweiterte Resource-Stop Handler AddEventHandler('onResourceStop', function(resourceName) if GetCurrentResourceName() == resourceName then - if isMenuOpen then - SetNuiFocus(false, false) - isMenuOpen = false + cleanup() + debugPrint("License-System Client gestoppt (erweitert)") + end +end) + +-- Erweiterte Resource-Start Handler +AddEventHandler('onResourceStart', function(resourceName) + if GetCurrentResourceName() == resourceName then + debugPrint("License-System Client gestartet (erweitert)") + + -- Initialisierung + CreateThread(function() + -- Warten bis QBCore geladen ist + while not QBCore do + Wait(100) + end + + debugPrint("QBCore erfolgreich geladen (erweitert)") + + -- Zusätzliche Initialisierung + Wait(1000) + + -- Performance-Stats zurücksetzen + performanceStats = { + menuOpens = 0, + licenseShows = 0, + errors = 0 + } + + debugPrint("Erweiterte Initialisierung abgeschlossen") + end) + end +end) + +-- Erweiterte Debug-Funktionen +local function debugPlayerInfo() + if not Config.Debug then return end + + local PlayerData = QBCore.Functions.GetPlayerData() + debugPrint("=== PLAYER DEBUG INFO ===") + debugPrint("Name: " .. (PlayerData.charinfo and PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname or "Unbekannt")) + debugPrint("Job: " .. (PlayerData.job and PlayerData.job.name or "Unbekannt")) + debugPrint("CitizenID: " .. (PlayerData.citizenid or "Unbekannt")) + debugPrint("Server ID: " .. GetPlayerServerId(PlayerId())) + debugPrint("========================") +end + +-- Debug-Command +RegisterCommand('licensedebug', function() + if not Config.Debug then + showNotification('Debug-Modus ist deaktiviert!', 'error') + return + end + + debugPlayerInfo() + + debugPrint("=== SYSTEM DEBUG INFO ===") + debugPrint("Nearby Players: " .. #nearbyPlayers) + debugPrint("License Showing: " .. tostring(isLicenseShowing)) + debugPrint("Menu Open: " .. tostring(lib.getOpenContextMenu() ~= nil)) + debugPrint("Cached Players: " .. #playerCache) + debugPrint("Menu History: " .. #menuHistory) + debugPrint("Pending Requests: " .. #pendingRequests) + debugPrint("========================") + + showNotification('Debug-Informationen in der Konsole ausgegeben!', 'success') +end, false) + +-- Erweiterte Keybind-Behandlung +CreateThread(function() + while true do + Wait(0) + + -- ESC-Taste für Lizenz schließen + if isLicenseShowing then + if IsControlJustPressed(0, 322) then -- ESC + closeLicense() + end + end + + -- Zusätzliche Hotkeys (falls konfiguriert) + if Config.Keybinds and Config.Keybinds.emergency_close then + if IsControlJustPressed(0, Config.Keybinds.emergency_close.control) then + cleanup() + showNotification('Notfall-Schließung aktiviert!', 'info') + end + end + + -- Performance-Optimierung + if not isLicenseShowing and not lib.getOpenContextMenu() then + Wait(500) end end end) -debugPrint("License-System Client geladen (Erweiterte Erstellung)") +-- Erweiterte Netzwerk-Events mit Retry-Mechanismus +local function sendEventWithRetry(eventName, data, maxRetries) + maxRetries = maxRetries or 3 + local retries = 0 + + local function attemptSend() + retries = retries + 1 + debugPrint("Sende Event: " .. eventName .. " (Versuch " .. retries .. "/" .. maxRetries .. ")") + + TriggerServerEvent(eventName, table.unpack(data or {})) + + -- Timeout für Response + CreateThread(function() + Wait(5000) -- 5 Sekunden Timeout + + if retries < maxRetries then + debugPrint("^3Timeout für Event " .. eventName .. " - Wiederhole...^7") + attemptSend() + else + debugPrint("^1Maximale Wiederholungen für Event " .. eventName .. " erreicht^7") + showNotification('Netzwerk-Fehler! Bitte versuche es später erneut.', 'error') + end + end) + end + + attemptSend() +end + +-- Erweiterte Export-Funktionen für andere Resources +exports('hasLicense', function(licenseType) + -- Diese Funktion kann von anderen Resources verwendet werden + local PlayerData = QBCore.Functions.GetPlayerData() + if not PlayerData or not PlayerData.citizenid then return false end + + -- Hier würde normalerweise eine Server-Anfrage gemacht werden + -- Für jetzt geben wir false zurück + return false +end) + +exports('showPlayerLicense', function(targetId, licenseType) + -- Export für andere Resources um Lizenzen anzuzeigen + if licenseType then + TriggerServerEvent('license-system:server:requestSpecificLicense', targetId, licenseType) + else + showPlayerLicense(targetId) + end +end) + +exports('openLicenseMenu', function() + -- Export für andere Resources um das Lizenz-Menü zu öffnen + openLicenseMenu() +end) + + +-- Neuer Event-Handler für benutzerdefinierte Lizenzen +RegisterNetEvent('license-system:server:issueCustomLicense', function(targetId, licenseType, customData, classes) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + local TargetPlayer = QBCore.Functions.GetPlayer(targetId) + + if not Player or not TargetPlayer then + debugPrint("Spieler nicht gefunden: " .. src .. " -> " .. targetId) + return + end + + -- Berechtigung prüfen + if not isAuthorized(Player.PlayerData.job.name) then + TriggerClientEvent('QBCore:Notify', src, Config.Notifications.no_permission.message, Config.Notifications.no_permission.type) + return + end + + -- Lizenz-Konfiguration prüfen + local config = Config.LicenseTypes[licenseType] + if not config then + debugPrint("Unbekannter Lizenztyp: " .. licenseType) + return + end + + debugPrint("Erstelle benutzerdefinierte Lizenz: " .. licenseType .. " für " .. TargetPlayer.PlayerData.citizenid) + + -- Benutzerdefinierte Daten validieren und bereinigen + local validatedData = {} + + for _, field in ipairs(config.custom_fields or {}) do + local value = customData[field.name] + + -- Pflichtfeld-Prüfung + if field.required and (not value or value == "") then + TriggerClientEvent('QBCore:Notify', src, "Feld '" .. field.label .. "' ist erforderlich", "error") + return + end + + -- Wert bereinigen und validieren + if value and value ~= "" then + value = string.gsub(value, "'", "''") -- SQL-Injection Schutz + + -- Typ-spezifische Validierung + if field.type == "url" and not string.match(value, "^https?://") then + TriggerClientEvent('QBCore:Notify', src, "Ungültige URL in Feld '" .. field.label .. "'", "error") + return + end + + if field.type == "date" and not string.match(value, "^%d%d%.%d%d%.%d%d%d%d$") then + TriggerClientEvent('QBCore:Notify', src, "Ungültiges Datum in Feld '" .. field.label .. "'", "error") + return + end + + validatedData[field.name] = value + end + end + + -- Klassen validieren + local validatedClasses = {} + if config.classes and classes then + for _, class in ipairs(classes) do + if config.classes[class.key] then + table.insert(validatedClasses, class) + end + end + end + + -- Lizenz in Datenbank speichern + local success = saveCustomLicenseToDB( + TargetPlayer.PlayerData.citizenid, + licenseType, + Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname, + validatedData, + validatedClasses + ) + + if success then + debugPrint("Benutzerdefinierte Lizenz erfolgreich gespeichert") + + -- Cache invalidieren + invalidateCache(TargetPlayer.PlayerData.citizenid, licenseType) + + -- Benachrichtigungen + TriggerClientEvent('QBCore:Notify', src, Config.Notifications.license_issued.message, Config.Notifications.license_issued.type) + TriggerClientEvent('QBCore:Notify', targetId, "Du hast eine " .. config.label .. " erhalten!", "success") + + -- Events + TriggerClientEvent('license-system:client:licenseIssued', src, targetId, licenseType) + TriggerClientEvent('license-system:client:refreshMenu', src) + + -- Log + debugPrint("Lizenz ausgestellt: " .. licenseType .. " von " .. Player.PlayerData.name .. " an " .. TargetPlayer.PlayerData.name) + else + debugPrint("Fehler beim Speichern der benutzerdefinierten Lizenz") + TriggerClientEvent('QBCore:Notify', src, "Fehler beim Ausstellen der Lizenz", "error") + end +end) + +-- Funktion zum Speichern benutzerdefinierter Lizenzen +function saveCustomLicenseToDB(citizenid, licenseType, issuedBy, customData, classes) + local config = Config.LicenseTypes[licenseType] + if not config then return false end + + -- Ablaufdatum berechnen + local expireDate = nil + if config.validity_days then + expireDate = os.date('%Y-%m-%d %H:%M:%S', os.time() + (config.validity_days * 24 * 60 * 60)) + end + + -- Holder-Name aus Custom-Data oder Standard + local holderName = customData.holder_name or "Unbekannt" + + return safeDBOperation(function() + -- Alte Lizenzen deaktivieren + local deactivateQuery = "UPDATE player_licenses SET is_active = 0 WHERE citizenid = ? AND license_type = ?" + MySQL.query.await(deactivateQuery, {citizenid, licenseType}) + + -- Neue Lizenz einfügen + local insertQuery = [[ + INSERT INTO player_licenses + (citizenid, license_type, name, issue_date, expire_date, issued_by, is_active, classes, custom_data, holder_name) + VALUES (?, ?, ?, NOW(), ?, ?, 1, ?, ?, ?) + ]] + + local result = MySQL.insert.await(insertQuery, { + citizenid, + licenseType, + config.label, + expireDate, + issuedBy, + json.encode(classes or {}), + json.encode(customData or {}), + holderName + }) + + return result and result > 0 + end, "Benutzerdefinierte Lizenz speichern") +end + +-- Erweiterte Lizenz-Abruf-Funktion +function getLicenseFromDB(citizenid, licenseType, skipCache) + -- Cache prüfen + local cacheKey = citizenid .. "_" .. licenseType + if not skipCache and licenseCache[cacheKey] then + debugPrint("Lizenz aus Cache geladen: " .. licenseType) + return licenseCache[cacheKey] + end + + local license = safeDBOperation(function() + local query = [[ + SELECT *, + CASE + WHEN expire_date IS NULL THEN 1 + WHEN expire_date > NOW() THEN 1 + ELSE 0 + END as is_valid + FROM player_licenses + WHERE citizenid = ? AND license_type = ? AND is_active = 1 + ORDER BY created_at DESC + LIMIT 1 + ]] + + local result = MySQL.query.await(query, {citizenid, licenseType}) + return result and result[1] or nil + end, "Lizenz abrufen") + + if license then + -- Custom-Data und Classes parsen + if license.custom_data then + license.custom_data_parsed = json.decode(license.custom_data) + end + + if license.classes then + license.classes_parsed = json.decode(license.classes) + end + + -- Cache speichern + licenseCache[cacheKey] = license + debugPrint("Lizenz in Cache gespeichert: " .. licenseType) + end + + return license +end + + +-- Add to client/main.lua +RegisterNetEvent('license-system:client:openCamera', function(targetId, licenseData) + debugPrint("Event erhalten: openCamera für Spieler " .. tostring(targetId)) + + -- Store the data temporarily + currentLicenseData = licenseData + currentTargetId = targetId + + SendNUIMessage({ + action = 'openCamera', + citizenid = QBCore.Functions.GetPlayerData().citizenid + }) +end) + +-- Enhance the NUI callback for photo saving +RegisterNUICallback('savePhoto', function(data, cb) + debugPrint("NUI Callback: savePhoto") + + if data.photo and currentLicenseData and currentTargetId then + -- Add photo to license data + currentLicenseData.photo_url = data.photo + + -- Issue the license with the photo + TriggerServerEvent('license-system:server:issueManualLicense', currentTargetId, currentLicenseData) + + -- Reset temporary data + currentLicenseData = nil + currentTargetId = nil + + if cb and type(cb) == "function" then + cb('ok') + end + else + debugPrint("^1Fehler: Foto-Daten unvollständig oder keine aktuelle Lizenz-Ausstellung^7") + + if cb and type(cb) == "function" then + cb('error') + end + end +end) + + +debugPrint("License-System Server erweitert geladen (Custom License Support)") \ No newline at end of file diff --git a/resources/[tools]/nordi_license/config.lua b/resources/[tools]/nordi_license/config.lua index 3ed757ab3..d9a05f41f 100644 --- a/resources/[tools]/nordi_license/config.lua +++ b/resources/[tools]/nordi_license/config.lua @@ -1,347 +1,317 @@ Config = {} --- Debug-Modus +-- Allgemeine Einstellungen Config.Debug = true +Config.UseBackgroundImages = true +Config.MaxLicenseAge = 365 -- Tage bis Ablauf +Config.RenewalDays = 30 -- Tage vor Ablauf für Verlängerung -- Berechtigte Jobs Config.AuthorizedJobs = { ['police'] = true, ['sheriff'] = true, ['government'] = true, - ['doj'] = true, - ['ambulance'] = true, - ['mechanic'] = true + ['judge'] = true, + ['lawyer'] = true, + ['ambulance'] = true, -- Für medizinische Lizenzen + ['mechanic'] = true -- Für Fahrzeug-Lizenzen } --- Benachrichtigungen -Config.Notifications = { - no_permission = { - message = "Du hast keine Berechtigung dafür!", - type = "error" - }, - license_issued = { - message = "Lizenz erfolgreich ausgestellt!", - type = "success" - }, - license_revoked = { - message = "Lizenz erfolgreich entzogen!", - type = "success" - } +-- Jobs that can reactivate specific license types +Config.ReactivationPermissions = { + ['police'] = {'weapon_license', 'drivers_license'}, + ['admin'] = {'id_card', 'passport', 'business_license'}, + ['ambulance'] = {'medical_license'}, + ['driving_school'] = {'drivers_license'}, + ['harbor'] = {'boat_license'}, + ['airport'] = {'pilot_license'} } --- Lizenz-Typen (ERWEITERT mit benutzerdefinierten Feldern) + +-- Lizenz-Typen Config.LicenseTypes = { ['id_card'] = { label = 'Personalausweis', - description = 'Offizieller Personalausweis', - price = 50, - validity_days = nil, -- Unbegrenzt gültig - color = '#2E86AB', icon = 'fas fa-id-card', - template = 'id_card', - custom_fields = { - { - name = 'birth_date', - label = 'Geburtsdatum', - type = 'date', - required = true, - placeholder = 'TT.MM.JJJJ' - }, - { - name = 'birth_place', - label = 'Geburtsort', - type = 'text', - required = true, - placeholder = 'z.B. Los Santos' - }, - { - name = 'nationality', - label = 'Staatsangehörigkeit', - type = 'select', - required = true, - options = { - {value = 'usa', label = 'USA'}, - {value = 'germany', label = 'Deutschland'}, - {value = 'uk', label = 'Vereinigtes Königreich'}, - {value = 'france', label = 'Frankreich'}, - {value = 'other', label = 'Andere'} - } - }, - { - name = 'address', - label = 'Adresse', - type = 'textarea', - required = true, - placeholder = 'Vollständige Adresse' - }, - { - name = 'height', - label = 'Größe (cm)', - type = 'number', - required = false, - placeholder = 'z.B. 180' - }, - { - name = 'eye_color', - label = 'Augenfarbe', - type = 'select', - required = false, - options = { - {value = 'brown', label = 'Braun'}, - {value = 'blue', label = 'Blau'}, - {value = 'green', label = 'Grün'}, - {value = 'gray', label = 'Grau'}, - {value = 'hazel', label = 'Haselnuss'} - } - }, - { - name = 'photo_url', - label = 'Foto-URL', - type = 'url', - required = false, - placeholder = 'https://example.com/photo.jpg' - } - } + color = '#667eea', + price = 50, + required_items = {}, + can_expire = true, + validity_days = 3650, -- 10 Jahre + required_job = nil, + description = 'Offizieller Personalausweis' }, - ['driver_license'] = { + ['drivers_license'] = { label = 'Führerschein', - description = 'Führerschein für Kraftfahrzeuge', - price = 150, - validity_days = 1825, -- 5 Jahre - color = '#F18F01', icon = 'fas fa-car', - template = 'driver_license', + color = '#f093fb', + price = 500, + required_items = {'driving_test_certificate'}, + can_expire = true, + validity_days = 5475, -- 15 Jahre + required_job = 'driving_school', + description = 'Berechtigung zum Führen von Kraftfahrzeugen', classes = { - ['A'] = 'Motorräder', - ['B'] = 'PKW', - ['C'] = 'LKW', - ['D'] = 'Busse' - }, - custom_fields = { - { - name = 'birth_date', - label = 'Geburtsdatum', - type = 'date', - required = true, - placeholder = 'TT.MM.JJJJ' - }, - { - name = 'address', - label = 'Adresse', - type = 'textarea', - required = true, - placeholder = 'Vollständige Adresse' - }, - { - name = 'restrictions', - label = 'Beschränkungen', - type = 'textarea', - required = false, - placeholder = 'z.B. Brille erforderlich' - }, - { - name = 'photo_url', - label = 'Foto-URL', - type = 'url', - required = false, - placeholder = 'https://example.com/photo.jpg' - } + 'A', 'A1', 'A2', 'B', 'BE', 'C', 'CE', 'D', 'DE' } }, ['weapon_license'] = { label = 'Waffenschein', - description = 'Berechtigung zum Führen von Waffen', - price = 500, - validity_days = 365, -- 1 Jahr - color = '#C73E1D', icon = 'fas fa-crosshairs', - template = 'weapon_license', - custom_fields = { - { - name = 'birth_date', - label = 'Geburtsdatum', - type = 'date', - required = true, - placeholder = 'TT.MM.JJJJ' - }, - { - name = 'weapon_type', - label = 'Waffentyp', - type = 'select', - required = true, - options = { - {value = 'pistol', label = 'Pistole'}, - {value = 'rifle', label = 'Gewehr'}, - {value = 'shotgun', label = 'Schrotflinte'}, - {value = 'all', label = 'Alle Waffentypen'} - } - }, - { - name = 'purpose', - label = 'Verwendungszweck', - type = 'select', - required = true, - options = { - {value = 'self_defense', label = 'Selbstverteidigung'}, - {value = 'sport', label = 'Sport'}, - {value = 'hunting', label = 'Jagd'}, - {value = 'collection', label = 'Sammlung'}, - {value = 'security', label = 'Sicherheitsdienst'} - } - }, - { - name = 'restrictions', - label = 'Beschränkungen', - type = 'textarea', - required = false, - placeholder = 'Besondere Auflagen oder Beschränkungen' - }, - { - name = 'photo_url', - label = 'Foto-URL', - type = 'url', - required = false, - placeholder = 'https://example.com/photo.jpg' - } + color = '#4facfe', + price = 2500, + required_items = {'weapon_course_certificate', 'psychological_evaluation'}, + can_expire = true, + validity_days = 1095, -- 3 Jahre + required_job = 'police', + description = 'Berechtigung zum Führen von Schusswaffen', + restrictions = { + 'Nur für registrierte Waffen', + 'Regelmäßige Überprüfung erforderlich', + 'Nicht übertragbar' } }, - ['pilot_license'] = { - label = 'Pilotenlizenz', - description = 'Berechtigung zum Führen von Luftfahrzeugen', - price = 1000, - validity_days = 730, -- 2 Jahre - color = '#6A994E', - icon = 'fas fa-plane', - template = 'pilot_license', - classes = { - ['PPL'] = 'Private Pilot License', - ['CPL'] = 'Commercial Pilot License', - ['ATPL'] = 'Airline Transport Pilot License', - ['HELI'] = 'Helicopter License' - }, - custom_fields = { - { - name = 'birth_date', - label = 'Geburtsdatum', - type = 'date', - required = true, - placeholder = 'TT.MM.JJJJ' - }, - { - name = 'medical_cert', - label = 'Medical Certificate', - type = 'text', - required = true, - placeholder = 'z.B. Class 1, Class 2' - }, - { - name = 'flight_hours', - label = 'Flugstunden', - type = 'number', - required = true, - placeholder = 'Gesamte Flugstunden' - }, - { - name = 'aircraft_types', - label = 'Luftfahrzeugtypen', - type = 'textarea', - required = false, - placeholder = 'Berechtigte Luftfahrzeugtypen' - }, - { - name = 'restrictions', - label = 'Beschränkungen', - type = 'textarea', - required = false, - placeholder = 'z.B. nur bei Tageslicht' - }, - { - name = 'photo_url', - label = 'Foto-URL', - type = 'url', - required = false, - placeholder = 'https://example.com/photo.jpg' - } - } + ['passport'] = { + label = 'Reisepass', + icon = 'fas fa-passport', + color = '#43e97b', + price = 150, + required_items = {'birth_certificate', 'id_card'}, + can_expire = true, + validity_days = 3650, -- 10 Jahre + required_job = 'government', + description = 'Internationales Reisedokument' }, ['business_license'] = { label = 'Gewerbeschein', - description = 'Berechtigung zur Ausübung eines Gewerbes', - price = 300, - validity_days = 365, -- 1 Jahr - color = '#7209B7', icon = 'fas fa-briefcase', - template = 'business_license', - custom_fields = { - { - name = 'business_name', - label = 'Firmenname', - type = 'text', - required = true, - placeholder = 'Name des Unternehmens' - }, - { - name = 'business_type', - label = 'Gewerbetyp', - type = 'select', - required = true, - options = { - {value = 'retail', label = 'Einzelhandel'}, - {value = 'restaurant', label = 'Gastronomie'}, - {value = 'service', label = 'Dienstleistung'}, - {value = 'manufacturing', label = 'Herstellung'}, - {value = 'transport', label = 'Transport'}, - {value = 'other', label = 'Sonstiges'} - } - }, - { - name = 'business_address', - label = 'Geschäftsadresse', - type = 'textarea', - required = true, - placeholder = 'Vollständige Geschäftsadresse' - }, - { - name = 'tax_number', - label = 'Steuernummer', - type = 'text', - required = false, - placeholder = 'z.B. 123/456/78901' - }, - { - name = 'employees', - label = 'Anzahl Mitarbeiter', - type = 'number', - required = false, - placeholder = 'Geplante Mitarbeiterzahl' - }, - { - name = 'logo_url', - label = 'Firmenlogo-URL', - type = 'url', - required = false, - placeholder = 'https://example.com/logo.jpg' - } + color = '#fa709a', + price = 1000, + required_items = {'business_plan', 'tax_certificate'}, + can_expire = true, + validity_days = 1825, -- 5 Jahre + required_job = 'government', + description = 'Berechtigung zur Ausübung eines Gewerbes' + }, + ['pilot_license'] = { + label = 'Pilotenlizenz', + icon = 'fas fa-plane', + color = '#667eea', + price = 5000, + required_items = {'flight_hours_log', 'medical_certificate'}, + can_expire = true, + validity_days = 730, -- 2 Jahre + required_job = 'airport', + description = 'Berechtigung zum Führen von Luftfahrzeugen' + }, + ['boat_license'] = { + label = 'Bootsführerschein', + icon = 'fas fa-ship', + color = '#00f2fe', + price = 800, + required_items = {'boat_course_certificate'}, + can_expire = true, + validity_days = 1825, -- 5 Jahre + required_job = 'harbor', + description = 'Berechtigung zum Führen von Wasserfahrzeugen' + }, + ['medical_license'] = { + label = 'Approbation', + icon = 'fas fa-user-md', + color = '#ff6b6b', + price = 0, -- Kostenlos für Ärzte + required_items = {'medical_degree', 'medical_exam'}, + can_expire = false, + validity_days = nil, + required_job = 'ambulance', + description = 'Berechtigung zur Ausübung der Heilkunde' + }, + ['hunting_license'] = { + label = 'Jagdschein', + icon = 'fas fa-crosshairs', + color = '#8b5a3c', + price = 300, + required_items = {'hunting_course_certificate'}, + can_expire = true, + validity_days = 1095, -- 3 Jahre + required_job = 'police', + description = 'Berechtigung zur Ausübung der Jagd' + }, + ['fishing_license'] = { + label = 'Angelschein', + icon = 'fas fa-fish', + color = '#4ecdc4', + price = 50, + required_items = {}, + can_expire = true, + validity_days = 365, -- 1 Jahr + required_job = 'police', + description = 'Berechtigung zum Angeln in öffentlichen Gewässern' + } +} + +-- Standorte für Lizenz-Ausgabe +Config.LicenseLocations = { + ['city_hall'] = { + label = 'Rathaus', + coords = vector3(-544.85, -204.13, 38.22), + blip = { + sprite = 419, + color = 2, + scale = 0.8 + }, + available_licenses = { + 'id_card', 'passport', 'business_license' + }, + ped = { + model = 'a_m_m_business_01', + coords = vector4(-544.9543, -204.8450, 37.2151, 219.1676) + } + }, + ['driving_school'] = { + label = 'Fahrschule', + coords = vector3(-829.22, -1209.58, 7.33), + blip = { + sprite = 225, + color = 46, + scale = 0.8 + }, + available_licenses = { + 'drivers_license' + }, + ped = { + model = 'a_m_y_business_02', + coords = vector4(-829.22, -1209.58, 6.33, 90.0) + } + }, + ['police_station'] = { + label = 'Polizeiwache', + coords = vector3(441.07, -979.76, 30.69), + blip = { + sprite = 60, + color = 29, + scale = 0.8 + }, + available_licenses = { + 'weapon_license' + }, + ped = { + model = 's_m_y_cop_01', + coords = vector4(441.07, -979.76, 29.69, 270.0) + } + }, + ['hospital'] = { + label = 'Krankenhaus', + coords = vector3(307.7, -1433.4, 29.9), + blip = { + sprite = 61, + color = 1, + scale = 0.8 + }, + available_licenses = { + 'medical_license' + }, + ped = { + model = 's_m_m_doctor_01', + coords = vector4(307.7, -1433.4, 28.9, 180.0) } } } --- UI-Einstellungen -Config.UI = { - position = 'center', -- 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right' - animation = 'fade', -- 'fade', 'slide', 'zoom' - theme = 'dark', -- 'dark', 'light' - blur_background = true, - max_image_size = 5 * 1024 * 1024, -- 5MB - allowed_image_formats = {'jpg', 'jpeg', 'png', 'gif', 'webp'}, - default_avatar = 'https://via.placeholder.com/150x200/cccccc/666666?text=Kein+Foto' +-- Kommandos +Config.Commands = { + ['license'] = { + name = 'lizenz', + help = 'Lizenz-System öffnen', + restricted = true -- Nur für berechtigte Jobs + }, + ['mylicense'] = { + name = 'meinelizenz', + help = 'Eigene Lizenzen anzeigen', + restricted = false -- Für alle Spieler + }, + ['givelicense'] = { + name = 'givelicense', + help = 'Lizenz an Spieler vergeben', + restricted = true, + admin_only = true + }, + ['revokelicense'] = { + name = 'revokelicense', + help = 'Lizenz entziehen', + restricted = true, + admin_only = false + } } --- Validierung -Config.Validation = { - name_min_length = 2, - name_max_length = 50, - url_pattern = '^https?://.+', - date_pattern = '^%d%d%.%d%d%.%d%d%d%d$', -- DD.MM.YYYY - phone_pattern = '^%+?[%d%s%-%(%)]+$' +-- Keybinds +Config.Keybinds = { + ['open_license_menu'] = { + key = 'F6', + command = 'lizenz', + description = 'Lizenz-System öffnen' + }, + ['show_my_licenses'] = { + key = 'F7', + command = 'meinelizenz', + description = 'Meine Lizenzen anzeigen' + } +} + +-- Benachrichtigungen +Config.Notifications = { + ['no_permission'] = { + message = 'Du hast keine Berechtigung!', + type = 'error' + }, + ['no_players_nearby'] = { + message = 'Keine Spieler in der Nähe!', + type = 'error' + }, + ['license_not_found'] = { + message = 'Keine Lizenz gefunden!', + type = 'error' + }, + ['license_expired'] = { + message = 'Diese Lizenz ist abgelaufen!', + type = 'warning' + }, + ['license_expires_soon'] = { + message = 'Diese Lizenz läuft bald ab!', + type = 'warning' + }, + ['license_granted'] = { + message = 'Lizenz erfolgreich ausgestellt!', + type = 'success' + }, + ['license_revoked'] = { + message = 'Lizenz wurde entzogen!', + type = 'info' + }, + ['photo_saved'] = { + message = 'Foto gespeichert!', + type = 'success' + }, + ['insufficient_funds'] = { + message = 'Nicht genügend Geld!', + type = 'error' + }, + ['missing_items'] = { + message = 'Benötigte Gegenstände fehlen!', + type = 'error' + } +} + +-- Sounds +Config.Sounds = { + ['card_flip'] = 'sounds/card_flip.mp3', + ['camera_shutter'] = 'sounds/camera_shutter.mp3', + ['notification'] = 'sounds/notification.mp3' +} + +-- Datenbank-Einstellungen +Config.Database = { + ['table_name'] = 'player_licenses', + ['auto_cleanup'] = true, -- Alte Lizenzen automatisch löschen + ['cleanup_days'] = 365 -- Nach wie vielen Tagen löschen } diff --git a/resources/[tools]/nordi_license/html/index.html b/resources/[tools]/nordi_license/html/index.html index 874ab116f..bc0d9109e 100644 --- a/resources/[tools]/nordi_license/html/index.html +++ b/resources/[tools]/nordi_license/html/index.html @@ -5,190 +5,270 @@ License System - + + + + - -