diff --git a/resources/[tools]/nordi_license/client/main.lua b/resources/[tools]/nordi_license/client/main.lua
new file mode 100644
index 000000000..f00da1395
--- /dev/null
+++ b/resources/[tools]/nordi_license/client/main.lua
@@ -0,0 +1,391 @@
+local QBCore = exports['qb-core']:GetCoreObject()
+local playerLicenses = {}
+local nearbyPlayers = {}
+
+-- Keybind registrieren
+RegisterKeyMapping(Config.Command, 'Lizenz-Menü öffnen', 'keyboard', Config.OpenKey)
+
+-- Events
+RegisterNetEvent('license-system:client:openMenu', function()
+ TriggerServerEvent('license-system:server:getLicenses')
+end)
+
+RegisterNetEvent('license-system:client:receiveLicenses', function(licenses)
+ playerLicenses = licenses
+ openMainMenu()
+end)
+
+RegisterNetEvent('license-system:client:receiveNearbyPlayers', function(players)
+ nearbyPlayers = {}
+ local playerPed = PlayerPedId()
+ local playerCoords = GetEntityCoords(playerPed)
+
+ for _, playerId in ipairs(players) do
+ if playerId ~= GetPlayerServerId(PlayerId()) then
+ local targetPed = GetPlayerPed(GetPlayerFromServerId(playerId))
+ if targetPed and targetPed ~= 0 then
+ local targetCoords = GetEntityCoords(targetPed)
+ local distance = #(playerCoords - targetCoords)
+
+ if distance <= 5.0 then
+ local targetPlayer = QBCore.Functions.GetPlayerData(playerId)
+ table.insert(nearbyPlayers, {
+ id = playerId,
+ name = targetPlayer and (targetPlayer.charinfo.firstname .. ' ' .. targetPlayer.charinfo.lastname) or 'Unbekannt'
+ })
+ end
+ end
+ end
+ end
+end)
+
+RegisterNetEvent('license-system:client:viewLicense', function(licenseData, issuerName)
+ showLicenseOverlay(licenseData, issuerName)
+end)
+
+-- Hauptmenü
+function openMainMenu()
+ local options = {
+ {
+ title = 'Meine Lizenzen anzeigen',
+ description = 'Zeige alle deine Lizenzen an',
+ icon = 'fas fa-eye',
+ onSelect = function()
+ showMyLicenses()
+ end
+ },
+ {
+ title = 'Lizenz zeigen',
+ description = 'Zeige jemandem in der Nähe eine Lizenz',
+ icon = 'fas fa-share',
+ onSelect = function()
+ showLicenseToPlayer()
+ end
+ }
+ }
+
+ -- Admin-Optionen hinzufügen
+ local PlayerData = QBCore.Functions.GetPlayerData()
+ if PlayerData.job and Config.AuthorizedJobs[PlayerData.job.name] then
+ table.insert(options, {
+ title = 'Lizenz ausstellen',
+ description = 'Stelle eine neue Lizenz aus',
+ icon = 'fas fa-plus',
+ onSelect = function()
+ issueLicenseMenu()
+ end
+ })
+
+ table.insert(options, {
+ title = 'Lizenz verwalten',
+ description = 'Entziehe oder stelle Lizenzen wieder her',
+ icon = 'fas fa-cog',
+ onSelect = function()
+ manageLicensesMenu()
+ end
+ })
+ end
+
+ lib.registerContext({
+ id = 'license_main_menu',
+ title = 'Lizenz-System',
+ options = options
+ })
+
+ lib.showContext('license_main_menu')
+end
+
+-- Meine Lizenzen anzeigen
+function showMyLicenses()
+ local options = {}
+
+ for _, license in ipairs(playerLicenses) do
+ local licenseConfig = Config.Licenses[license.license_type]
+ if licenseConfig then
+ local statusText = license.is_active and "✅ Aktiv" or "❌ Entzogen"
+ table.insert(options, {
+ title = licenseConfig.label,
+ description = 'Status: ' .. statusText .. ' | Ausgestellt: ' .. license.issue_date,
+ icon = licenseConfig.icon,
+ onSelect = function()
+ showLicenseOverlay(license)
+ end
+ })
+ end
+ end
+
+ if #options == 0 then
+ options = {{
+ title = 'Keine Lizenzen vorhanden',
+ description = 'Du besitzt derzeit keine Lizenzen',
+ icon = 'fas fa-exclamation-triangle'
+ }}
+ end
+
+ lib.registerContext({
+ id = 'my_licenses_menu',
+ title = 'Meine Lizenzen',
+ menu = 'license_main_menu',
+ options = options
+ })
+
+ lib.showContext('my_licenses_menu')
+end
+
+-- Lizenz jemandem zeigen
+function showLicenseToPlayer()
+ TriggerServerEvent('license-system:server:getNearbyPlayers')
+
+ Wait(500) -- Kurz warten für die Antwort
+
+ if #nearbyPlayers == 0 then
+ lib.notify({
+ title = 'Lizenz-System',
+ description = 'Keine Spieler in der Nähe gefunden',
+ type = 'error'
+ })
+ return
+ end
+
+ local licenseOptions = {}
+ for _, license in ipairs(playerLicenses) do
+ if license.is_active then
+ local licenseConfig = Config.Licenses[license.license_type]
+ if licenseConfig then
+ table.insert(licenseOptions, {
+ title = licenseConfig.label,
+ description = 'Ausgestellt: ' .. license.issue_date,
+ icon = licenseConfig.icon,
+ onSelect = function()
+ selectPlayerToShow(license)
+ end
+ })
+ end
+ end
+ end
+
+ if #licenseOptions == 0 then
+ lib.notify({
+ title = 'Lizenz-System',
+ description = 'Du hast keine aktiven Lizenzen zum Zeigen',
+ type = 'error'
+ })
+ return
+ end
+
+ lib.registerContext({
+ id = 'select_license_to_show',
+ title = 'Lizenz auswählen',
+ menu = 'license_main_menu',
+ options = licenseOptions
+ })
+
+ lib.showContext('select_license_to_show')
+end
+
+function selectPlayerToShow(license)
+ local playerOptions = {}
+
+ for _, player in ipairs(nearbyPlayers) do
+ table.insert(playerOptions, {
+ title = player.name,
+ description = 'Zeige diesem Spieler deine Lizenz',
+ icon = 'fas fa-user',
+ onSelect = function()
+ TriggerServerEvent('license-system:server:showLicense', player.id, license)
+ lib.notify({
+ title = 'Lizenz-System',
+ description = 'Lizenz wurde ' .. player.name .. ' gezeigt',
+ type = 'success'
+ })
+ end
+ })
+ end
+
+ lib.registerContext({
+ id = 'select_player_to_show',
+ title = 'Spieler auswählen',
+ menu = 'select_license_to_show',
+ options = playerOptions
+ })
+
+ lib.showContext('select_player_to_show')
+end
+
+-- Lizenz ausstellen
+function issueLicenseMenu()
+ local PlayerData = QBCore.Functions.GetPlayerData()
+ local authorizedLicenses = Config.AuthorizedJobs[PlayerData.job.name].canIssue
+
+ local options = {}
+ for _, licenseType in ipairs(authorizedLicenses) do
+ local licenseConfig = Config.Licenses[licenseType]
+ if licenseConfig then
+ table.insert(options, {
+ title = licenseConfig.label,
+ description = 'Stelle einen ' .. licenseConfig.label .. ' aus',
+ icon = licenseConfig.icon,
+ onSelect = function()
+ selectPlayerForLicense(licenseType)
+ end
+ })
+ end
+ end
+
+ lib.registerContext({
+ id = 'issue_license_menu',
+ title = 'Lizenz ausstellen',
+ menu = 'license_main_menu',
+ options = options
+ })
+
+ lib.showContext('issue_license_menu')
+end
+
+function selectPlayerForLicense(licenseType)
+ TriggerServerEvent('license-system:server:getNearbyPlayers')
+
+ Wait(500)
+
+ if #nearbyPlayers == 0 then
+ lib.notify({
+ title = 'Lizenz-System',
+ description = 'Keine Spieler in der Nähe gefunden',
+ type = 'error'
+ })
+ return
+ end
+
+ local playerOptions = {}
+ for _, player in ipairs(nearbyPlayers) do
+ table.insert(playerOptions, {
+ title = player.name,
+ description = 'Stelle diesem Spieler eine Lizenz aus',
+ icon = 'fas fa-user',
+ onSelect = function()
+ openLicenseForm(player.id, licenseType)
+ end
+ })
+ end
+
+ lib.registerContext({
+ id = 'select_player_for_license',
+ title = 'Spieler auswählen',
+ menu = 'issue_license_menu',
+ options = playerOptions
+ })
+
+ lib.showContext('select_player_for_license')
+end
+
+function openLicenseForm(targetId, licenseType)
+ local licenseConfig = Config.Licenses[licenseType]
+ local input = {}
+
+ if licenseConfig.fields.name then
+ table.insert(input, {
+ type = 'input',
+ isRequired = true,
+ label = 'Name',
+ placeholder = 'Vor- und Nachname'
+ })
+ end
+
+ if licenseConfig.fields.birthday then
+ table.insert(input, {
+ type = 'date',
+ isRequired = true,
+ label = 'Geburtsdatum'
+ })
+ end
+
+ if licenseConfig.fields.gender then
+ table.insert(input, {
+ type = 'select',
+ label = 'Geschlecht',
+ options = Config.Genders
+ })
+ end
+
+ if licenseConfig.fields.issue_date then
+ table.insert(input, {
+ type = 'date',
+ isRequired = true,
+ label = 'Ausstellungsdatum',
+ default = os.date('%Y-%m-%d')
+ })
+ end
+
+ if licenseConfig.fields.expire_date then
+ table.insert(input, {
+ type = 'date',
+ isRequired = true,
+ label = 'Ablaufdatum'
+ })
+ end
+
+ if licenseConfig.fields.classes then
+ local classOptions = {}
+ for _, class in ipairs(Config.LicenseClasses) do
+ table.insert(classOptions, {
+ value = class,
+ label = 'Klasse ' .. class
+ })
+ end
+
+ table.insert(input, {
+ type = 'multi-select',
+ label = 'Führerscheinklassen',
+ options = classOptions
+ })
+ end
+
+ local dialog = lib.inputDialog('Lizenz ausstellen - ' .. licenseConfig.label, input)
+
+ if dialog then
+ local licenseData = {
+ name = dialog[1],
+ birthday = dialog[2],
+ gender = dialog[3],
+ issue_date = dialog[4] or os.date('%Y-%m-%d'),
+ expire_date = dialog[5],
+ classes = dialog[6]
+ }
+
+ TriggerServerEvent('license-system:server:issueLicense', targetId, licenseType, licenseData)
+ end
+end
+
+-- Lizenz-Overlay anzeigen
+function showLicenseOverlay(licenseData, issuerName)
+ local licenseConfig = Config.Licenses[licenseData.license_type]
+ if not licenseConfig then return end
+
+ -- NUI öffnen
+ SetNuiFocus(true, true)
+ SendNUIMessage({
+ action = 'showLicense',
+ data = {
+ license = licenseData,
+ config = licenseConfig,
+ issuer = issuerName
+ }
+ })
+end
+
+-- NUI Callbacks
+RegisterNUICallback('closeLicense', function(data, cb)
+ SetNuiFocus(false, false)
+ cb('ok')
+end)
+
+-- Lizenz verwalten (für autorisierte Jobs)
+function manageLicensesMenu()
+ -- Hier würde die Verwaltung für das Entziehen/Wiederherstellen von Lizenzen implementiert
+ -- Ähnlich wie die anderen Menüs, aber mit Suchfunktion nach Spielern
+ lib.notify({
+ title = 'Lizenz-System',
+ description = 'Lizenz-Verwaltung wird implementiert...',
+ type = 'info'
+ })
+end
diff --git a/resources/[tools]/nordi_license/config.lua b/resources/[tools]/nordi_license/config.lua
new file mode 100644
index 000000000..11834ebb5
--- /dev/null
+++ b/resources/[tools]/nordi_license/config.lua
@@ -0,0 +1,107 @@
+Config = {}
+
+-- Keybind zum Öffnen des Menüs
+Config.OpenKey = 'F6'
+
+-- Command zum Öffnen
+Config.Command = 'licenses'
+
+-- Jobs die Lizenzen ausstellen können
+Config.AuthorizedJobs = {
+ ['police'] = {
+ canIssue = {'id_card', 'drivers_license', 'weapon_license'},
+ canRevoke = {'drivers_license', 'weapon_license'},
+ canRestore = {'drivers_license', 'weapon_license'}
+ },
+ ['government'] = {
+ canIssue = {'id_card', 'drivers_license', 'passport', 'business_license'},
+ canRevoke = {'drivers_license', 'business_license'},
+ canRestore = {'drivers_license', 'business_license'}
+ },
+ ['driving_school'] = {
+ canIssue = {'drivers_license'},
+ canRevoke = {},
+ canRestore = {}
+ }
+}
+
+-- Verfügbare Lizenzen/Ausweise
+Config.Licenses = {
+ ['id_card'] = {
+ label = 'Personalausweis',
+ icon = 'fas fa-id-card',
+ fields = {
+ name = true,
+ birthday = true,
+ gender = true,
+ issue_date = true,
+ expire_date = true,
+ classes = false
+ },
+ template = 'id_card'
+ },
+ ['drivers_license'] = {
+ label = 'Führerschein',
+ icon = 'fas fa-car',
+ fields = {
+ name = true,
+ birthday = true,
+ gender = true,
+ issue_date = true,
+ expire_date = true,
+ classes = true
+ },
+ template = 'drivers_license'
+ },
+ ['weapon_license'] = {
+ label = 'Waffenschein',
+ icon = 'fas fa-gun',
+ fields = {
+ name = true,
+ birthday = true,
+ gender = true,
+ issue_date = true,
+ expire_date = true,
+ classes = false
+ },
+ template = 'weapon_license'
+ },
+ ['passport'] = {
+ label = 'Reisepass',
+ icon = 'fas fa-passport',
+ fields = {
+ name = true,
+ birthday = true,
+ gender = true,
+ issue_date = true,
+ expire_date = true,
+ classes = false
+ },
+ template = 'passport'
+ },
+ ['business_license'] = {
+ label = 'Gewerbeschein',
+ icon = 'fas fa-briefcase',
+ fields = {
+ name = true,
+ birthday = false,
+ gender = false,
+ issue_date = true,
+ expire_date = true,
+ classes = false
+ },
+ template = 'business_license'
+ }
+}
+
+-- Führerscheinklassen
+Config.LicenseClasses = {
+ 'A', 'B', 'C', 'Boot', 'Flugzeug'
+}
+
+-- Geschlechter
+Config.Genders = {
+ {value = 'male', label = 'Männlich'},
+ {value = 'female', label = 'Weiblich'},
+ {value = 'other', label = 'Divers'}
+}
diff --git a/resources/[tools]/nordi_license/fxmanifest.lua b/resources/[tools]/nordi_license/fxmanifest.lua
new file mode 100644
index 000000000..2ddcd9f06
--- /dev/null
+++ b/resources/[tools]/nordi_license/fxmanifest.lua
@@ -0,0 +1,35 @@
+fx_version 'cerulean'
+game 'gta5'
+
+author 'YourName'
+description 'License System for QBCore'
+version '1.0.0'
+
+shared_scripts {
+ '@ox_lib/init.lua',
+ 'config.lua'
+}
+
+client_scripts {
+ 'client/main.lua'
+}
+
+server_scripts {
+ '@oxmysql/lib/MySQL.lua',
+ 'server/main.lua'
+}
+
+ui_page 'html/index.html'
+
+files {
+ 'html/index.html',
+ 'html/style.css',
+ 'html/script.js',
+ 'html/images/*.png'
+}
+
+dependencies {
+ 'qb-core',
+ 'ox_lib',
+ 'oxmysql'
+}
diff --git a/resources/[tools]/nordi_license/html/index.html b/resources/[tools]/nordi_license/html/index.html
new file mode 100644
index 000000000..07e45cf8b
--- /dev/null
+++ b/resources/[tools]/nordi_license/html/index.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ License System
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name:
+
+
+
+ Geburtsdatum:
+
+
+
+ Geschlecht:
+
+
+
+ Ausgestellt:
+
+
+
+ Gültig bis:
+
+
+
+ Klassen:
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/[tools]/nordi_license/html/script.js b/resources/[tools]/nordi_license/html/script.js
new file mode 100644
index 000000000..fd6da9c7a
--- /dev/null
+++ b/resources/[tools]/nordi_license/html/script.js
@@ -0,0 +1,103 @@
+let currentLicense = null;
+
+// Event Listener für Nachrichten von FiveM
+window.addEventListener('message', function(event) {
+ const data = event.data;
+
+ switch(data.action) {
+ case 'showLicense':
+ showLicense(data.data);
+ break;
+ }
+});
+
+// Lizenz anzeigen
+function showLicense(data) {
+ currentLicense = data;
+ const container = document.getElementById('license-container');
+ const card = document.getElementById('license-card');
+
+ // Lizenztyp-spezifische Klasse hinzufügen
+ card.className = 'license-card ' + data.license.license_type;
+
+ // Header befüllen
+ document.querySelector('.license-title').textContent = data.config.label;
+ document.querySelector('.license-icon').className = 'license-icon ' + data.config.icon;
+
+ // Lizenzinformationen befüllen
+ document.getElementById('license-name').textContent = data.license.name || 'N/A';
+ document.getElementById('license-birthday').textContent = data.license.birthday || 'N/A';
+ document.getElementById('license-gender').textContent = formatGender(data.license.gender) || 'N/A';
+ document.getElementById('license-issue').textContent = data.license.issue_date || 'N/A';
+ document.getElementById('license-expire').textContent = data.license.expire_date || 'N/A';
+
+ // Klassen anzeigen (nur bei Führerschein)
+ const classesRow = document.getElementById('license-classes-row');
+ if (data.license.classes && data.license.classes !== '[]') {
+ try {
+ const classes = JSON.parse(data.license.classes);
+ if (classes && classes.length > 0) {
+ document.getElementById('license-classes').textContent = classes.join(', ');
+ classesRow.style.display = 'flex';
+ } else {
+ classesRow.style.display = 'none';
+ }
+ } catch (e) {
+ classesRow.style.display = 'none';
+ }
+ } else {
+ classesRow.style.display = 'none';
+ }
+
+ // Status anzeigen
+ const statusElement = document.getElementById('license-status');
+ if (data.license.is_active) {
+ statusElement.textContent = '✅ Gültig';
+ statusElement.className = 'license-status active';
+ } else {
+ statusElement.textContent = '❌ Ungültig';
+ statusElement.className = 'license-status inactive';
+ }
+
+ // Container anzeigen
+ container.classList.remove('hidden');
+}
+
+// Geschlecht formatieren
+function formatGender(gender) {
+ const genderMap = {
+ 'male': 'Männlich',
+ 'female': 'Weiblich',
+ 'other': 'Divers'
+ };
+ return genderMap[gender] || gender;
+}
+
+// Lizenz schließen
+function closeLicense() {
+ const container = document.getElementById('license-container');
+ container.classList.add('hidden');
+
+ // Callback an FiveM senden
+ fetch(`https://${GetParentResourceName()}/closeLicense`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ },
+ body: JSON.stringify({})
+ });
+}
+
+// ESC-Taste zum Schließen
+document.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape') {
+ closeLicense();
+ }
+});
+
+// Klick außerhalb der Karte zum Schließen
+document.getElementById('license-container').addEventListener('click', function(event) {
+ if (event.target === this) {
+ closeLicense();
+ }
+});
diff --git a/resources/[tools]/nordi_license/html/style.css b/resources/[tools]/nordi_license/html/style.css
new file mode 100644
index 000000000..cd5e99b71
--- /dev/null
+++ b/resources/[tools]/nordi_license/html/style.css
@@ -0,0 +1,220 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Arial', sans-serif;
+ background: transparent;
+ overflow: hidden;
+}
+
+.hidden {
+ display: none !important;
+}
+
+#license-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.license-card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 15px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
+ width: 450px;
+ min-height: 300px;
+ color: white;
+ position: relative;
+ overflow: hidden;
+}
+
+.license-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,');
+ opacity: 0.3;
+ pointer-events: none;
+}
+
+.license-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ border-bottom: 2px solid rgba(255, 255, 255, 0.2);
+}
+
+.license-title {
+ font-size: 24px;
+ font-weight: bold;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.license-logo {
+ font-size: 32px;
+ opacity: 0.8;
+}
+
+.license-content {
+ display: flex;
+ padding: 20px;
+ gap: 20px;
+}
+
+.license-photo {
+ width: 100px;
+ height: 120px;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 48px;
+ color: rgba(255, 255, 255, 0.7);
+ flex-shrink: 0;
+}
+
+.license-info {
+ flex: 1;
+}
+
+.info-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ padding: 8px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.info-row:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.label {
+ font-weight: bold;
+ opacity: 0.9;
+ min-width: 100px;
+}
+
+.value {
+ text-align: right;
+ opacity: 0.8;
+}
+
+.license-footer {
+ padding: 20px;
+ border-top: 2px solid rgba(255, 255, 255, 0.2);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.license-status {
+ font-weight: bold;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 14px;
+}
+
+.license-status.active {
+ background: rgba(76, 175, 80, 0.3);
+ color: #4CAF50;
+ border: 1px solid #4CAF50;
+}
+
+.license-status.inactive {
+ background: rgba(244, 67, 54, 0.3);
+ color: #F44336;
+ border: 1px solid #F44336;
+}
+
+.close-btn {
+ background: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ padding: 10px 20px;
+ border-radius: 25px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.close-btn:hover {
+ background: rgba(255, 255, 255, 0.3);
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
+}
+
+/* Spezielle Styles für verschiedene Lizenztypen */
+.license-card.id_card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.license-card.drivers_license {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.license-card.weapon_license {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.license-card.passport {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.license-card.business_license {
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
+}
+
+/* Responsive Design */
+@media (max-width: 500px) {
+ .license-card {
+ width: 90vw;
+ margin: 20px;
+ }
+
+ .license-content {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .license-photo {
+ width: 80px;
+ height: 100px;
+ font-size: 36px;
+ }
+}
+
+/* Animation für das Erscheinen */
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50px) scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.license-card {
+ animation: slideIn 0.3s ease-out;
+}
diff --git a/resources/[tools]/nordi_license/server/main.lua b/resources/[tools]/nordi_license/server/main.lua
new file mode 100644
index 000000000..216a87e07
--- /dev/null
+++ b/resources/[tools]/nordi_license/server/main.lua
@@ -0,0 +1,292 @@
+local QBCore = exports['qb-core']:GetCoreObject()
+
+-- Datenbank Setup
+MySQL.ready(function()
+ MySQL.Async.execute([[
+ CREATE TABLE IF NOT EXISTS player_licenses (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ citizenid VARCHAR(50) NOT NULL,
+ license_type VARCHAR(50) NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ birthday VARCHAR(20),
+ gender VARCHAR(20),
+ issue_date VARCHAR(20) NOT NULL,
+ expire_date VARCHAR(20),
+ classes TEXT,
+ issued_by VARCHAR(50) NOT NULL,
+ is_active BOOLEAN DEFAULT TRUE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX(citizenid),
+ INDEX(license_type)
+ )
+ ]])
+end)
+
+-- Command registrieren
+QBCore.Commands.Add(Config.Command, 'Öffne das Lizenz-Menü', {}, false, function(source, args)
+ TriggerClientEvent('license-system:client:openMenu', source)
+end)
+
+-- Events
+RegisterNetEvent('license-system:server:getLicenses', function()
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+ if not Player then return end
+
+ MySQL.Async.fetchAll('SELECT * FROM player_licenses WHERE citizenid = ?', {
+ Player.PlayerData.citizenid
+ }, function(result)
+ TriggerClientEvent('license-system:client:receiveLicenses', src, result)
+ end)
+end)
+
+RegisterNetEvent('license-system:server:getNearbyPlayers', function()
+ local src = source
+ TriggerClientEvent('license-system:client:receiveNearbyPlayers', src, QBCore.Functions.GetPlayers())
+end)
+
+RegisterNetEvent('license-system:server:showLicense', function(targetId, licenseData)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+ if not Player then return end
+
+ TriggerClientEvent('license-system:client:viewLicense', targetId, licenseData, Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname)
+end)
+
+RegisterNetEvent('license-system:server:issueLicense', function(targetId, licenseType, licenseData)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+ local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
+
+ if not Player or not TargetPlayer then return end
+
+ -- Job-Berechtigung prüfen
+ local playerJob = Player.PlayerData.job.name
+ if not Config.AuthorizedJobs[playerJob] or not hasPermission(Config.AuthorizedJobs[playerJob].canIssue, licenseType) then
+ TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Aktion!', 'error')
+ return
+ end
+
+ -- Lizenz in Datenbank speichern
+ MySQL.Async.execute('INSERT INTO player_licenses (citizenid, license_type, name, birthday, gender, issue_date, expire_date, classes, issued_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', {
+ TargetPlayer.PlayerData.citizenid,
+ licenseType,
+ licenseData.name,
+ licenseData.birthday,
+ licenseData.gender,
+ licenseData.issue_date,
+ licenseData.expire_date,
+ json.encode(licenseData.classes or {}),
+ Player.PlayerData.citizenid
+ }, function(insertId)
+ if insertId then
+ TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich ausgestellt!', 'success')
+ TriggerClientEvent('QBCore:Notify', targetId, 'Du hast eine neue Lizenz erhalten: ' .. Config.Licenses[licenseType].label, 'success')
+ else
+ TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Ausstellen der Lizenz!', 'error')
+ end
+ end)
+end)
+
+RegisterNetEvent('license-system:server:revokeLicense', function(targetId, licenseId)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+
+ if not Player then return end
+
+ -- Job-Berechtigung prüfen
+ local playerJob = Player.PlayerData.job.name
+ if not Config.AuthorizedJobs[playerJob] then
+ TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Aktion!', 'error')
+ return
+ end
+
+ MySQL.Async.execute('UPDATE player_licenses SET is_active = FALSE WHERE id = ?', {
+ licenseId
+ }, function(affectedRows)
+ if affectedRows > 0 then
+ TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich entzogen!', 'success')
+ if targetId then
+ TriggerClientEvent('QBCore:Notify', targetId, 'Eine deiner Lizenzen wurde entzogen!', 'error')
+ end
+ else
+ TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Entziehen der Lizenz!', 'error')
+ end
+ end)
+end)
+
+RegisterNetEvent('license-system:server:restoreLicense', function(targetId, licenseId)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+
+ if not Player then return end
+
+ -- Job-Berechtigung prüfen
+ local playerJob = Player.PlayerData.job.name
+ if not Config.AuthorizedJobs[playerJob] then
+ TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Aktion!', 'error')
+ return
+ end
+
+ MySQL.Async.execute('UPDATE player_licenses SET is_active = TRUE WHERE id = ?', {
+ licenseId
+ }, function(affectedRows)
+ if affectedRows > 0 then
+ TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich wiederhergestellt!', 'success')
+ if targetId then
+ TriggerClientEvent('QBCore:Notify', targetId, 'Eine deiner Lizenzen wurde wiederhergestellt!', 'success')
+ end
+ else
+ TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Wiederherstellen der Lizenz!', 'error')
+ end
+ end)
+end)
+
+-- Hilfsfunktionen
+function hasPermission(permissions, licenseType)
+ for _, permission in ipairs(permissions) do
+ if permission == licenseType then
+ return true
+ end
+ end
+ return false
+end
+
+-- Erweiterte Funktionen für Lizenzverwaltung
+
+RegisterNetEvent('license-system:server:searchPlayer', function(searchTerm)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+
+ if not Player then return end
+
+ -- Job-Berechtigung prüfen
+ local playerJob = Player.PlayerData.job.name
+ if not Config.AuthorizedJobs[playerJob] then
+ TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Aktion!', 'error')
+ return
+ end
+
+ -- Spieler suchen
+ MySQL.Async.fetchAll('SELECT citizenid, charinfo FROM players WHERE JSON_EXTRACT(charinfo, "$.firstname") LIKE ? OR JSON_EXTRACT(charinfo, "$.lastname") LIKE ? LIMIT 10', {
+ '%' .. searchTerm .. '%',
+ '%' .. searchTerm .. '%'
+ }, function(result)
+ local players = {}
+ for _, player in ipairs(result) do
+ local charinfo = json.decode(player.charinfo)
+ table.insert(players, {
+ citizenid = player.citizenid,
+ name = charinfo.firstname .. ' ' .. charinfo.lastname
+ })
+ end
+ TriggerClientEvent('license-system:client:receiveSearchResults', src, players)
+ end)
+end)
+
+RegisterNetEvent('license-system:server:getPlayerLicenses', function(citizenid)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+
+ if not Player then return end
+
+ -- Job-Berechtigung prüfen
+ local playerJob = Player.PlayerData.job.name
+ if not Config.AuthorizedJobs[playerJob] then
+ TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Aktion!', 'error')
+ return
+ end
+
+ MySQL.Async.fetchAll('SELECT * FROM player_licenses WHERE citizenid = ? ORDER BY created_at DESC', {
+ citizenid
+ }, function(result)
+ TriggerClientEvent('license-system:client:receivePlayerLicenses', src, result, citizenid)
+ end)
+end)
+
+-- Lizenz-Historie hinzufügen
+function addLicenseHistory(licenseId, action, performedBy, performedByName, reason)
+ MySQL.Async.execute('INSERT INTO license_history (license_id, action, performed_by, performed_by_name, reason) VALUES (?, ?, ?, ?, ?)', {
+ licenseId,
+ action,
+ performedBy,
+ performedByName,
+ reason or ''
+ })
+end
+
+-- Erweiterte Revoke-Funktion mit Grund
+RegisterNetEvent('license-system:server:revokeLicenseWithReason', function(licenseId, reason)
+ local src = source
+ local Player = QBCore.Functions.GetPlayer(src)
+
+ if not Player then return end
+
+ local playerJob = Player.PlayerData.job.name
+ if not Config.AuthorizedJobs[playerJob] then
+ TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Aktion!', 'error')
+ return
+ end
+
+ local playerName = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname
+
+ MySQL.Async.execute('UPDATE player_licenses SET is_active = FALSE, revoked_by = ?, revoked_by_name = ?, revoked_date = NOW(), revoked_reason = ? WHERE id = ?', {
+ Player.PlayerData.citizenid,
+ playerName,
+ reason,
+ licenseId
+ }, function(affectedRows)
+ if affectedRows > 0 then
+ addLicenseHistory(licenseId, 'revoked', Player.PlayerData.citizenid, playerName, reason)
+ TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich entzogen!', 'success')
+ else
+ TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Entziehen der Lizenz!', 'error')
+ end
+ end)
+end)
+
+-- Automatische Lizenz-Überprüfung (läuft alle 24 Stunden)
+CreateThread(function()
+ while true do
+ Wait(24 * 60 * 60 * 1000) -- 24 Stunden
+
+ -- Abgelaufene Lizenzen deaktivieren
+ MySQL.Async.execute('UPDATE player_licenses SET is_active = FALSE WHERE expire_date < CURDATE() AND is_active = TRUE', {}, function(affectedRows)
+ if affectedRows > 0 then
+ print('^3[License-System]^7 ' .. affectedRows .. ' abgelaufene Lizenzen wurden deaktiviert.')
+ end
+ end)
+ end
+end)
+
+-- Export-Funktionen für andere Ressourcen
+exports('hasLicense', function(citizenid, licenseType)
+ local result = MySQL.Sync.fetchScalar('SELECT COUNT(*) FROM player_licenses WHERE citizenid = ? AND license_type = ? AND is_active = TRUE', {
+ citizenid, licenseType
+ })
+ return result > 0
+end)
+
+exports('getLicenses', function(citizenid)
+ return MySQL.Sync.fetchAll('SELECT * FROM player_licenses WHERE citizenid = ? AND is_active = TRUE', {
+ citizenid
+ })
+end)
+
+exports('hasLicenseClass', function(citizenid, class)
+ local result = MySQL.Sync.fetchAll('SELECT classes FROM player_licenses WHERE citizenid = ? AND license_type = "drivers_license" AND is_active = TRUE', {
+ citizenid
+ })
+
+ for _, license in ipairs(result) do
+ if license.classes then
+ local classes = json.decode(license.classes)
+ for _, licenseClass in ipairs(classes) do
+ if licenseClass == class then
+ return true
+ end
+ end
+ end
+ end
+ return false
+end)