diff --git a/resources/[tools]/nordi_license/client/main.lua b/resources/[tools]/nordi_license/client/main.lua index 5bfad1e16..c6f3eba7b 100644 --- a/resources/[tools]/nordi_license/client/main.lua +++ b/resources/[tools]/nordi_license/client/main.lua @@ -1,1322 +1,357 @@ local QBCore = exports['qb-core']:GetCoreObject() - --- Lokale Variablen local isMenuOpen = false -local currentTarget = nil -local nearbyPlayers = {} -local isLicenseShowing = false -local pendingRequests = {} +local currentLicenseData = nil --- Hilfsfunktionen +-- Debug-Funktion local function debugPrint(message) if Config.Debug then - print("^3[License-System Client] " .. message .. "^7") + print("^2[License-System Client] " .. message .. "^7") end end -local function showNotification(message, type) - QBCore.Functions.Notify(message, type or 'primary') -end +-- Hilfsfunktionen +local function getClosestPlayer() + local players = GetActivePlayers() + local closestDistance = -1 + local closestPlayer = -1 + local ped = PlayerPedId() + local coords = GetEntityCoords(ped) --- 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 - }) + 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 end end end - - -- 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 + + return closestPlayer, closestDistance end --- 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 +-- 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 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 +-- 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 - debugPrint("Zeige Lizenz: " .. licenseData.license.license_type) + local day, month, year = date:match("(%d+)%.(%d+)%.(%d+)") + day, month, year = tonumber(day), tonumber(month), tonumber(year) - 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 + 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 return true end --- Erweiterte showLicense Funktion mit Validierung -local function showLicenseWithValidation(licenseData) - if not validateLicenseData(licenseData) then - showNotification('Ungültige Lizenz-Daten!', 'error') +-- Bild-URL validieren +local function validateImageUrl(url, callback) + if not url or url == "" then + callback(true, Config.UI.default_avatar) return end - debugPrint("Zeige Lizenz: " .. licenseData.license.license_type) + if not isValidUrl(url) then + callback(false, "Ungültige URL") + return + end - -- 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() + -- Dateiformat prüfen + local extension = string.lower(url:match("%.([^%.]+)$") or "") + local isValidFormat = false + + for _, format in ipairs(Config.UI.allowed_image_formats) do + if extension == format then + isValidFormat = true + break + 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 = 'showLicense', - data = nuitData + action = "openCustomLicenseForm", + data = formData }) - SetNuiFocus(true, true) - isLicenseShowing = true - performanceStats.licenseShows = performanceStats.licenseShows + 1 + isMenuOpen = true 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 +-- 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 --- Cache für Spieler-Daten -local playerCache = {} -local cacheTimeout = 30000 -- 30 Sekunden - -local function getCachedPlayerData(playerId) - local now = GetGameTimer() - local cached = playerCache[playerId] +-- Hauptmenü öffnen +local function openMainMenu() + debugPrint("Öffne Hauptmenü") - if cached and (now - cached.timestamp) < cacheTimeout then - debugPrint("Verwende gecachte Spieler-Daten für ID: " .. playerId) - return cached.data - end + local targetId, distance = getClosestPlayer() - return nil -end - -local function setCachedPlayerData(playerId, data) - playerCache[playerId] = { - data = data, - timestamp = GetGameTimer() + local menuData = { + licenseTypes = Config.LicenseTypes, + targetId = targetId, + targetDistance = distance, + hasTarget = targetId ~= -1 and distance <= 3.0 } - debugPrint("Spieler-Daten gecacht für ID: " .. playerId) + + SetNuiFocus(true, true) + SendNUIMessage({ + action = "openMainMenu", + data = menuData + }) + + isMenuOpen = true 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 +-- NUI-Callbacks +RegisterNUICallback('closeMenu', function(data, cb) + debugPrint("Schließe Menü") + SetNuiFocus(false, false) + isMenuOpen = false + cb('ok') 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) +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) - 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 + 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 - -- Nach Entfernung sortieren - table.sort(players, function(a, b) - return a.distance < b.distance + 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 + }) end) - - debugPrint("Gefundene Spieler in der Nähe: " .. #players) - return players -end +end) --- Überschreibe die ursprüngliche getNearbyPlayers Funktion -getNearbyPlayers = getNearbyPlayersWithCache - --- Erweiterte Notification-Funktion -local function showNotificationWithSound(message, type, sound) - QBCore.Functions.Notify(message, type or 'primary') +RegisterNUICallback('submitCustomLicense', function(data, cb) + debugPrint("Benutzerdefinierte Lizenz eingereicht") + debugPrint("Daten: " .. json.encode(data)) - 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 ===") + local licenseType = data.licenseType + local targetId = data.targetId + local customData = data.customData + local classes = data.classes or {} - if not validateLicenseData(licenseData) then - showNotificationWithSound('Ungültige Lizenz-Daten erhalten!', 'error', 'error') + -- Validierung + local config = Config.LicenseTypes[licenseType] + if not config then + cb({success = false, error = "Unbekannter Lizenztyp"}) 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 - - TaskPlayAnim(playerPed, anim.dict, anim.name, 8.0, -8.0, -1, anim.flag or 49, 0, false, false, false) - end - - -- Lizenz anzeigen - showLicense(licenseData) - - -- Animation nach kurzer Zeit stoppen - if Config.Animations and Config.Animations.show_license then - CreateThread(function() - Wait(2000) - ClearPedTasks(playerPed) - end) - end -end - --- 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 - return nil -end - -local function clearMenuHistory() - menuHistory = {} - debugPrint("Menü-Historie geleert") -end - --- 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 - --- Erweiterte Resource-Stop Handler -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - 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) + -- 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 - debugPrint("QBCore erfolgreich geladen (erweitert)") + -- Spezielle Validierung + if field.type == "date" and not isValidDate(value) then + cb({success = false, error = "Ungültiges Datum in Feld '" .. field.label .. "'"}) + return + end - -- Zusätzliche Initialisierung - Wait(1000) - - -- Performance-Stats zurücksetzen - performanceStats = { - menuOpens = 0, - licenseShows = 0, - errors = 0 - } - - debugPrint("Erweiterte Initialisierung abgeschlossen") - end) + if field.type == "url" and not isValidUrl(value) then + cb({success = false, error = "Ungültige URL in Feld '" .. field.label .. "'"}) + return + end + 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") end end) --- Erweiterte Debug-Funktionen -local function debugPlayerInfo() - if not Config.Debug then return end +RegisterNetEvent('license-system:client:receiveMyLicense', function(licenseData, licenseType) + debugPrint("Eigene Lizenz erhalten: " .. (licenseType or "unbekannt")) - 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 + 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) - 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') + 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() + end +end) + +-- Commands +RegisterCommand('lizenz', function() + if not isMenuOpen then + openMainMenu() + end 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) +RegisterCommand('ausweis', function() + TriggerServerEvent('license-system:server:requestMyLicense', 'id_card') +end, false) + +-- Keybinds +RegisterKeyMapping('lizenz', 'Lizenz-System öffnen', 'keyboard', 'F6') +RegisterKeyMapping('ausweis', 'Eigenen Ausweis zeigen', 'keyboard', 'F7') + +-- Cleanup +AddEventHandler('onResourceStop', function(resourceName) + if GetCurrentResourceName() == resourceName then + if isMenuOpen then + SetNuiFocus(false, false) + isMenuOpen = false end end end) --- 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) - --- Finaler Debug-Output -debugPrint("License-System Client vollständig geladen - Alle Funktionen verfügbar") - +debugPrint("License-System Client geladen (Erweiterte Erstellung)") diff --git a/resources/[tools]/nordi_license/config.lua b/resources/[tools]/nordi_license/config.lua index facd63b09..3ed757ab3 100644 --- a/resources/[tools]/nordi_license/config.lua +++ b/resources/[tools]/nordi_license/config.lua @@ -1,306 +1,347 @@ Config = {} --- Allgemeine Einstellungen +-- Debug-Modus 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, - ['judge'] = true, - ['lawyer'] = true, - ['ambulance'] = true, -- Für medizinische Lizenzen - ['mechanic'] = true -- Für Fahrzeug-Lizenzen -} - --- Lizenz-Typen -Config.LicenseTypes = { - ['id_card'] = { - label = 'Personalausweis', - icon = 'fas fa-id-card', - color = '#667eea', - price = 50, - required_items = {}, - can_expire = true, - validity_days = 3650, -- 10 Jahre - required_job = nil, - description = 'Offizieller Personalausweis' - }, - ['drivers_license'] = { - label = 'Führerschein', - icon = 'fas fa-car', - 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', 'A1', 'A2', 'B', 'BE', 'C', 'CE', 'D', 'DE' - } - }, - ['weapon_license'] = { - label = 'Waffenschein', - icon = 'fas fa-crosshairs', - 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' - } - }, - ['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', - icon = 'fas fa-briefcase', - 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 = 'ranger', - 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 = nil, - 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) - } - } -} - --- 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 - } -} - --- 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' - } + ['doj'] = true, + ['ambulance'] = true, + ['mechanic'] = true } -- Benachrichtigungen Config.Notifications = { - ['no_permission'] = { - message = 'Du hast keine Berechtigung!', - type = 'error' + no_permission = { + message = "Du hast keine Berechtigung dafür!", + type = "error" }, - ['no_players_nearby'] = { - message = 'Keine Spieler in der Nähe!', - type = 'error' + license_issued = { + message = "Lizenz erfolgreich ausgestellt!", + type = "success" }, - ['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' + license_revoked = { + message = "Lizenz erfolgreich entzogen!", + type = "success" } } --- Sounds -Config.Sounds = { - ['card_flip'] = 'sounds/card_flip.mp3', - ['camera_shutter'] = 'sounds/camera_shutter.mp3', - ['notification'] = 'sounds/notification.mp3' +-- Lizenz-Typen (ERWEITERT mit benutzerdefinierten Feldern) +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' + } + } + }, + ['driver_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', + 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' + } + } + }, + ['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' + } + } + }, + ['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' + } + } + }, + ['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' + } + } + } } --- 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 +-- 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' +} + +-- 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%-%(%)]+$' } diff --git a/resources/[tools]/nordi_license/html/index.html b/resources/[tools]/nordi_license/html/index.html index bc0d9109e..874ab116f 100644 --- a/resources/[tools]/nordi_license/html/index.html +++ b/resources/[tools]/nordi_license/html/index.html @@ -5,270 +5,190 @@ License System - - - - + - - - - -