forked from Simnation/Main
ed
This commit is contained in:
parent
4f8d916728
commit
4d24104e50
6 changed files with 3089 additions and 2383 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,347 +1,317 @@
|
||||||
Config = {}
|
Config = {}
|
||||||
|
|
||||||
-- Debug-Modus
|
-- Allgemeine Einstellungen
|
||||||
Config.Debug = true
|
Config.Debug = true
|
||||||
|
Config.UseBackgroundImages = true
|
||||||
|
Config.MaxLicenseAge = 365 -- Tage bis Ablauf
|
||||||
|
Config.RenewalDays = 30 -- Tage vor Ablauf für Verlängerung
|
||||||
|
|
||||||
-- Berechtigte Jobs
|
-- Berechtigte Jobs
|
||||||
Config.AuthorizedJobs = {
|
Config.AuthorizedJobs = {
|
||||||
['police'] = true,
|
['police'] = true,
|
||||||
['sheriff'] = true,
|
['sheriff'] = true,
|
||||||
['government'] = true,
|
['government'] = true,
|
||||||
['doj'] = true,
|
['judge'] = true,
|
||||||
['ambulance'] = true,
|
['lawyer'] = true,
|
||||||
['mechanic'] = true
|
['ambulance'] = true, -- Für medizinische Lizenzen
|
||||||
|
['mechanic'] = true -- Für Fahrzeug-Lizenzen
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Benachrichtigungen
|
-- Jobs that can reactivate specific license types
|
||||||
Config.Notifications = {
|
Config.ReactivationPermissions = {
|
||||||
no_permission = {
|
['police'] = {'weapon_license', 'drivers_license'},
|
||||||
message = "Du hast keine Berechtigung dafür!",
|
['admin'] = {'id_card', 'passport', 'business_license'},
|
||||||
type = "error"
|
['ambulance'] = {'medical_license'},
|
||||||
},
|
['driving_school'] = {'drivers_license'},
|
||||||
license_issued = {
|
['harbor'] = {'boat_license'},
|
||||||
message = "Lizenz erfolgreich ausgestellt!",
|
['airport'] = {'pilot_license'}
|
||||||
type = "success"
|
|
||||||
},
|
|
||||||
license_revoked = {
|
|
||||||
message = "Lizenz erfolgreich entzogen!",
|
|
||||||
type = "success"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Lizenz-Typen (ERWEITERT mit benutzerdefinierten Feldern)
|
|
||||||
|
-- Lizenz-Typen
|
||||||
Config.LicenseTypes = {
|
Config.LicenseTypes = {
|
||||||
['id_card'] = {
|
['id_card'] = {
|
||||||
label = 'Personalausweis',
|
label = 'Personalausweis',
|
||||||
description = 'Offizieller Personalausweis',
|
|
||||||
price = 50,
|
|
||||||
validity_days = nil, -- Unbegrenzt gültig
|
|
||||||
color = '#2E86AB',
|
|
||||||
icon = 'fas fa-id-card',
|
icon = 'fas fa-id-card',
|
||||||
template = 'id_card',
|
color = '#667eea',
|
||||||
custom_fields = {
|
price = 50,
|
||||||
{
|
required_items = {},
|
||||||
name = 'birth_date',
|
can_expire = true,
|
||||||
label = 'Geburtsdatum',
|
validity_days = 3650, -- 10 Jahre
|
||||||
type = 'date',
|
required_job = nil,
|
||||||
required = true,
|
description = 'Offizieller Personalausweis'
|
||||||
placeholder = 'TT.MM.JJJJ'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'birth_place',
|
|
||||||
label = 'Geburtsort',
|
|
||||||
type = 'text',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'z.B. Los Santos'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'nationality',
|
|
||||||
label = 'Staatsangehörigkeit',
|
|
||||||
type = 'select',
|
|
||||||
required = true,
|
|
||||||
options = {
|
|
||||||
{value = 'usa', label = 'USA'},
|
|
||||||
{value = 'germany', label = 'Deutschland'},
|
|
||||||
{value = 'uk', label = 'Vereinigtes Königreich'},
|
|
||||||
{value = 'france', label = 'Frankreich'},
|
|
||||||
{value = 'other', label = 'Andere'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'address',
|
|
||||||
label = 'Adresse',
|
|
||||||
type = 'textarea',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'Vollständige Adresse'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'height',
|
|
||||||
label = 'Größe (cm)',
|
|
||||||
type = 'number',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'z.B. 180'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'eye_color',
|
|
||||||
label = 'Augenfarbe',
|
|
||||||
type = 'select',
|
|
||||||
required = false,
|
|
||||||
options = {
|
|
||||||
{value = 'brown', label = 'Braun'},
|
|
||||||
{value = 'blue', label = 'Blau'},
|
|
||||||
{value = 'green', label = 'Grün'},
|
|
||||||
{value = 'gray', label = 'Grau'},
|
|
||||||
{value = 'hazel', label = 'Haselnuss'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'photo_url',
|
|
||||||
label = 'Foto-URL',
|
|
||||||
type = 'url',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'https://example.com/photo.jpg'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
['driver_license'] = {
|
['drivers_license'] = {
|
||||||
label = 'Führerschein',
|
label = 'Führerschein',
|
||||||
description = 'Führerschein für Kraftfahrzeuge',
|
|
||||||
price = 150,
|
|
||||||
validity_days = 1825, -- 5 Jahre
|
|
||||||
color = '#F18F01',
|
|
||||||
icon = 'fas fa-car',
|
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 = {
|
classes = {
|
||||||
['A'] = 'Motorräder',
|
'A', 'A1', 'A2', 'B', 'BE', 'C', 'CE', 'D', 'DE'
|
||||||
['B'] = 'PKW',
|
|
||||||
['C'] = 'LKW',
|
|
||||||
['D'] = 'Busse'
|
|
||||||
},
|
|
||||||
custom_fields = {
|
|
||||||
{
|
|
||||||
name = 'birth_date',
|
|
||||||
label = 'Geburtsdatum',
|
|
||||||
type = 'date',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'TT.MM.JJJJ'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'address',
|
|
||||||
label = 'Adresse',
|
|
||||||
type = 'textarea',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'Vollständige Adresse'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'restrictions',
|
|
||||||
label = 'Beschränkungen',
|
|
||||||
type = 'textarea',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'z.B. Brille erforderlich'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'photo_url',
|
|
||||||
label = 'Foto-URL',
|
|
||||||
type = 'url',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'https://example.com/photo.jpg'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
['weapon_license'] = {
|
['weapon_license'] = {
|
||||||
label = 'Waffenschein',
|
label = 'Waffenschein',
|
||||||
description = 'Berechtigung zum Führen von Waffen',
|
|
||||||
price = 500,
|
|
||||||
validity_days = 365, -- 1 Jahr
|
|
||||||
color = '#C73E1D',
|
|
||||||
icon = 'fas fa-crosshairs',
|
icon = 'fas fa-crosshairs',
|
||||||
template = 'weapon_license',
|
color = '#4facfe',
|
||||||
custom_fields = {
|
price = 2500,
|
||||||
{
|
required_items = {'weapon_course_certificate', 'psychological_evaluation'},
|
||||||
name = 'birth_date',
|
can_expire = true,
|
||||||
label = 'Geburtsdatum',
|
validity_days = 1095, -- 3 Jahre
|
||||||
type = 'date',
|
required_job = 'police',
|
||||||
required = true,
|
description = 'Berechtigung zum Führen von Schusswaffen',
|
||||||
placeholder = 'TT.MM.JJJJ'
|
restrictions = {
|
||||||
},
|
'Nur für registrierte Waffen',
|
||||||
{
|
'Regelmäßige Überprüfung erforderlich',
|
||||||
name = 'weapon_type',
|
'Nicht übertragbar'
|
||||||
label = 'Waffentyp',
|
|
||||||
type = 'select',
|
|
||||||
required = true,
|
|
||||||
options = {
|
|
||||||
{value = 'pistol', label = 'Pistole'},
|
|
||||||
{value = 'rifle', label = 'Gewehr'},
|
|
||||||
{value = 'shotgun', label = 'Schrotflinte'},
|
|
||||||
{value = 'all', label = 'Alle Waffentypen'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'purpose',
|
|
||||||
label = 'Verwendungszweck',
|
|
||||||
type = 'select',
|
|
||||||
required = true,
|
|
||||||
options = {
|
|
||||||
{value = 'self_defense', label = 'Selbstverteidigung'},
|
|
||||||
{value = 'sport', label = 'Sport'},
|
|
||||||
{value = 'hunting', label = 'Jagd'},
|
|
||||||
{value = 'collection', label = 'Sammlung'},
|
|
||||||
{value = 'security', label = 'Sicherheitsdienst'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'restrictions',
|
|
||||||
label = 'Beschränkungen',
|
|
||||||
type = 'textarea',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'Besondere Auflagen oder Beschränkungen'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'photo_url',
|
|
||||||
label = 'Foto-URL',
|
|
||||||
type = 'url',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'https://example.com/photo.jpg'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
['pilot_license'] = {
|
['passport'] = {
|
||||||
label = 'Pilotenlizenz',
|
label = 'Reisepass',
|
||||||
description = 'Berechtigung zum Führen von Luftfahrzeugen',
|
icon = 'fas fa-passport',
|
||||||
price = 1000,
|
color = '#43e97b',
|
||||||
validity_days = 730, -- 2 Jahre
|
price = 150,
|
||||||
color = '#6A994E',
|
required_items = {'birth_certificate', 'id_card'},
|
||||||
icon = 'fas fa-plane',
|
can_expire = true,
|
||||||
template = 'pilot_license',
|
validity_days = 3650, -- 10 Jahre
|
||||||
classes = {
|
required_job = 'government',
|
||||||
['PPL'] = 'Private Pilot License',
|
description = 'Internationales Reisedokument'
|
||||||
['CPL'] = 'Commercial Pilot License',
|
|
||||||
['ATPL'] = 'Airline Transport Pilot License',
|
|
||||||
['HELI'] = 'Helicopter License'
|
|
||||||
},
|
|
||||||
custom_fields = {
|
|
||||||
{
|
|
||||||
name = 'birth_date',
|
|
||||||
label = 'Geburtsdatum',
|
|
||||||
type = 'date',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'TT.MM.JJJJ'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'medical_cert',
|
|
||||||
label = 'Medical Certificate',
|
|
||||||
type = 'text',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'z.B. Class 1, Class 2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'flight_hours',
|
|
||||||
label = 'Flugstunden',
|
|
||||||
type = 'number',
|
|
||||||
required = true,
|
|
||||||
placeholder = 'Gesamte Flugstunden'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'aircraft_types',
|
|
||||||
label = 'Luftfahrzeugtypen',
|
|
||||||
type = 'textarea',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'Berechtigte Luftfahrzeugtypen'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'restrictions',
|
|
||||||
label = 'Beschränkungen',
|
|
||||||
type = 'textarea',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'z.B. nur bei Tageslicht'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = 'photo_url',
|
|
||||||
label = 'Foto-URL',
|
|
||||||
type = 'url',
|
|
||||||
required = false,
|
|
||||||
placeholder = 'https://example.com/photo.jpg'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
['business_license'] = {
|
['business_license'] = {
|
||||||
label = 'Gewerbeschein',
|
label = 'Gewerbeschein',
|
||||||
description = 'Berechtigung zur Ausübung eines Gewerbes',
|
|
||||||
price = 300,
|
|
||||||
validity_days = 365, -- 1 Jahr
|
|
||||||
color = '#7209B7',
|
|
||||||
icon = 'fas fa-briefcase',
|
icon = 'fas fa-briefcase',
|
||||||
template = 'business_license',
|
color = '#fa709a',
|
||||||
custom_fields = {
|
price = 1000,
|
||||||
{
|
required_items = {'business_plan', 'tax_certificate'},
|
||||||
name = 'business_name',
|
can_expire = true,
|
||||||
label = 'Firmenname',
|
validity_days = 1825, -- 5 Jahre
|
||||||
type = 'text',
|
required_job = 'government',
|
||||||
required = true,
|
description = 'Berechtigung zur Ausübung eines Gewerbes'
|
||||||
placeholder = 'Name des Unternehmens'
|
},
|
||||||
},
|
['pilot_license'] = {
|
||||||
{
|
label = 'Pilotenlizenz',
|
||||||
name = 'business_type',
|
icon = 'fas fa-plane',
|
||||||
label = 'Gewerbetyp',
|
color = '#667eea',
|
||||||
type = 'select',
|
price = 5000,
|
||||||
required = true,
|
required_items = {'flight_hours_log', 'medical_certificate'},
|
||||||
options = {
|
can_expire = true,
|
||||||
{value = 'retail', label = 'Einzelhandel'},
|
validity_days = 730, -- 2 Jahre
|
||||||
{value = 'restaurant', label = 'Gastronomie'},
|
required_job = 'airport',
|
||||||
{value = 'service', label = 'Dienstleistung'},
|
description = 'Berechtigung zum Führen von Luftfahrzeugen'
|
||||||
{value = 'manufacturing', label = 'Herstellung'},
|
},
|
||||||
{value = 'transport', label = 'Transport'},
|
['boat_license'] = {
|
||||||
{value = 'other', label = 'Sonstiges'}
|
label = 'Bootsführerschein',
|
||||||
}
|
icon = 'fas fa-ship',
|
||||||
},
|
color = '#00f2fe',
|
||||||
{
|
price = 800,
|
||||||
name = 'business_address',
|
required_items = {'boat_course_certificate'},
|
||||||
label = 'Geschäftsadresse',
|
can_expire = true,
|
||||||
type = 'textarea',
|
validity_days = 1825, -- 5 Jahre
|
||||||
required = true,
|
required_job = 'harbor',
|
||||||
placeholder = 'Vollständige Geschäftsadresse'
|
description = 'Berechtigung zum Führen von Wasserfahrzeugen'
|
||||||
},
|
},
|
||||||
{
|
['medical_license'] = {
|
||||||
name = 'tax_number',
|
label = 'Approbation',
|
||||||
label = 'Steuernummer',
|
icon = 'fas fa-user-md',
|
||||||
type = 'text',
|
color = '#ff6b6b',
|
||||||
required = false,
|
price = 0, -- Kostenlos für Ärzte
|
||||||
placeholder = 'z.B. 123/456/78901'
|
required_items = {'medical_degree', 'medical_exam'},
|
||||||
},
|
can_expire = false,
|
||||||
{
|
validity_days = nil,
|
||||||
name = 'employees',
|
required_job = 'ambulance',
|
||||||
label = 'Anzahl Mitarbeiter',
|
description = 'Berechtigung zur Ausübung der Heilkunde'
|
||||||
type = 'number',
|
},
|
||||||
required = false,
|
['hunting_license'] = {
|
||||||
placeholder = 'Geplante Mitarbeiterzahl'
|
label = 'Jagdschein',
|
||||||
},
|
icon = 'fas fa-crosshairs',
|
||||||
{
|
color = '#8b5a3c',
|
||||||
name = 'logo_url',
|
price = 300,
|
||||||
label = 'Firmenlogo-URL',
|
required_items = {'hunting_course_certificate'},
|
||||||
type = 'url',
|
can_expire = true,
|
||||||
required = false,
|
validity_days = 1095, -- 3 Jahre
|
||||||
placeholder = 'https://example.com/logo.jpg'
|
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
|
-- Kommandos
|
||||||
Config.UI = {
|
Config.Commands = {
|
||||||
position = 'center', -- 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right'
|
['license'] = {
|
||||||
animation = 'fade', -- 'fade', 'slide', 'zoom'
|
name = 'lizenz',
|
||||||
theme = 'dark', -- 'dark', 'light'
|
help = 'Lizenz-System öffnen',
|
||||||
blur_background = true,
|
restricted = true -- Nur für berechtigte Jobs
|
||||||
max_image_size = 5 * 1024 * 1024, -- 5MB
|
},
|
||||||
allowed_image_formats = {'jpg', 'jpeg', 'png', 'gif', 'webp'},
|
['mylicense'] = {
|
||||||
default_avatar = 'https://via.placeholder.com/150x200/cccccc/666666?text=Kein+Foto'
|
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
|
-- Keybinds
|
||||||
Config.Validation = {
|
Config.Keybinds = {
|
||||||
name_min_length = 2,
|
['open_license_menu'] = {
|
||||||
name_max_length = 50,
|
key = 'F6',
|
||||||
url_pattern = '^https?://.+',
|
command = 'lizenz',
|
||||||
date_pattern = '^%d%d%.%d%d%.%d%d%d%d$', -- DD.MM.YYYY
|
description = 'Lizenz-System öffnen'
|
||||||
phone_pattern = '^%+?[%d%s%-%(%)]+$'
|
},
|
||||||
|
['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
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,190 +5,270 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>License System</title>
|
<title>License System</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Hauptmenü -->
|
<!-- Hauptcontainer für Lizenz-Anzeige -->
|
||||||
<div id="mainMenu" class="menu-container" style="display: none;">
|
<div id="license-container" class="hidden">
|
||||||
<div class="menu-content">
|
<div class="license-overlay" onclick="closeLicense()"></div>
|
||||||
<div class="menu-header">
|
|
||||||
<h2><i class="fas fa-id-card"></i> Lizenz-System</h2>
|
<div class="license-card" id="license-card">
|
||||||
<button class="close-btn" onclick="closeMenu()">
|
<!-- Sicherheitselemente -->
|
||||||
<i class="fas fa-times"></i>
|
<div class="security-strip"></div>
|
||||||
</button>
|
<div class="hologram"></div>
|
||||||
|
<div class="microtext">SECURE DOCUMENT • OFFICIAL USE ONLY • GOVERNMENT ISSUED</div>
|
||||||
|
|
||||||
|
<!-- Header der Lizenz -->
|
||||||
|
<div class="license-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="license-title" id="license-title">Personalausweis</div>
|
||||||
|
<div class="license-subtitle">Bundesrepublik Deutschland</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="license-logo">
|
||||||
|
<i class="license-icon" id="license-icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="government-seal">
|
||||||
|
<i class="fas fa-certificate"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-body">
|
<!-- Hauptinhalt der Lizenz -->
|
||||||
<div class="menu-section">
|
<div class="license-content">
|
||||||
<h3>Eigene Aktionen</h3>
|
<!-- Foto-Bereich -->
|
||||||
<div class="button-grid">
|
<div class="license-photo-section">
|
||||||
<button class="menu-btn" onclick="requestMyLicense('id_card')">
|
<div class="license-photo" id="license-photo">
|
||||||
<i class="fas fa-id-card"></i>
|
<img id="player-photo" class="hidden" alt="Spieler Foto">
|
||||||
<span>Eigenen Ausweis zeigen</span>
|
<div id="photo-placeholder" class="photo-placeholder">
|
||||||
</button>
|
<i class="fas fa-user"></i>
|
||||||
<button class="menu-btn" onclick="requestMyLicense('driver_license')">
|
</div>
|
||||||
<i class="fas fa-car"></i>
|
</div>
|
||||||
<span>Eigenen Führerschein zeigen</span>
|
<div class="photo-frame"></div>
|
||||||
</button>
|
</div>
|
||||||
<button class="menu-btn" onclick="requestMyLicense('weapon_license')">
|
|
||||||
<i class="fas fa-crosshairs"></i>
|
<!-- Informations-Bereich -->
|
||||||
<span>Eigenen Waffenschein zeigen</span>
|
<div class="license-info">
|
||||||
</button>
|
<div class="info-section personal-info">
|
||||||
<button class="menu-btn" onclick="requestMyLicense('pilot_license')">
|
<h3 class="section-title">Persönliche Daten</h3>
|
||||||
<i class="fas fa-plane"></i>
|
|
||||||
<span>Eigene Pilotenlizenz zeigen</span>
|
<div class="info-row">
|
||||||
</button>
|
<span class="label">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
Name:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-name">Max Mustermann</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-birthday-cake"></i>
|
||||||
|
Geburtsdatum:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-birthday">01.01.1990</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-venus-mars"></i>
|
||||||
|
Geschlecht:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-gender">Männlich</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section document-info">
|
||||||
|
<h3 class="section-title">Dokument-Informationen</h3>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-calendar-plus"></i>
|
||||||
|
Ausgestellt:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-issue">01.01.2024</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-calendar-times"></i>
|
||||||
|
Gültig bis:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-expire">01.01.2034</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-hashtag"></i>
|
||||||
|
Dokument-ID:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-id">#000001</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row" id="license-classes-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
Klassen:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-classes">A, B, C</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">
|
||||||
|
<i class="fas fa-user-tie"></i>
|
||||||
|
Ausgestellt von:
|
||||||
|
</span>
|
||||||
|
<span class="value" id="license-issuer">Behörde</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer der Lizenz -->
|
||||||
|
<div class="license-footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
<div class="license-status" id="license-status">
|
||||||
|
<i class="status-icon"></i>
|
||||||
|
<span class="status-text">Gültig</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="validity-indicator" id="validity-indicator">
|
||||||
|
<div class="validity-bar">
|
||||||
|
<div class="validity-fill" id="validity-fill"></div>
|
||||||
|
</div>
|
||||||
|
<span class="validity-text" id="validity-text">Noch 365 Tage gültig</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-section" id="targetSection" style="display: none;">
|
<div class="footer-center">
|
||||||
<h3>Aktionen für nahestehende Person</h3>
|
<div class="qr-code" id="qr-code">
|
||||||
<div class="target-info">
|
<div class="qr-pattern">
|
||||||
<p>Entfernung: <span id="targetDistance">-</span>m</p>
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
<div class="qr-dot"></div>
|
||||||
|
</div>
|
||||||
|
<span class="qr-label">QR</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-grid">
|
</div>
|
||||||
<button class="menu-btn" onclick="requestLicense()">
|
|
||||||
<i class="fas fa-search"></i>
|
<div class="footer-right">
|
||||||
<span>Lizenz anzeigen lassen</span>
|
<button class="action-btn view-btn" onclick="flipCard()" title="Rückseite anzeigen">
|
||||||
</button>
|
<i class="fas fa-sync-alt"></i>
|
||||||
<button class="menu-btn" onclick="requestPlayerLicenses()">
|
<span>Drehen</span>
|
||||||
<i class="fas fa-list"></i>
|
</button>
|
||||||
<span>Alle Lizenzen anzeigen</span>
|
|
||||||
</button>
|
<button class="action-btn close-btn" onclick="closeLicense()" title="Schließen">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
<span>Schließen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rückseite der Lizenz (für Führerschein-Klassen etc.) -->
|
||||||
|
<div class="license-back hidden" id="license-back">
|
||||||
|
<div class="back-header">
|
||||||
|
<h3>Zusätzliche Informationen</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="back-content">
|
||||||
|
<div class="classes-grid" id="classes-grid">
|
||||||
|
<!-- Wird dynamisch befüllt -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="license-creation">
|
<div class="restrictions" id="restrictions">
|
||||||
<h4>Lizenz ausstellen</h4>
|
<h4>Beschränkungen:</h4>
|
||||||
<div class="button-grid" id="licenseButtons">
|
<ul id="restrictions-list">
|
||||||
<!-- Wird dynamisch gefüllt -->
|
<!-- Wird dynamisch befüllt -->
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="notes" id="notes">
|
||||||
|
<h4>Bemerkungen:</h4>
|
||||||
|
<p id="notes-text">Keine besonderen Bemerkungen</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="back-footer">
|
||||||
|
<button class="action-btn" onclick="flipCard()">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Erweiterte Lizenz-Erstellung -->
|
<!-- Kamera-Interface für Foto-Aufnahme -->
|
||||||
<div id="customLicenseForm" class="menu-container" style="display: none;">
|
<div id="camera-container" class="hidden">
|
||||||
<div class="menu-content large">
|
<div class="camera-overlay" onclick="closeCamera()"></div>
|
||||||
<div class="menu-header">
|
|
||||||
<h2><i class="fas fa-plus-circle"></i> <span id="formTitle">Lizenz erstellen</span></h2>
|
<div class="camera-interface">
|
||||||
<button class="close-btn" onclick="closeCustomForm()">
|
<div class="camera-header">
|
||||||
|
<h3>Foto für Lizenz aufnehmen</h3>
|
||||||
|
<button class="camera-close" onclick="closeCamera()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-body">
|
<div class="camera-content">
|
||||||
<form id="licenseForm" class="custom-form">
|
<div class="camera-preview">
|
||||||
<div class="form-grid">
|
<video id="camera-video" autoplay playsinline></video>
|
||||||
<!-- Basis-Informationen -->
|
<canvas id="camera-canvas" class="hidden"></canvas>
|
||||||
<div class="form-section">
|
|
||||||
<h3>Basis-Informationen</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="holderName">Name des Inhabers</label>
|
|
||||||
<input type="text" id="holderName" name="holderName" readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Benutzerdefinierte Felder -->
|
<div class="camera-overlay-guide">
|
||||||
<div class="form-section">
|
<div class="face-guide">
|
||||||
<h3>Zusätzliche Informationen</h3>
|
<div class="guide-circle"></div>
|
||||||
<div id="customFields">
|
<p>Gesicht hier positionieren</p>
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Klassen (falls vorhanden) -->
|
|
||||||
<div class="form-section" id="classesSection" style="display: none;">
|
|
||||||
<h3>Klassen/Kategorien</h3>
|
|
||||||
<div id="classesContainer">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Vorschau -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h3>Vorschau</h3>
|
|
||||||
<div class="license-
|
|
||||||
<div class="license-preview" id="licensePreview">
|
|
||||||
<div class="preview-header">
|
|
||||||
<div class="preview-photo">
|
|
||||||
<img id="previewPhoto" src="" alt="Foto" onerror="this.src='https://via.placeholder.com/150x200/cccccc/666666?text=Kein+Foto'">
|
|
||||||
</div>
|
|
||||||
<div class="preview-info">
|
|
||||||
<h4 id="previewName">-</h4>
|
|
||||||
<p id="previewType">-</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="preview-details" id="previewDetails">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="camera-controls">
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeCustomForm()">
|
<button class="camera-btn capture-btn" onclick="takePhoto()">
|
||||||
<i class="fas fa-times"></i> Abbrechen
|
<i class="fas fa-camera"></i>
|
||||||
</button>
|
Foto aufnehmen
|
||||||
<button type="submit" class="btn btn-primary">
|
</button>
|
||||||
<i class="fas fa-check"></i> Lizenz ausstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lizenz-Anzeige -->
|
<button class="camera-btn cancel-btn" onclick="closeCamera()">
|
||||||
<div id="licenseDisplay" class="menu-container" style="display: none;">
|
<i class="fas fa-ban"></i>
|
||||||
<div class="menu-content">
|
Abbrechen
|
||||||
<div class="menu-header">
|
</button>
|
||||||
<h2><i class="fas fa-id-card"></i> <span id="licenseTitle">Lizenz</span></h2>
|
|
||||||
<button class="close-btn" onclick="closeLicenseDisplay()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="menu-body">
|
|
||||||
<div class="license-card" id="licenseCard">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spieler-Lizenzen Übersicht -->
|
<!-- Loading-Overlay -->
|
||||||
<div id="playerLicensesDisplay" class="menu-container" style="display: none;">
|
<div id="loading-overlay" class="hidden">
|
||||||
<div class="menu-content large">
|
|
||||||
<div class="menu-header">
|
|
||||||
<h2><i class="fas fa-list"></i> Lizenzen von <span id="playerName">-</span></h2>
|
|
||||||
<button class="close-btn" onclick="closePlayerLicenses()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="menu-body">
|
|
||||||
<div class="licenses-grid" id="licensesGrid">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="no-licenses" id="noLicenses" style="display: none;">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<p>Keine Lizenzen gefunden</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
|
||||||
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
|
|
||||||
<div class="loading-spinner">
|
<div class="loading-spinner">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<div class="spinner"></div>
|
||||||
<p>Lade...</p>
|
<p>Lizenz wird geladen...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Notification-System -->
|
||||||
|
<div id="notification-container">
|
||||||
|
<!-- Notifications werden hier dynamisch eingefügt -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio für Sound-Effekte -->
|
||||||
|
<audio id="card-flip-sound" preload="auto">
|
||||||
|
<source src="sounds/card_flip.mp3" type="audio/mpeg">
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
<audio id="camera-shutter-sound" preload="auto">
|
||||||
|
<source src="sounds/camera_shutter.mp3" type="audio/mpeg">
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
<!-- JavaScript -->
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -984,191 +984,379 @@ function table.count(t)
|
||||||
return count
|
return count
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Neuer Event-Handler für benutzerdefinierte Lizenzen
|
-- Add to server/main.lua
|
||||||
RegisterNetEvent('license-system:server:issueCustomLicense', function(targetId, licenseType, customData, classes)
|
RegisterNetEvent('license-system:server:reactivateLicense', function(targetId, licenseType)
|
||||||
local src = source
|
local src = source
|
||||||
local Player = QBCore.Functions.GetPlayer(src)
|
debugPrint("=== Event: reactivateLicense ===")
|
||||||
local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
|
debugPrint("Source: " .. src .. ", Target: " .. targetId .. ", Type: " .. licenseType)
|
||||||
|
|
||||||
if not Player or not TargetPlayer then
|
-- Check if player has permission to reactivate this license type
|
||||||
debugPrint("Spieler nicht gefunden: " .. src .. " -> " .. targetId)
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
if not Player then return end
|
||||||
|
|
||||||
|
local job = Player.PlayerData.job.name
|
||||||
|
local canReactivate = false
|
||||||
|
|
||||||
|
-- Check if job can reactivate this license type
|
||||||
|
if Config.ReactivationPermissions and Config.ReactivationPermissions[job] then
|
||||||
|
for _, allowedType in ipairs(Config.ReactivationPermissions[job]) do
|
||||||
|
if allowedType == licenseType then
|
||||||
|
canReactivate = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not canReactivate then
|
||||||
|
TriggerClientEvent('QBCore:Notify', src, 'Du darfst diesen Lizenztyp nicht reaktivieren!', 'error')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Berechtigung prüfen
|
-- Get target player
|
||||||
if not isAuthorized(Player.PlayerData.job.name) then
|
local targetPlayer = QBCore.Functions.GetPlayer(targetId)
|
||||||
|
if not targetPlayer then
|
||||||
|
TriggerClientEvent('QBCore:Notify', src, 'Spieler nicht gefunden!', 'error')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local targetCitizenId = targetPlayer.PlayerData.citizenid
|
||||||
|
|
||||||
|
-- Find the most recent license of this type (even if inactive)
|
||||||
|
local query = [[
|
||||||
|
SELECT * FROM player_licenses
|
||||||
|
WHERE citizenid = ? AND license_type = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
]]
|
||||||
|
|
||||||
|
local result = MySQL.query.await(query, {targetCitizenId, licenseType})
|
||||||
|
|
||||||
|
if not result or #result == 0 then
|
||||||
|
TriggerClientEvent('QBCore:Notify', src, 'Keine Lizenz dieses Typs gefunden!', 'error')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reactivate the license
|
||||||
|
local updateQuery = "UPDATE player_licenses SET is_active = 1 WHERE id = ?"
|
||||||
|
local success = MySQL.update.await(updateQuery, {result[1].id})
|
||||||
|
|
||||||
|
if success then
|
||||||
|
-- Invalidate cache
|
||||||
|
invalidateCache(targetCitizenId, licenseType)
|
||||||
|
|
||||||
|
local targetName = getPlayerName(targetId)
|
||||||
|
local issuerName = getPlayerName(src)
|
||||||
|
local config = Config.LicenseTypes[licenseType]
|
||||||
|
|
||||||
|
-- Notifications
|
||||||
|
TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich reaktiviert für ' .. targetName, 'success')
|
||||||
|
TriggerClientEvent('QBCore:Notify', targetId, 'Deine ' .. (config.label or licenseType) .. ' wurde reaktiviert!', 'success')
|
||||||
|
|
||||||
|
-- Events
|
||||||
|
TriggerClientEvent('license-system:client:licenseReactivated', src, targetId, licenseType)
|
||||||
|
TriggerClientEvent('license-system:client:refreshMenu', src)
|
||||||
|
|
||||||
|
-- Log
|
||||||
|
debugPrint("Lizenz " .. licenseType .. " reaktiviert von " .. issuerName .. " für " .. targetName)
|
||||||
|
else
|
||||||
|
TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Reaktivieren der Lizenz!', 'error')
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Add to client/main.lua
|
||||||
|
-- Add a reactivation option to the player license menu
|
||||||
|
local function openReactivateLicenseMenu(targetId, targetName)
|
||||||
|
debugPrint("Öffne Lizenz-Reaktivierungs-Menü für: " .. targetName)
|
||||||
|
|
||||||
|
-- Request all licenses including inactive ones
|
||||||
|
TriggerServerEvent('license-system:server:requestAllPlayerLicenses', targetId, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- New event to receive all licenses including inactive ones
|
||||||
|
RegisterNetEvent('license-system:client:receiveAllPlayerLicenses', function(licenses, targetId, targetName)
|
||||||
|
debugPrint("=== Event: receiveAllPlayerLicenses ===")
|
||||||
|
debugPrint("Erhaltene Lizenzen: " .. #licenses)
|
||||||
|
|
||||||
|
local menuOptions = {}
|
||||||
|
|
||||||
|
if licenses and #licenses > 0 then
|
||||||
|
for _, license in ipairs(licenses) do
|
||||||
|
if license.is_active == 0 then -- Only show inactive licenses
|
||||||
|
local licenseConfig = Config.LicenseTypes[license.license_type] or {
|
||||||
|
label = license.license_type,
|
||||||
|
icon = 'fas fa-id-card',
|
||||||
|
color = '#667eea'
|
||||||
|
}
|
||||||
|
|
||||||
|
table.insert(menuOptions, {
|
||||||
|
title = licenseConfig.label .. ' ❌',
|
||||||
|
description = 'Status: Ungültig | Ausgestellt: ' .. (license.issue_date or 'Unbekannt'),
|
||||||
|
icon = licenseConfig.icon,
|
||||||
|
onSelect = function()
|
||||||
|
-- Confirmation dialog
|
||||||
|
lib.registerContext({
|
||||||
|
id = 'confirm_reactivate_license',
|
||||||
|
title = 'Lizenz reaktivieren bestätigen',
|
||||||
|
options = {
|
||||||
|
{
|
||||||
|
title = 'Spieler: ' .. targetName,
|
||||||
|
disabled = true,
|
||||||
|
icon = 'fas fa-user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = 'Lizenztyp: ' .. licenseConfig.label,
|
||||||
|
disabled = true,
|
||||||
|
icon = licenseConfig.icon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = '─────────────────',
|
||||||
|
disabled = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = '✅ Bestätigen',
|
||||||
|
description = 'Lizenz jetzt reaktivieren',
|
||||||
|
icon = 'fas fa-check',
|
||||||
|
onSelect = function()
|
||||||
|
debugPrint("Sende Lizenz-Reaktivierung an Server...")
|
||||||
|
TriggerServerEvent('license-system:server:reactivateLicense', targetId, license.license_type)
|
||||||
|
lib.hideContext()
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = '❌ Abbrechen',
|
||||||
|
description = 'Vorgang abbrechen',
|
||||||
|
icon = 'fas fa-times',
|
||||||
|
onSelect = function()
|
||||||
|
openReactivateLicenseMenu(targetId, targetName)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lib.showContext('confirm_reactivate_license')
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #menuOptions == 0 then
|
||||||
|
table.insert(menuOptions, {
|
||||||
|
title = 'Keine inaktiven Lizenzen',
|
||||||
|
description = 'Dieser Spieler hat keine inaktiven 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 = 'reactivate_license',
|
||||||
|
title = 'Lizenz reaktivieren: ' .. targetName,
|
||||||
|
options = menuOptions
|
||||||
|
})
|
||||||
|
|
||||||
|
lib.showContext('reactivate_license')
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Add this option to the player license menu
|
||||||
|
-- In openPlayerLicenseMenu function, add:
|
||||||
|
table.insert(menuOptions, {
|
||||||
|
title = 'Lizenz reaktivieren',
|
||||||
|
description = 'Eine inaktive Lizenz wieder aktivieren',
|
||||||
|
icon = 'fas fa-redo',
|
||||||
|
onSelect = function()
|
||||||
|
openReactivateLicenseMenu(targetId, targetName)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add to server/main.lua
|
||||||
|
RegisterNetEvent('license-system:server:issueManualLicense', function(targetId, licenseData)
|
||||||
|
local src = source
|
||||||
|
debugPrint("=== Event: issueManualLicense ===")
|
||||||
|
debugPrint("Source: " .. src .. ", Target: " .. targetId)
|
||||||
|
|
||||||
|
if not hasPermission(src) then
|
||||||
|
debugPrint("Keine Berechtigung für Spieler: " .. src)
|
||||||
TriggerClientEvent('QBCore:Notify', src, Config.Notifications.no_permission.message, Config.Notifications.no_permission.type)
|
TriggerClientEvent('QBCore:Notify', src, Config.Notifications.no_permission.message, Config.Notifications.no_permission.type)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Lizenz-Konfiguration prüfen
|
local targetPlayer = QBCore.Functions.GetPlayer(targetId)
|
||||||
local config = Config.LicenseTypes[licenseType]
|
if not targetPlayer then
|
||||||
if not config then
|
debugPrint("Ziel-Spieler nicht gefunden: " .. targetId)
|
||||||
debugPrint("Unbekannter Lizenztyp: " .. licenseType)
|
TriggerClientEvent('QBCore:Notify', src, 'Spieler nicht gefunden!', 'error')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
debugPrint("Erstelle benutzerdefinierte Lizenz: " .. licenseType .. " für " .. TargetPlayer.PlayerData.citizenid)
|
local issuerPlayer = QBCore.Functions.GetPlayer(src)
|
||||||
|
if not issuerPlayer then
|
||||||
-- Benutzerdefinierte Daten validieren und bereinigen
|
debugPrint("Aussteller nicht gefunden: " .. src)
|
||||||
local validatedData = {}
|
return
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
-- Klassen validieren
|
local targetCitizenId = targetPlayer.PlayerData.citizenid
|
||||||
local validatedClasses = {}
|
local issuerCitizenId = issuerPlayer.PlayerData.citizenid
|
||||||
if config.classes and classes then
|
|
||||||
for _, class in ipairs(classes) do
|
-- Check if license type is valid
|
||||||
if config.classes[class.key] then
|
if not Config.LicenseTypes[licenseData.license_type] then
|
||||||
table.insert(validatedClasses, class)
|
TriggerClientEvent('QBCore:Notify', src, 'Ungültiger Lizenztyp!', 'error')
|
||||||
end
|
return
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Lizenz in Datenbank speichern
|
-- Save photo if provided
|
||||||
local success = saveCustomLicenseToDB(
|
if licenseData.photo_url then
|
||||||
TargetPlayer.PlayerData.citizenid,
|
-- Here you would save the photo to your storage system
|
||||||
licenseType,
|
-- For example, to a folder or database
|
||||||
Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname,
|
-- This is just a placeholder
|
||||||
validatedData,
|
debugPrint("Foto für Lizenz vorhanden")
|
||||||
validatedClasses
|
end
|
||||||
)
|
|
||||||
|
|
||||||
if success then
|
-- Insert into database with manual data
|
||||||
debugPrint("Benutzerdefinierte Lizenz erfolgreich gespeichert")
|
local query = [[
|
||||||
|
INSERT INTO player_licenses
|
||||||
|
(citizenid, license_type, name, birthday, gender, issue_date, expire_date, issued_by, is_active, photo_url, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
||||||
|
]]
|
||||||
|
|
||||||
-- Cache invalidieren
|
local createdAt = os.time()
|
||||||
invalidateCache(TargetPlayer.PlayerData.citizenid, licenseType)
|
|
||||||
|
|
||||||
-- Benachrichtigungen
|
local insertData = {
|
||||||
TriggerClientEvent('QBCore:Notify', src, Config.Notifications.license_issued.message, Config.Notifications.license_issued.type)
|
targetCitizenId,
|
||||||
TriggerClientEvent('QBCore:Notify', targetId, "Du hast eine " .. config.label .. " erhalten!", "success")
|
licenseData.license_type,
|
||||||
|
licenseData.name,
|
||||||
|
licenseData.birthday,
|
||||||
|
licenseData.gender,
|
||||||
|
licenseData.issue_date,
|
||||||
|
licenseData.expire_date,
|
||||||
|
issuerCitizenId,
|
||||||
|
licenseData.photo_url or '',
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
-- First deactivate any existing licenses of this type
|
||||||
|
local deactivateQuery = "UPDATE player_licenses SET is_active = 0 WHERE citizenid = ? AND license_type = ?"
|
||||||
|
MySQL.query.await(deactivateQuery, {targetCitizenId, licenseData.license_type})
|
||||||
|
|
||||||
|
-- Then insert the new license
|
||||||
|
local result = MySQL.insert.await(query, insertData)
|
||||||
|
|
||||||
|
if result then
|
||||||
|
-- Invalidate cache
|
||||||
|
invalidateCache(targetCitizenId)
|
||||||
|
|
||||||
|
local targetName = getPlayerName(targetId)
|
||||||
|
local issuerName = getPlayerName(src)
|
||||||
|
local config = Config.LicenseTypes[licenseData.license_type]
|
||||||
|
|
||||||
|
-- Notifications
|
||||||
|
TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich ausgestellt für ' .. targetName, 'success')
|
||||||
|
TriggerClientEvent('QBCore:Notify', targetId, 'Du hast eine neue Lizenz erhalten: ' .. config.label, 'success')
|
||||||
|
|
||||||
-- Events
|
-- Events
|
||||||
TriggerClientEvent('license-system:client:licenseIssued', src, targetId, licenseType)
|
TriggerClientEvent('license-system:client:licenseIssued', src, targetId, licenseData.license_type)
|
||||||
TriggerClientEvent('license-system:client:refreshMenu', src)
|
TriggerClientEvent('license-system:client:refreshMenu', src)
|
||||||
|
|
||||||
-- Log
|
-- Log
|
||||||
debugPrint("Lizenz ausgestellt: " .. licenseType .. " von " .. Player.PlayerData.name .. " an " .. TargetPlayer.PlayerData.name)
|
debugPrint("Lizenz " .. licenseData.license_type .. " manuell ausgestellt von " .. issuerName .. " für " .. targetName)
|
||||||
else
|
else
|
||||||
debugPrint("Fehler beim Speichern der benutzerdefinierten Lizenz")
|
TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Ausstellen der Lizenz!', 'error')
|
||||||
TriggerClientEvent('QBCore:Notify', src, "Fehler beim Ausstellen der Lizenz", "error")
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Funktion zum Speichern benutzerdefinierter Lizenzen
|
-- Add a new event to get all licenses including inactive ones
|
||||||
function saveCustomLicenseToDB(citizenid, licenseType, issuedBy, customData, classes)
|
RegisterNetEvent('license-system:server:requestAllPlayerLicenses', function(targetId, includeInactive)
|
||||||
local config = Config.LicenseTypes[licenseType]
|
local src = source
|
||||||
if not config then return false end
|
debugPrint("=== Event: requestAllPlayerLicenses ===")
|
||||||
|
debugPrint("Source: " .. src .. ", Target: " .. targetId .. ", IncludeInactive: " .. tostring(includeInactive))
|
||||||
|
|
||||||
-- Ablaufdatum berechnen
|
if not hasPermission(src) then
|
||||||
local expireDate = nil
|
debugPrint("Keine Berechtigung für Spieler: " .. src)
|
||||||
if config.validity_days then
|
TriggerClientEvent('license-system:client:receiveAllPlayerLicenses', src, {}, targetId, "Unbekannt")
|
||||||
expireDate = os.date('%Y-%m-%d %H:%M:%S', os.time() + (config.validity_days * 24 * 60 * 60))
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Holder-Name aus Custom-Data oder Standard
|
local targetPlayer = QBCore.Functions.GetPlayer(targetId)
|
||||||
local holderName = customData.holder_name or "Unbekannt"
|
if not targetPlayer then
|
||||||
|
debugPrint("Ziel-Spieler nicht gefunden: " .. targetId)
|
||||||
return safeDBOperation(function()
|
TriggerClientEvent('license-system:client:receiveAllPlayerLicenses', src, {}, targetId, "Unbekannt")
|
||||||
-- Alte Lizenzen deaktivieren
|
return
|
||||||
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
|
end
|
||||||
|
|
||||||
local license = safeDBOperation(function()
|
local citizenid = targetPlayer.PlayerData.citizenid
|
||||||
local query = [[
|
local targetName = getPlayerName(targetId)
|
||||||
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})
|
-- Query to get all licenses, including inactive ones if requested
|
||||||
return result and result[1] or nil
|
local query = "SELECT * FROM player_licenses WHERE citizenid = ? ORDER BY license_type, created_at DESC"
|
||||||
end, "Lizenz abrufen")
|
|
||||||
|
|
||||||
if license then
|
local result = MySQL.query.await(query, {citizenid})
|
||||||
-- Custom-Data und Classes parsen
|
|
||||||
if license.custom_data then
|
if not result or #result == 0 then
|
||||||
license.custom_data_parsed = json.decode(license.custom_data)
|
debugPrint("Keine Lizenzen gefunden für: " .. citizenid)
|
||||||
|
TriggerClientEvent('license-system:client:receiveAllPlayerLicenses', src, {}, targetId, targetName)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Process licenses
|
||||||
|
local licenses = {}
|
||||||
|
local seenTypes = {}
|
||||||
|
|
||||||
|
for _, license in ipairs(result) do
|
||||||
|
-- If includeInactive is true, add all licenses
|
||||||
|
-- Otherwise, only add the newest license per type
|
||||||
|
local shouldAdd = includeInactive or not seenTypes[license.license_type]
|
||||||
|
|
||||||
|
if shouldAdd then
|
||||||
|
seenTypes[license.license_type] = true
|
||||||
|
|
||||||
|
-- Add holder name
|
||||||
|
license.holder_name = targetName
|
||||||
|
|
||||||
|
-- Add issuer name
|
||||||
|
if license.issued_by then
|
||||||
|
local issuerQuery = "SELECT charinfo FROM players WHERE citizenid = ?"
|
||||||
|
local issuerResult = MySQL.query.await(issuerQuery, {license.issued_by})
|
||||||
|
|
||||||
|
if issuerResult and #issuerResult > 0 then
|
||||||
|
license.issued_by_name = extractPlayerName(issuerResult[1].charinfo)
|
||||||
|
else
|
||||||
|
license.issued_by_name = "System"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
license.issued_by_name = "System"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse classes
|
||||||
|
if license.classes then
|
||||||
|
local success, classes = pcall(json.decode, license.classes)
|
||||||
|
if success and type(classes) == "table" then
|
||||||
|
license.classes = classes
|
||||||
|
else
|
||||||
|
license.classes = {}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
license.classes = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Normalize is_active
|
||||||
|
if license.is_active == nil then
|
||||||
|
license.is_active = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(licenses, license)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
return license
|
debugPrint("Sende " .. #licenses .. " Lizenzen für " .. targetName)
|
||||||
end
|
TriggerClientEvent('license-system:client:receiveAllPlayerLicenses', src, licenses, targetId, targetName)
|
||||||
|
end)
|
||||||
debugPrint("License-System Server erweitert geladen (Custom License Support)")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
debugPrint("License-System Server vollständig geladen (Status-Fix)")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue