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
-
+
+
+
+
-
-
-