diff --git a/bridge/esx/server.lua b/bridge/esx/server.lua index bde4741..c39a476 100644 --- a/bridge/esx/server.lua +++ b/bridge/esx/server.lua @@ -1,6 +1,15 @@ local ESX = exports["es_extended"]:getSharedObject() local Bridge = {} +-- Alpha + +-- Framework-specific character unload handler +function Bridge.onCharacterUnloaded(callback) + AddEventHandler("esx:playerLogout", callback) +end + +-- Alpha + --[[ NEEDS TO BE CREATED ]] -- SetTimeout(500, function() -- NDCore:loadSQL({ diff --git a/bridge/nd/server.lua b/bridge/nd/server.lua index f5aaeea..e221522 100644 --- a/bridge/nd/server.lua +++ b/bridge/nd/server.lua @@ -1,6 +1,15 @@ local NDCore = exports["ND_Core"] local Bridge = {} +-- Alpha + +-- Framework-specific character unload handler +function Bridge.onCharacterUnloaded(callback) + AddEventHandler("ND:characterUnloaded", callback) +end + +-- Alpha + SetTimeout(500, function() NDCore:loadSQL({ "bridge/nd/database/bolos.sql", @@ -29,7 +38,7 @@ local function queryDatabaseProfiles(first, last) local firstname = (item.firstname or ""):lower() local lastname = (item.lastname or ""):lower() - if first ~= "" and firstname:find(first) or last ~= "" and lastname:find(last) then + if first ~= "" and firstname:find(first) or last ~= "" and lastname:find(last) then profiles[item.charid] = { firstName = item.firstname, lastName = item.lastname, @@ -337,10 +346,10 @@ function Bridge.viewEmployees(src, search) if ply.id == info.charid then local job, jobInfo = getPermsFromGroups(ply.groups) if not config.policeAccess[job] then goto next end - + local metadata = ply.metadata if not filterEmployeeSearch(ply, metadata, search or "") then goto next end - + employees[#employees+1] = { source = ply.source, charId = ply.id, @@ -357,7 +366,7 @@ function Bridge.viewEmployees(src, search) goto next end end - + local groups = info.groups and json.decode(info.groups) or {} local job, jobInfo = getPermsFromGroups(groups) @@ -365,7 +374,7 @@ function Bridge.viewEmployees(src, search) local metadata = info.metadata and json.decode(info.metadata) or {} if not filterEmployeeSearch(info, metadata, search or "") then goto next end - + employees[#employees+1] = { charId = info.charid, first = info.firstname, @@ -390,7 +399,7 @@ function Bridge.employeeUpdateCallsign(src, charid, callsign) if not player then return false, "An issue occured try again later!" end - + if not tonumber(callsign) then return false, "Callsign must be a number!" end @@ -453,7 +462,7 @@ function Bridge.employeeUpdateCallsign(src, charid, callsign) end characterMetadata.callsign = callsign - + MySQL.update.await("UPDATE nd_characters SET `metadata` = ? WHERE charid = ?", { json.encode(characterMetadata), charid diff --git a/bridge/qb/client.lua b/bridge/qb/client.lua new file mode 100644 index 0000000..b31e485 --- /dev/null +++ b/bridge/qb/client.lua @@ -0,0 +1,87 @@ +local QBCore = exports["qb-core"]:GetCoreObject() +local Bridge = {} + +---@return table +function Bridge.getPlayerInfo() + local player = QBCore.Functions.GetPlayerData() or {} + return { + firstName = player.charinfo.firstname, + lastName = player.charinfo.lastname, + job = player.job.name, + jobLabel = player.job.label, + callsign = player.metadata.callsign, + img = player.metadata.img or "user.jpg", + isBoss = player.job.isboss + } +end + +---@param job string +---@return boolean +function Bridge.hasAccess(job) + return config.policeAccess[job] or config.fireAccess[job] +end + +---@return string +function Bridge.rankName() + local player = QBCore.Functions.GetPlayerData() + return player.job.grade.name or "" +end + +---@param id number +---@param info table +---@return table +--- info is from returned profiles in server.lua +function Bridge.getCitizenInfo(id, info) + return { + img = info.img or "user.jpg", + characterId = id, + firstName = info.firstName, + lastName = info.lastName, + dob = info.dob, + gender = info.gender, + phone = info.phone, + ethnicity = info.ethnicity + } +end + +function Bridge.getRanks(job) + local jobGrades = QBCore.Shared.Jobs[job] and QBCore.Shared.Jobs[job].grades or nil + if not jobGrades then return end + + local options = {} + for grade, info in pairs(jobGrades) do + options[#options + 1] = { + value = tonumber(grade), + label = info.name + } + end + table.sort(options, function(a, b) return a.value < b.value end) + + return options, job +end + +AddEventHandler("QBCore:Client:OnPlayerUnload", function() + TriggerServerEvent("ND_MDT:qb:server:playerUnloaded") +end) + +if GetResourceState("qb-inventory") == "started" then + RegisterNetEvent("ND_MDT:qb:client:useTablet") + AddEventHandler("ND_MDT:qb:client:useTablet", function() + if not exports["qb-inventory"]:HasItem("mdt", 1) then return end + exports["ND_MDT"]:useTablet() + end) +end + +-- Waypoint event for house locations +RegisterNetEvent("ND_MDT:setWaypoint") +AddEventHandler("ND_MDT:setWaypoint", function(x, y) + local playerInfo = Bridge.getPlayerInfo() + if not Bridge.hasAccess(playerInfo.job) then + QBCore.Functions.Notify('You do not have access to this command', 'error') + return + end + + SetNewWaypoint(x, y) +end) + +return Bridge \ No newline at end of file diff --git a/bridge/qb/database/bolos.sql b/bridge/qb/database/bolos.sql new file mode 100644 index 0000000..e0bc031 --- /dev/null +++ b/bridge/qb/database/bolos.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `nd_mdt_bolos` ( + `id` INT(11) AUTO_INCREMENT, + `type` VARCHAR(50) DEFAULT NULL, + `data` LONGTEXT DEFAULT '[]', + `timestamp` INT(11) DEFAULT unix_timestamp(), + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/bridge/qb/database/improvements.sql b/bridge/qb/database/improvements.sql new file mode 100644 index 0000000..19cd52d --- /dev/null +++ b/bridge/qb/database/improvements.sql @@ -0,0 +1,2 @@ +ALTER TABLE `player_vehicles` + ADD COLUMN IF NOT EXISTS `stolen` BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/bridge/qb/database/records.sql b/bridge/qb/database/records.sql new file mode 100644 index 0000000..e7867fd --- /dev/null +++ b/bridge/qb/database/records.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `nd_mdt_records` ( + `character` VARCHAR(50) NOT NULL COLLATE utf8mb4_general_ci, + `records` LONGTEXT DEFAULT '[]', + PRIMARY KEY (`character`), + CONSTRAINT `FK_nd_mdt_records_players` FOREIGN KEY (`character`) REFERENCES `players` (`citizenid`) ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/bridge/qb/database/reports.sql b/bridge/qb/database/reports.sql new file mode 100644 index 0000000..368d189 --- /dev/null +++ b/bridge/qb/database/reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS `nd_mdt_reports` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `type` VARCHAR(50) DEFAULT NULL, + `data` LONGTEXT DEFAULT '[]', + `timestamp` INT(11) DEFAULT unix_timestamp(), + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + diff --git a/bridge/qb/database/weapons.sql b/bridge/qb/database/weapons.sql new file mode 100644 index 0000000..9934011 --- /dev/null +++ b/bridge/qb/database/weapons.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `nd_mdt_weapons` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `character` VARCHAR(50) NOT NULL COLLATE utf8mb4_general_ci, + `weapon` VARCHAR(50) DEFAULT NULL, + `serial` VARCHAR(50) DEFAULT NULL, + `owner_name` VARCHAR(100) DEFAULT NULL, + `stolen` INT(1) DEFAULT '0', + PRIMARY KEY (`id`), + INDEX `FK_nd_mdt_weapons_players` (`character`) USING BTREE, + CONSTRAINT `FK_nd_mdt_weapons_players` FOREIGN KEY (`character`) REFERENCES `players` (`citizenid`) ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/bridge/qb/notes.txt b/bridge/qb/notes.txt new file mode 100644 index 0000000..4acdc37 --- /dev/null +++ b/bridge/qb/notes.txt @@ -0,0 +1,107 @@ +# QBCore Integration Notes + +## Weapon Registration Integration for qb-inventory + +If you are using qb-inventory instead of ox_inventory, you need to manually add weapon registration to your qb-inventory. + +### Installation Steps: + +1. Open your `qb-inventory/server/functions.lua` file +2. Find the `AddItem` function (around line 667-774) +3. Locate the weapon handling section that looks like this: + +```lua +if itemInfo.type == 'weapon' then + if not inventory[slot].info.serie then + inventory[slot].info.serie = tostring(QBCore.Shared.RandomInt(2) .. QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(1) .. QBCore.Shared.RandomStr(2) .. QBCore.Shared.RandomInt(3) .. QBCore.Shared.RandomStr(4)) + end + if not inventory[slot].info.quality then + inventory[slot].info.quality = 100 + end +end +``` + +4. Replace that section with this enhanced version: + +```lua +if itemInfo.type == 'weapon' then + if not inventory[slot].info.serie then + inventory[slot].info.serie = tostring(QBCore.Shared.RandomInt(2) .. QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(1) .. QBCore.Shared.RandomStr(2) .. QBCore.Shared.RandomInt(3) .. QBCore.Shared.RandomStr(4)) + end + if not inventory[slot].info.quality then + inventory[slot].info.quality = 100 + end + + -- MDT Weapon Registration with duplicate check + if player and inventory[slot].info.serie then + local success, result = pcall(function() + return exports['ND_MDT']:registerWeapon(identifier, itemInfo.label, inventory[slot].info.serie) + end) + if not success then + print('[qb-inventory] Failed to register weapon in MDT:', result) + end + end +end +``` + +### What this does: + +- Automatically registers weapons in the MDT database when they are added to a player's inventory +- Includes duplicate check to prevent re-registering the same weapon serial number +- Uses the weapon's serial number, label, and player information +- Includes error handling to prevent inventory issues if MDT is not available +- Only registers weapons for actual players (not for inventories or drops) + +### Requirements: + +- Make sure ND_MDT resource is started before qb-inventory +- The player must be online when receiving the weapon +- The weapon must have a valid serial number + +### Notes: + +- This integration works for all weapons added through the AddItem function +- Weapons added through other methods may not be automatically registered +- You can verify registration by checking the weapons section in the MDT interface + +🐞 KNOWN BUGS +1-) +Bug Report: Only the Last Added Record Is Saved on "Save All Changes" + +Description: +When multiple records are added in the "Records" section (e.g., 5 records), and then "Save All Changes" is clicked, only the last record added gets saved. The previously added records are not persisted unless "Save All Changes" is clicked after each individual record addition. + +Steps to Reproduce: + +Go to the Records section. + +Add multiple records (e.g., 5 records) without clicking "Save All Changes" after each addition. + +Click "Save All Changes" only once after adding all the records. + +Refresh or revisit the page. + +Expected Result: +All added records should be saved when "Save All Changes" is clicked once. + +Actual Result: +Only the last added record is saved; the others are lost. + +2-) +Important Notice About License Dates + +**WARNING**: The license system currently does not implement proper date/time management. + +The timestamp data for licenses (issued date, expiry date, etc.) should be considered as **random/placeholder data** and not reliable for actual license validation or expiry checking. + +### Current Limitations: +- License dates are generated but not validated +- No automatic expiry system is implemented +- Timestamps are for display/record purposes only +- No background job checks for expired licenses + +### Recommendations: +- Treat all license dates as informational only +- Do not rely on expiry dates for game mechanics +- Consider implementing proper date validation if needed for your server +- Manual license management is recommended until proper date system is implemented \ No newline at end of file diff --git a/bridge/qb/server.lua b/bridge/qb/server.lua new file mode 100644 index 0000000..10e6398 --- /dev/null +++ b/bridge/qb/server.lua @@ -0,0 +1,840 @@ +local QBCore = exports["qb-core"]:GetCoreObject() +local Bridge = {} + +-- Framework-specific character unload handler +function Bridge.onCharacterUnloaded(callback) + RegisterNetEvent("ND_MDT:qb:server:playerUnloaded") + AddEventHandler("ND_MDT:qb:server:playerUnloaded", callback) +end + +SetTimeout(500, function() + local fileLocations = { + "bridge/qb/database/bolos.sql", + "bridge/qb/database/records.sql", + "bridge/qb/database/reports.sql", + "bridge/qb/database/weapons.sql", + "bridge/qb/database/improvements.sql" + } + + local resourceName = GetCurrentResourceName() + for _, fileLocation in ipairs(fileLocations) do + local file = LoadResourceFile(resourceName, fileLocation) + if not file then return end + + MySQL.query(file) + Wait(100) + end +end) + +local function getPlayerSource(id) + local player = QBCore.Functions.GetPlayerByCitizenId(id) + return player and player.PlayerData.source or false +end + +local function queryDatabaseProfiles(first, last) + local result = MySQL.query.await("SELECT * FROM players") + local profiles = {} + for i = 1, #result do + local item = result[i] + local metadata = json.decode(item.metadata) + local charinfo = json.decode(item.charinfo) + local firstname = (charinfo.firstname or ""):lower() + local lastname = (charinfo.lastname or ""):lower() + + if first ~= "" and firstname:find(first) or last ~= "" and lastname:find(last) then + profiles[item.citizenid] = { + firstName = charinfo.firstname, + lastName = charinfo.lastname, + dob = charinfo.birthdate, + gender = charinfo.gender == 0 and "Male" or "Female", + phone = charinfo.phone, + id = getPlayerSource(item.citizenid), + img = metadata.img or "user.jpg", + ethnicity = charinfo.nationality + } + end + end + return profiles +end + +---@param src number +---@param first string|nil +---@param last string|nil +---@return table +function Bridge.nameSearch(src, first, last) + local player = QBCore.Functions.GetPlayer(src) + if not config.policeAccess[player.PlayerData.job.name] then return {} end + + local profiles = {} + local firstname = (first or ""):lower() + local lastname = (last):lower() + local data = queryDatabaseProfiles(firstname, lastname) + + for k, v in pairs(data) do + profiles[k] = v + end + + return profiles +end + +local function findCharacterById(id) + local player = QBCore.Functions.GetPlayerByCitizenId(id) + if player then + return { + citizenid = player.PlayerData.citizenid, + firstname = player.PlayerData.charinfo.firstname, + lastname = player.PlayerData.charinfo.lastname, + dob = player.PlayerData.charinfo.birthdate, + gender = player.PlayerData.charinfo.gender, + phone = player.PlayerData.charinfo.phone, + source = player.PlayerData.source, + metadata = player.PlayerData.metadata + } + end + + -- If player is not online, query database + local result = MySQL.query.await("SELECT * FROM players WHERE citizenid = ?", {id}) + if result and result[1] then + local item = result[1] + local charinfo = json.decode(item.charinfo) or {} + local metadata = json.decode(item.metadata) or {} + return { + citizenid = item.citizenid, + firstname = charinfo.firstname, + lastname = charinfo.lastname, + dob = charinfo.birthdate, + gender = charinfo.gender, + phone = charinfo.phone, + source = nil, + metadata = metadata + } + end + + return nil +end + +---@param source number +---@param characterSearched number +---@return table +function Bridge.characterSearch(source, characterSearched) + local player = QBCore.Functions.GetPlayer(source) + if not config.policeAccess[player.PlayerData.job.name] then return {} end + + local profiles = {} + local item = findCharacterById(characterSearched) + + if not item then + return profiles + end + + profiles[item.citizenid] = { + firstName = item.firstname, + lastName = item.lastname, + dob = item.dob, + gender = item.gender == 0 and "Male" or "Female", + phone = item.phone, + id = item.source, + img = item.metadata.img or "user.jpg", + ethnicity = item.metadata.ethnicity or "Unknown" + } + + return profiles +end + +---@param src number +---@return table +function Bridge.getPlayerInfo(src) + local player = QBCore.Functions.GetPlayer(src) or {} + return { + firstName = player.PlayerData.charinfo.firstname or "", + lastName = player.PlayerData.charinfo.lastname or "", + job = player.PlayerData.job.name or "", + jobLabel = player.PlayerData.job.label or "", + callsign = player.PlayerData.metadata.callsign or "", + img = player.PlayerData.metadata.img or "user.jpg", + characterId = player.PlayerData.citizenid + } +end + +local function getVehicleCharacter(owner) + local item = findCharacterById(owner) + if item then + return { + firstName = item.firstname, + lastName = item.lastname, + characterId = item.citizenid + } + end + return nil +end + +local function queryDatabaseVehicles(find, findData) + local query = ("SELECT * FROM player_vehicles WHERE %s = ?"):format(find) + local result = MySQL.query.await(query, {findData}) + local vehicles = {} + local character = find == "citizenid" and getVehicleCharacter(findData) + + for i = 1, #result do + local item = result[i] + if find == "plate" then + character = getVehicleCharacter(item.citizenid) + end + + local mods = item.mods and json.decode(item.mods) or {} + + local modelName = item.vehicle or "Unknown" + local vehicleData = QBCore.Shared.Vehicles[modelName:lower()] + local displayName = vehicleData and vehicleData.name or modelName + local brand = vehicleData and vehicleData.brand or "Unknown" + local category = vehicleData and vehicleData.category or "Unknown" + + vehicles[item.id] = { + id = item.id, + plate = item.plate, + color = mods.color1 or "Unknown", + make = brand, + model = displayName, + class = category, + stolen = item.stolen, + character = character, + vehicle = item.vehicle, + garage = item.garage, + state = item.state, + fuel = item.fuel, + engine = item.engine, + body = item.body, + mods = mods + } + end + return vehicles +end + +---@param src number +---@param searchBy string +---@param data number|string +---@return table +function Bridge.viewVehicles(src, searchBy, data) + local player = QBCore.Functions.GetPlayer(src) + if not config.policeAccess[player.PlayerData.job.name] then return {} end + + local vehicles = {} + if searchBy == "plate" then + local data = queryDatabaseVehicles("plate", data) + for k, v in pairs(data) do + vehicles[k] = v + end + elseif searchBy == "owner" then + local data = queryDatabaseVehicles("citizenid", data) + for k, v in pairs(data) do + vehicles[k] = v + end + end + return vehicles +end + +---@param id number +---@return table +function Bridge.getProperties(id) + if GetResourceState("qb-houses") ~= "started" then + return {} + end + + local adresses = {} + local result = MySQL.query.await("SELECT house FROM player_houses WHERE citizenid = ?", {id}) + if not result or not result[1] then return adresses end + for _, house in pairs(result) do + adresses[#adresses+1] = house.house + end + return adresses +end + +-- Helper function to get table keys +local function table_keys(t) + local keys = {} + for k, v in pairs(t) do + keys[#keys+1] = k + end + return keys +end + +---@param id number +---@return table +function Bridge.getLicenses(id) + --[[ info in a license. + { + type = string (driver, weapon, hunting, etc), + status = string (valid, expired, suspended, etc), + issued = timestamp, + expires = timestamp, + identifier = in ND it's a 16 character identifier including letters and numbers. + } + ]] + + --print('[MDT] getLicenses called for citizenid:', id) + + local result = MySQL.query.await("SELECT `metadata` FROM players WHERE citizenid = ?", {id}) + if not result or not result[1] then + --print('[MDT] Player not found in database for citizenid:', id) + return {} + end + + local metadata = json.decode(result[1].metadata) or {} + --print('[MDT] Metadata loaded, keys:', json.encode(table_keys(metadata))) + + -- Check if we already have MDT licenses stored and they are not empty + local mdtLicenses = metadata.mdtLicenses + if mdtLicenses and #mdtLicenses > 0 then + --print('[MDT] Found existing mdtLicenses, count:', #mdtLicenses) + return mdtLicenses + end + + -- Convert QBCore licenses to MDT format if no MDT licenses exist + local rawLicenses = metadata.licences or {} + --print('[MDT] Converting QBCore licenses to MDT format. Raw licenses:', json.encode(rawLicenses)) + + local licenses = {} + + for licenseType, hasLicense in pairs(rawLicenses) do + --print('[MDT] Processing license:', licenseType, hasLicense) + -- Create a consistent identifier based on citizenid and license type + local identifier = string.format("%s_%s", id, licenseType) + + licenses[#licenses + 1] = { + type = licenseType, + status = hasLicense and "valid" or "expired", + issued = os.time(), + expires = hasLicense and (os.time() + (365 * 24 * 60 * 60)) or (os.time() - (365 * 24 * 60 * 60)), + identifier = identifier + } + end + + --print('[MDT] Created licenses count:', #licenses) + + -- Store the converted licenses in metadata for future use + if #licenses > 0 then + metadata.mdtLicenses = licenses + MySQL.update.await("UPDATE players SET metadata = ? WHERE citizenid = ?", { + json.encode(metadata), + id + }) + --print('[MDT] Stored mdtLicenses in database') + else + --print('[MDT] No licenses found to store') + end + + return licenses +end + + + +---@param characterId number +---@param licenseIdentifier string +---@param newLicenseStatus string +function Bridge.editPlayerLicense(characterId, licenseIdentifier, newLicenseStatus) + --print('[MDT] editPlayerLicense called:', characterId, licenseIdentifier, newLicenseStatus) + + local player = QBCore.Functions.GetPlayerByCitizenId(characterId) + + if player then + -- Player is online, but need to get fresh metadata from database + --print('[MDT] Player is online, loading fresh metadata from database') + local result = MySQL.query.await("SELECT metadata FROM players WHERE citizenid = ?", {characterId}) + if not result or not result[1] then + --print('[MDT] Failed to load metadata from database') + return + end + + local metadata = json.decode(result[1].metadata) or {} + local mdtLicenses = metadata.mdtLicenses or {} + + --print('[MDT] Fresh mdtLicenses count from database:', #mdtLicenses) + + -- Find and update license by identifier + local found = false + local licenseType = nil + for i, license in ipairs(mdtLicenses) do + if license.identifier == licenseIdentifier then + --print('[MDT] Found license to update:', license.type, 'from', license.status, 'to', newLicenseStatus) + mdtLicenses[i].status = newLicenseStatus + licenseType = license.type + found = true + break + end + end + + if not found then + --print('[MDT] License not found with identifier:', licenseIdentifier) + else + -- Also update QBCore licences system + local licences = metadata.licences or {} + if licenseType then + -- valid = true, expired/suspended = false + licences[licenseType] = (newLicenseStatus == "valid") + metadata.licences = licences + --print('[MDT] Updated QBCore licences:', licenseType, '=', licences[licenseType]) + end + end + + -- Update both database and player memory + metadata.mdtLicenses = mdtLicenses + MySQL.update.await("UPDATE players SET metadata = ? WHERE citizenid = ?", { + json.encode(metadata), + characterId + }) + player.Functions.SetPlayerData("metadata", metadata) + --print('[MDT] Metadata updated in both database and player memory') + else + -- Player is offline, update database + --print('[MDT] Player is offline, updating database') + local result = MySQL.query.await("SELECT metadata FROM players WHERE citizenid = ?", {characterId}) + if result and result[1] then + local metadata = json.decode(result[1].metadata) or {} + local mdtLicenses = metadata.mdtLicenses or {} + + --print('[MDT] Offline - Current mdtLicenses count:', #mdtLicenses) + + -- Find and update license by identifier + local found = false + local licenseType = nil + for i, license in ipairs(mdtLicenses) do + if license.identifier == licenseIdentifier then + --print('[MDT] Found offline license to update:', license.type, 'from', license.status, 'to', newLicenseStatus) + mdtLicenses[i].status = newLicenseStatus + licenseType = license.type + found = true + break + end + end + + if not found then + --print('[MDT] Offline license not found with identifier:', licenseIdentifier) + else + -- Also update QBCore licences system for offline player + local licences = metadata.licences or {} + if licenseType then + -- valid = true, expired/suspended = false + licences[licenseType] = (newLicenseStatus == "valid") + metadata.licences = licences + --print('[MDT] Updated offline QBCore licences:', licenseType, '=', licences[licenseType]) + end + end + + -- Update metadata + metadata.mdtLicenses = mdtLicenses + MySQL.update.await("UPDATE players SET metadata = ? WHERE citizenid = ?", { + json.encode(metadata), + characterId + }) + --print('[MDT] Database updated for offline player') + end + end +end + +---@param characterId number +---@param fine number +---@param source number +function Bridge.createInvoice(characterId, fine, source) + -- Get the police player who is creating the invoice + local policePlayer = QBCore.Functions.GetPlayer(source) + if not policePlayer then return false end + + -- Get the target player (can be offline) + local targetPlayer = QBCore.Functions.GetPlayerByCitizenId(characterId) + + if not targetPlayer then + -- Player is offline, still create invoice in database + local result = MySQL.query.await("SELECT charinfo FROM players WHERE citizenid = ?", {characterId}) + if result and result[1] then + -- Insert invoice into phone_invoices table + MySQL.insert('INSERT INTO phone_invoices (citizenid, amount, society, sender, sendercitizenid) VALUES (?, ?, ?, ?, ?)', { + characterId, + fine, + policePlayer.PlayerData.job.name, + policePlayer.PlayerData.charinfo.firstname .. " " .. policePlayer.PlayerData.charinfo.lastname, + policePlayer.PlayerData.citizenid + }) + + return true + end + return false + else + -- Player is online + if fine and fine > 0 then + -- Insert invoice into phone_invoices table + MySQL.insert('INSERT INTO phone_invoices (citizenid, amount, society, sender, sendercitizenid) VALUES (?, ?, ?, ?, ?)', { + targetPlayer.PlayerData.citizenid, + fine, + policePlayer.PlayerData.job.name, + policePlayer.PlayerData.charinfo.firstname .. " " .. policePlayer.PlayerData.charinfo.lastname, + policePlayer.PlayerData.citizenid + }) + + -- Refresh phone for online player + TriggerClientEvent('qb-phone:RefreshPhone', targetPlayer.PlayerData.source) + TriggerClientEvent('QBCore:Notify', targetPlayer.PlayerData.source, 'New Invoice Received') + + return true + end + return false + end +end + +---@param id number +---@param stolen boolean +---@param plate string +function Bridge.vehicleStolen(id, stolen, plate) + MySQL.query("UPDATE `player_vehicles` SET `stolen` = ? WHERE `id` = ?", {stolen and 1 or 0, id}) +end + +---@return table +function Bridge.getStolenVehicles() + local plates = {} + local result = MySQL.query.await("SELECT `plate` FROM `player_vehicles` WHERE `stolen` = 1") + for i = 1, #result do + local veh = result[i] + plates[#plates+1] = veh.plate + end + + local bolos = MySQL.query.await("SELECT `data` FROM `nd_mdt_bolos` WHERE `type` = 'vehicle'") + for i = 1, #bolos do + local veh = bolos[i] + local info = json.decode(veh.data) or {} + if info.plate then + plates[#plates+1] = info.plate + end + end + + return plates +end + +---@param characterId number +function Bridge.getPlayerImage(characterId) + local player = findCharacterById(characterId) + return player and player.metadata and player.metadata.img or "user.jpg" +end + +---@param characterId number +---@param key any +---@param value any +function Bridge.updatePlayerMetadata(source, characterId, key, value) + local player = QBCore.Functions.GetPlayer(source) + player.Functions.SetMetaData(key, value) +end + +function Bridge.getRecords(id) + local result = MySQL.query.await("SELECT records FROM nd_mdt_records WHERE `character` = ? LIMIT 1", {id}) + if not result or not result[1] then + return {}, false + end + return json.decode(result[1].records), true +end + +local function getPermsFromGroups(groups) + -- QBCore doesn't use groups like ND, it uses job system + -- This function might not be needed for QBCore but implementing for compatibility + if groups and groups.job then + return groups.job.name, groups.job + end + return nil, nil +end + +local function filterEmployeeSearch(player, metadata, search) + local toSearch = ("%s %s %s"):format( + (player.PlayerData.charinfo.firstname or ""):lower(), + (player.PlayerData.charinfo.lastname or ""):lower(), + (metadata.callsign and tostring(metadata.callsign) or ""):lower() + ) + + if toSearch:find(search:lower()) then + return true + end +end + +-- local function getPlayerSourceFromPlayers(players, id) +-- for src, info in pairs(players) do +-- if info.id == id then +-- return src +-- end +-- end +-- end + +function Bridge.viewEmployees(src, search) + local player = QBCore.Functions.GetPlayer(src) + if not config.policeAccess[player.PlayerData.job.name] then return {} end + + local employees = {} + local onlinePlayers = QBCore.Functions.GetQBPlayers() + + -- Check online players first + for _, onlinePlayer in pairs(onlinePlayers) do + if config.policeAccess[onlinePlayer.PlayerData.job.name] then + local metadata = onlinePlayer.PlayerData.metadata + if filterEmployeeSearch(onlinePlayer, metadata, search or "") then + employees[#employees+1] = { + source = onlinePlayer.PlayerData.source, + charId = onlinePlayer.PlayerData.citizenid, + first = onlinePlayer.PlayerData.charinfo.firstname, + last = onlinePlayer.PlayerData.charinfo.lastname, + img = metadata.img or "user.jpg", + callsign = metadata.callsign or "NO CALLSIGN", + job = onlinePlayer.PlayerData.job.name, + jobInfo = { + grade = onlinePlayer.PlayerData.job.grade.level, + name = onlinePlayer.PlayerData.job.grade.name, + label = onlinePlayer.PlayerData.job.label, + rankName = onlinePlayer.PlayerData.job.grade.name + }, + dob = onlinePlayer.PlayerData.charinfo.birthdate, + gender = onlinePlayer.PlayerData.charinfo.gender == 0 and "Male" or "Female", + phone = onlinePlayer.PlayerData.charinfo.phone + } + end + end + end + + -- Check offline players + local result = MySQL.query.await("SELECT * FROM players") + for i = 1, #result do + local info = result[i] + local charinfo = json.decode(info.charinfo) or {} + local job = json.decode(info.job) or {} + local metadata = json.decode(info.metadata) or {} + + if config.policeAccess[job.name] then + -- Check if player is not already added (online) + local alreadyAdded = false + for _, emp in pairs(employees) do + if emp.charId == info.citizenid then + alreadyAdded = true + break + end + end + + if not alreadyAdded then + local mockPlayer = { + PlayerData = { + charinfo = charinfo, + metadata = metadata + } + } + + if filterEmployeeSearch(mockPlayer, metadata, search or "") then + employees[#employees+1] = { + charId = info.citizenid, + first = charinfo.firstname or "", + last = charinfo.lastname or "", + img = metadata.img or "user.jpg", + callsign = metadata.callsign or "NO CALLSIGN", + job = job.name or "Unknown", + jobInfo = { + grade = job.grade and job.grade.level or 0, + name = job.grade and job.grade.name or "Unknown", + label = job.label or "Unknown", + rankName = job.grade and job.grade.name or "Unknown" + }, + dob = charinfo.birthdate or "", + gender = charinfo.gender == 0 and "Male" or "Female", + phone = charinfo.phone + } + end + end + end + end + + return employees +end + +function Bridge.employeeUpdateCallsign(src, charid, callsign) + local player = QBCore.Functions.GetPlayer(src) + if not player then + return false, "An issue occurred, try again later!" + end + + if not tonumber(callsign) then + return false, "Callsign must be a number!" + end + + callsign = tostring(callsign) + if not callsign then + return false, "Incorrect callsign" + end + + charid = tostring(charid) -- QBCore uses string citizenid + if not charid then + return false, "Employee not found!" + end + + -- Check if target player is online + local targetPlayer = QBCore.Functions.GetPlayerByCitizenId(charid) + if targetPlayer then + -- Player is online, update directly + targetPlayer.Functions.SetMetaData("callsign", callsign) + return callsign + else + -- Player is offline, update database + local result = MySQL.query.await("SELECT metadata FROM players WHERE citizenid = ?", {charid}) + if result and result[1] then + local metadata = json.decode(result[1].metadata) or {} + metadata.callsign = callsign + + MySQL.update.await("UPDATE players SET metadata = ? WHERE citizenid = ?", { + json.encode(metadata), + charid + }) + return callsign + else + return false, "Employee not found" + end + end +end + +function Bridge.updateEmployeeRank(src, update) + local player = QBCore.Functions.GetPlayer(src) + if not player then + return false, "An issue occurred, try again later!" + end + + local playerJob = player.PlayerData.job + local newRank = tonumber(update.newRank) + if not newRank then + return false, "Invalid rank!" + end + + local charid = tostring(update.charid) + if not charid then + return false, "Employee not found!" + end + + -- Check if target player is online + local targetPlayer = QBCore.Functions.GetPlayerByCitizenId(charid) + if targetPlayer then + -- Player is online, update directly + if targetPlayer.Functions.SetJob(update.job, newRank) then + return targetPlayer.PlayerData.job.grade.name + else + return false, "Failed to update rank!" + end + else + -- Player is offline, update database + local result = MySQL.query.await("SELECT job FROM players WHERE citizenid = ?", {charid}) + if result and result[1] then + local job = json.decode(result[1].job) or {} + job.grade = job.grade or {} + job.grade.level = newRank + + -- You might need to get the grade name from QBCore shared jobs config + -- For now, using a basic approach + job.grade.name = "Rank " .. newRank + + MySQL.update.await("UPDATE players SET job = ? WHERE citizenid = ?", { + json.encode(job), + charid + }) + return job.grade.name + else + return false, "Employee not found!" + end + end +end + +function Bridge.removeEmployeeJob(src, charid) + local player = QBCore.Functions.GetPlayer(src) + if not player then + return false, "An issue occurred, try again later!" + end + + charid = tostring(charid) + if not charid then + return false, "Employee not found!" + end + + -- Check if target player is online + local targetPlayer = QBCore.Functions.GetPlayerByCitizenId(charid) + if targetPlayer then + -- Player is online, set to unemployed + if targetPlayer.Functions.SetJob("unemployed", 0) then + return true + else + return false, "Failed to remove job!" + end + else + -- Player is offline, update database + local unemployedJob = { + name = "unemployed", + label = "Civilian", + payment = 10, + onduty = false, + isboss = false, + grade = { + name = "Freelancer", + level = 0, + isboss = false, + payment = 10 + }, + type = "none" + } + + MySQL.update.await("UPDATE players SET job = ? WHERE citizenid = ?", { + json.encode(unemployedJob), + charid + }) + return true + end +end + +function Bridge.invitePlayerToJob(src, target) + local player = QBCore.Functions.GetPlayer(src) + if not player.PlayerData.job.name then return end + + local targetPlayer = QBCore.Functions.GetPlayer(target) + targetPlayer.Functions.SetJob(player.PlayerData.job.name, 0) + return true +end + +function Bridge.ComparePlates(plate1, plate2) + return plate1:gsub("0", "O") == plate2:gsub("0", "O") +end + +if GetResourceState("qb-inventory") == "started" then + QBCore.Functions.CreateUseableItem("mdt", function(source, item) + local Player = QBCore.Functions.GetPlayer(source) + if not Player.Functions.GetItemByName(item.name) then return end + + TriggerClientEvent("ND_MDT:qb:client:useTablet", source) + end) +end + +if GetResourceState("qb-houses") == "started" then + RegisterCommand("sethouselocation", function(source, args, rawCommand) + local player = QBCore.Functions.GetPlayer(source) + if not config.policeAccess[player.PlayerData.job.name] then + TriggerClientEvent('QBCore:Notify', source, 'You do not have access to this command', 'error') + return + end + + if not args[1] then + TriggerClientEvent('QBCore:Notify', source, 'Usage: /sethouselocation [house name]', 'error') + return + end + + local houseName = string.lower(table.concat(args, " ")) + + local houseData = MySQL.query.await('SELECT coords FROM houselocations WHERE name = ?', {houseName}) + + if not houseData or #houseData == 0 then + TriggerClientEvent('QBCore:Notify', source, 'House not found: ' .. houseName, 'error') + return + end + + local coords = json.decode(houseData[1].coords) + if not coords or not coords.enter then + TriggerClientEvent('QBCore:Notify', source, 'Invalid house coordinates', 'error') + return + end + + TriggerClientEvent('ND_MDT:setWaypoint', source, coords.enter.x, coords.enter.y) + TriggerClientEvent('QBCore:Notify', source, 'Waypoint set for house: ' .. houseName, 'success') + end) +end + +return Bridge diff --git a/config/config.lua b/config/config.lua index 83283d4..af5dac4 100644 --- a/config/config.lua +++ b/config/config.lua @@ -14,7 +14,7 @@ config = { ["lsfd"] = true }, - + -- blip colors. vehicleBlips = { ["police"] = 3, @@ -49,5 +49,5 @@ config = { return 422 end end - + } diff --git a/fxmanifest.lua b/fxmanifest.lua index af2b90d..b751b45 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -38,6 +38,6 @@ client_scripts { dependencies { "oxmysql", - "ox_inventory", + --"ox_inventory", "ox_lib" } diff --git a/source/client.lua b/source/client.lua index 802e9ca..fca43dc 100644 --- a/source/client.lua +++ b/source/client.lua @@ -47,7 +47,7 @@ function OpenMDT(status) PlaySoundFrontend(-1, "DELETE", "HUD_DEATHMATCH_SOUNDSET", 1) end ----@return boolean display Is tablet Open +---@return boolean display Is tablet Open function isOpen() return display end @@ -252,7 +252,7 @@ RegisterNuiCallback("empoyeeAction", function(data, cb) newRank = input[1], job = job }) - + if success then cb(success) else @@ -277,7 +277,7 @@ RegisterNuiCallback("empoyeeAction", function(data, cb) if not input or not input[1] then return --OpenMDT(true) end - + local success, errorMessage = lib.callback.await("ND_MDT:employeeUpdateCallsign", false, charid, input[1]) if success then cb(tostring(success)) @@ -323,7 +323,7 @@ local function getNearbyPlayers() disabled = true } end - + return list end @@ -448,7 +448,7 @@ end) RegisterNUICallback("saveRecords", function(data) PlaySoundFrontend(-1, "PIN_BUTTON", "ATM_SOUNDS", 1) - + local data = { characterId = data.character, notes = data.notes, @@ -520,7 +520,7 @@ RegisterNetEvent("ND_MDT:update911Calls", function(emeregencyCalls, blipInfo, no TriggerServerEvent("ND_MDT:setUnitStatus", "In service", "10-8") return removeResponseBlipAndPoint() end - + if responseBlipCall > 0 and responseBlipCall ~= blipInfo.call then removeResponseBlipAndPoint() end @@ -552,7 +552,7 @@ RegisterNetEvent("ND_MDT:update911Calls", function(emeregencyCalls, blipInfo, no coords = coords, distance = 50 }) - + function point:onEnter() removeResponseBlipAndPoint() lib.notify({ diff --git a/source/server.lua b/source/server.lua index ee8811c..5feeda8 100644 --- a/source/server.lua +++ b/source/server.lua @@ -74,14 +74,19 @@ AddEventHandler("playerDropped", function() end) -- Remove unit from activeUnits if they change character without going 10-7. -AddEventHandler("ND:characterUnloaded", function(src) +-- AddEventHandler("ND:characterUnloaded", function(src) +-- if not activeUnits[src] then return end +-- activeUnits[src] = nil +-- TriggerClientEvent("ND_MDT:updateUnitStatus", -1, activeUnits) +-- end) +Bridge.onCharacterUnloaded(function(src) if not activeUnits[src] then return end activeUnits[src] = nil TriggerClientEvent("ND_MDT:updateUnitStatus", -1, activeUnits) end) -- This will just send all the current calls to the client. -lib.callback.register("ND_MDT:getUnitStatus", function(source) +lib.callback.register("ND_MDT:get911Calls", function(source) return emeregencyCalls end) @@ -209,7 +214,7 @@ RegisterNetEvent("ND_MDT:saveRecords", function(data) if fine > charge.fine then fine = charge.fine end - Bridge.createInvoice(data.characterId, fine) + Bridge.createInvoice(data.characterId, fine, src) end characterCharges[#characterCharges+1] = { @@ -230,14 +235,16 @@ RegisterNetEvent("ND_MDT:saveRecords", function(data) MySQL.insert("INSERT INTO nd_mdt_records (`character`, records) VALUES (?, ?)", {data.characterId, json.encode(records)}) end) -exports.ox_inventory:registerHook("createItem", function(payload) - local metadata = payload.metadata - if payload.item.weapon and metadata.registered then - local player = Bridge.getPlayerInfo(payload.inventoryId) - MySQL.insert("INSERT INTO `nd_mdt_weapons` (`character`, `weapon`, `serial`, `owner_name`) VALUES (?, ?, ?, ?)", {player.characterId, payload.item.label, metadata.serial, metadata.registered}) - end - return metadata -end) +if GetResourceState("ox_inventory") == "started" then + exports.ox_inventory:registerHook("createItem", function(payload) + local metadata = payload.metadata + if payload.item.weapon and metadata.registered then + local player = Bridge.getPlayerInfo(payload.inventoryId) + MySQL.insert("INSERT INTO `nd_mdt_weapons` (`character`, `weapon`, `serial`, `owner_name`) VALUES (?, ?, ?, ?)", {player.characterId, payload.item.label, metadata.serial, metadata.registered}) + end + return metadata + end) +end ---Save a weapon to the MDT weapons DB ---@param playerID number Player Server ID @@ -248,6 +255,14 @@ local function registerWeapon(playerID, weaponLabel, serialNumber) print('[^4WARNING^7] Missing parameters for ^8registerWeapon()^8', string.format("ID: %s, Label: %s, Serial: %s", tostring(playerID) or "nil", weaponLabel or "nil", serialNumber or "nil")) return end + + -- Check if weapon with this serial already exists in database + local existingWeapon = MySQL.query.await("SELECT id FROM nd_mdt_weapons WHERE serial = ?", {serialNumber}) + if existingWeapon and #existingWeapon > 0 then + -- print('[^3INFO^7] Weapon with serial ^8' .. serialNumber .. '^7 already exists in MDT database. Skipping registration.') + return + end + local player = Bridge.getPlayerInfo(playerID) MySQL.insert( "INSERT INTO `nd_mdt_weapons` (`character`, `weapon`, `serial`, `owner_name`) VALUES (?, ?, ?, ?)", @@ -258,6 +273,7 @@ local function registerWeapon(playerID, weaponLabel, serialNumber) player.firstName .. " " .. player.lastName } ) + -- print('[^2SUCCESS^7] Weapon ^8' .. weaponLabel .. '^7 with serial ^8' .. serialNumber .. '^7 registered to MDT database.') end exports("registerWeapon", registerWeapon) @@ -266,12 +282,12 @@ exports("registerWeapon", registerWeapon) lib.callback.register("ND_MDT:weaponSerialSearch", function(source, searchBy, search) local player = Bridge.getPlayerInfo(source) if not config.policeAccess[player.job] or not search then return false end - + local query = "SELECT * FROM nd_mdt_weapons WHERE serial RLIKE(?)" if searchBy == "owner" then query = "SELECT * FROM nd_mdt_weapons WHERE `character` = ?" end - + local weapons = {} local result = MySQL.query.await(query, {search}) for i=1, #result do @@ -321,7 +337,7 @@ RegisterNetEvent("ND_MDT:createBolo", function(data) local src = source local player = Bridge.getPlayerInfo(src) if not config.policeAccess[player.job] and not config.fireAccess[player.job] then return end - + if data.type == "person" and data.character then data.img = Bridge.getPlayerImage(data.character) end