├── config.lua ├── fxmanifest.lua ├── bridge ├── server │ ├── esx.lua │ └── qb.lua └── client │ ├── esx.lua │ └── qb.lua ├── sv_paycheck.lua ├── cl_paycheck.lua └── README.md /config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | model = `a_f_y_business_02`, 3 | coords = vec4(241.54, 227.0, 105.29, 150.23), -- Default Pacific Bank. 4 | } 5 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | game 'gta5' 3 | 4 | author 'Randolio' 5 | description 'Paycheck System' 6 | 7 | shared_scripts { 8 | '@ox_lib/init.lua', 9 | 'config.lua' 10 | } 11 | 12 | client_scripts { 13 | 'bridge/client/**.lua', 14 | 'cl_paycheck.lua' 15 | } 16 | 17 | server_scripts { 18 | '@oxmysql/lib/MySQL.lua', 19 | 'bridge/server/**.lua', 20 | 'sv_paycheck.lua' 21 | } 22 | 23 | lua54 'yes' 24 | -------------------------------------------------------------------------------- /bridge/server/esx.lua: -------------------------------------------------------------------------------- 1 | if GetResourceState('es_extended') ~= 'started' then return end 2 | 3 | local ESX = exports['es_extended']:getSharedObject() 4 | 5 | function GetPlayer(id) 6 | return ESX.GetPlayerFromId(id) 7 | end 8 | 9 | function DoNotification(src, text, nType) 10 | TriggerClientEvent('esx:showNotification', src, text, nType) 11 | end 12 | 13 | function GetPlyIdentifier(xPlayer) 14 | return xPlayer.identifier 15 | end 16 | 17 | function GetByIdentifier(cid) 18 | return ESX.GetPlayerFromIdentifier(cid) 19 | end 20 | 21 | function GetSourceFromIdentifier(cid) 22 | local xPlayer = ESX.GetPlayerFromIdentifier(cid) 23 | return xPlayer and xPlayer.source or false 24 | end 25 | 26 | function GetCharacterName(xPlayer) 27 | return xPlayer.getName() 28 | end 29 | 30 | function AddMoney(xPlayer, account, amount) 31 | if account == 'cash' then account = 'money' end 32 | xPlayer.addAccountMoney(account, amount, "paycheck-withdraw") 33 | end -------------------------------------------------------------------------------- /bridge/client/esx.lua: -------------------------------------------------------------------------------- 1 | if GetResourceState('es_extended') ~= 'started' then return end 2 | 3 | local ESX = exports['es_extended']:getSharedObject() 4 | local PlayerData = {} 5 | 6 | RegisterNetEvent('esx:playerLoaded', function(xPlayer) 7 | PlayerData = xPlayer 8 | ESX.PlayerLoaded = true 9 | OnPlayerLoaded() 10 | end) 11 | 12 | RegisterNetEvent('esx:onPlayerLogout', function() 13 | table.wipe(PlayerData) 14 | ESX.PlayerLoaded = false 15 | OnPlayerUnload() 16 | end) 17 | 18 | AddEventHandler('onResourceStart', function(res) 19 | if GetCurrentResourceName() ~= res or not ESX.PlayerLoaded then return end 20 | PlayerData = ESX.PlayerData 21 | OnPlayerLoaded() 22 | end) 23 | 24 | AddEventHandler('esx:setPlayerData', function(key, value) 25 | PlayerData[key] = value 26 | end) 27 | 28 | function hasPlyLoaded() 29 | return ESX.PlayerLoaded 30 | end 31 | 32 | function DoNotification(text, nType) 33 | ESX.ShowNotification(text, nType) 34 | end 35 | -------------------------------------------------------------------------------- /bridge/client/qb.lua: -------------------------------------------------------------------------------- 1 | if GetResourceState('qb-core') ~= 'started' then return end 2 | 3 | local QBCore = exports['qb-core']:GetCoreObject() 4 | local PlayerData = {} 5 | 6 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() 7 | PlayerData = QBCore.Functions.GetPlayerData() 8 | OnPlayerLoaded() 9 | end) 10 | 11 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() 12 | table.wipe(PlayerData) 13 | OnPlayerUnload() 14 | end) 15 | 16 | RegisterNetEvent('QBCore:Player:SetPlayerData', function(val) 17 | PlayerData = val 18 | end) 19 | 20 | AddEventHandler('onResourceStart', function(res) 21 | if GetCurrentResourceName() ~= res or not LocalPlayer.state.isLoggedIn then return end 22 | PlayerData = QBCore.Functions.GetPlayerData() 23 | OnPlayerLoaded() 24 | end) 25 | 26 | function hasPlyLoaded() 27 | return LocalPlayer.state.isLoggedIn 28 | end 29 | 30 | function DoNotification(text, nType) 31 | QBCore.Functions.Notify(text, nType) 32 | end 33 | -------------------------------------------------------------------------------- /bridge/server/qb.lua: -------------------------------------------------------------------------------- 1 | if GetResourceState('qb-core') ~= 'started' then return end 2 | 3 | local QBCore = exports['qb-core']:GetCoreObject() 4 | 5 | function GetPlayer(id) 6 | return QBCore.Functions.GetPlayer(id) 7 | end 8 | 9 | function DoNotification(src, text, nType) 10 | TriggerClientEvent('QBCore:Notify', src, text, nType) 11 | end 12 | 13 | function GetPlyIdentifier(Player) 14 | return Player.PlayerData.citizenid 15 | end 16 | 17 | function GetByIdentifier(cid) 18 | return QBCore.Functions.GetPlayerByCitizenId(cid) 19 | end 20 | 21 | function GetSourceFromIdentifier(cid) 22 | local Player = QBCore.Functions.GetPlayerByCitizenId(cid) 23 | return Player and Player.PlayerData.source or false 24 | end 25 | 26 | function GetCharacterName(Player) 27 | return Player.PlayerData.charinfo.firstname.. ' ' ..Player.PlayerData.charinfo.lastname 28 | end 29 | 30 | function AddMoney(Player, account, amount) 31 | Player.Functions.AddMoney(account, amount, "paycheck-withdraw") 32 | end -------------------------------------------------------------------------------- /sv_paycheck.lua: -------------------------------------------------------------------------------- 1 | local function AddToPaycheck(cid, amount) 2 | if not cid or not amount then return end 3 | 4 | MySQL.update.await([[ 5 | INSERT INTO paychecks (citizenid, amount) 6 | VALUES (?, ?) 7 | ON DUPLICATE KEY UPDATE amount = amount + ? 8 | ]], {cid, amount, amount}) 9 | 10 | local result = MySQL.single.await('SELECT amount FROM paychecks WHERE citizenid = ?', {cid}) 11 | if not result then return end 12 | 13 | local src = GetSourceFromIdentifier(cid) 14 | if src then 15 | DoNotification(src, ('$%s was added to your paycheck. New Total: $%s'):format(amount, result.amount), 'success') 16 | end 17 | end 18 | exports('AddToPaycheck', AddToPaycheck) 19 | 20 | lib.callback.register('randol_paycheck:server:withdraw', function(source, amount, accountType) 21 | local src = source 22 | local Player = GetPlayer(src) 23 | local cid = GetPlyIdentifier(Player) 24 | local result = MySQL.single.await('SELECT amount FROM paychecks WHERE citizenid = ?', {cid}) 25 | 26 | if not result or not result.amount then 27 | DoNotification(src, 'No available funds in your paycheck.', 'error') 28 | return false 29 | end 30 | 31 | if tonumber(result.amount) < amount then 32 | DoNotification(src, 'You don\'t have this much in your paycheck.', 'error') 33 | return false 34 | end 35 | 36 | result.amount -= amount 37 | MySQL.update.await('UPDATE paychecks SET amount = ? WHERE citizenid = ?', {result.amount, cid}) 38 | 39 | if accountType == 'cash' then 40 | AddMoney(Player, 'cash', amount) 41 | DoNotification(src, ('You withdrew $%s from your paycheck into your wallet.'):format(amount), 'success') 42 | else 43 | AddMoney(Player, 'bank', amount) 44 | DoNotification(src, ('You withdrew $%s from your paycheck into your bank account.'):format(amount), 'success') 45 | end 46 | return true 47 | end) 48 | 49 | lib.callback.register('randol_paycheck:server:checkPaycheck', function(source) 50 | local src = source 51 | local Player = GetPlayer(src) 52 | local cid = GetPlyIdentifier(Player) 53 | local result = MySQL.single.await('SELECT * FROM paychecks WHERE citizenid = ?', {cid}) 54 | local paycheckAmount = 0 55 | if result then 56 | paycheckAmount = result.amount 57 | else 58 | MySQL.insert.await('INSERT INTO paychecks (citizenid, amount) VALUE (?, ?)', {cid, 0}) 59 | end 60 | return paycheckAmount 61 | end) 62 | 63 | AddEventHandler('onResourceStart', function(resource) 64 | if resource == GetCurrentResourceName() then 65 | MySQL.query.await([[ 66 | CREATE TABLE IF NOT EXISTS paychecks ( 67 | citizenid VARCHAR(100) NOT NULL, 68 | amount INT DEFAULT 0, 69 | PRIMARY KEY (citizenid) 70 | ); 71 | ]]) 72 | end 73 | end) 74 | -------------------------------------------------------------------------------- /cl_paycheck.lua: -------------------------------------------------------------------------------- 1 | local Config = lib.require('config') 2 | local PC_PED, initZone 3 | local oxtarget = GetResourceState('ox_target') == 'started' 4 | 5 | local function targetLocalEntity(entity, options, distance) 6 | if oxtarget then 7 | for _, option in ipairs(options) do 8 | option.distance = distance 9 | option.onSelect = option.action 10 | option.action = nil 11 | end 12 | exports.ox_target:addLocalEntity(entity, options) 13 | else 14 | exports['qb-target']:AddTargetEntity(entity, { options = options, distance = distance }) 15 | end 16 | end 17 | 18 | local function InputWithdraw(amount) 19 | local response = lib.inputDialog('Withdrawal', { 20 | { type = 'number', label = 'How much?', icon = 'fa-solid fa-hand-pointer', description = ('Input an amount to withdraw. Balance: $%s'):format(amount), required = true }, 21 | { type = 'select', label = 'Account', required = true, icon = 'fa-solid fa-wallet', options = { 22 | { value = 'cash', label = 'Cash' }, 23 | { value = 'bank', label = 'Bank' }, 24 | }} 25 | }) 26 | if not response then return end 27 | local inputAmt = response[1] 28 | local accountType = response[2] 29 | 30 | if inputAmt < 1 then return DoNotification('Amount needs to be more than 0.', 'error') end 31 | if inputAmt > amount then return DoNotification('You don\'t have this much in your paycheck..', 'error') end 32 | 33 | local success = lib.callback.await('randol_paycheck:server:withdraw', false, inputAmt, accountType) 34 | if success then 35 | lib.playAnim(cache.ped, 'friends@laf@ig_5', 'nephew', 8.0, -8.0, -1, 49, 0, false, false, false) 36 | Wait(2000) 37 | ClearPedTasks(cache.ped) 38 | end 39 | end 40 | 41 | local function viewPaycheck() 42 | local paycheckAmount = lib.callback.await('randol_paycheck:server:checkPaycheck', true) 43 | lib.registerContext({ 44 | id = 'view_pc', 45 | title = ('Paycheck: $%s'):format(paycheckAmount), 46 | options = { 47 | { 48 | title = 'Withdraw', 49 | description = 'Withdraw money from your paycheck.', 50 | icon = 'fa-solid fa-money-check-dollar', 51 | onSelect = function() 52 | InputWithdraw(tonumber(paycheckAmount)) 53 | end, 54 | }, 55 | } 56 | }) 57 | lib.showContext('view_pc') 58 | end 59 | 60 | local function removePed() 61 | if not DoesEntityExist(PC_PED) then return end 62 | if oxtarget then 63 | exports.ox_target:removeLocalEntity(PC_PED, 'View Paycheck') 64 | else 65 | exports['qb-target']:RemoveTargetEntity(PC_PED, 'View Paycheck') 66 | end 67 | DeleteEntity(PC_PED) 68 | PC_PED = nil 69 | end 70 | 71 | local function spawnPed() 72 | lib.requestModel(Config.model, 10000) 73 | PC_PED = CreatePed(0, Config.model, Config.coords, false, false) 74 | SetEntityAsMissionEntity(PC_PED) 75 | SetPedFleeAttributes(PC_PED, 0, 0) 76 | SetBlockingOfNonTemporaryEvents(PC_PED, true) 77 | SetEntityInvincible(PC_PED, true) 78 | FreezeEntityPosition(PC_PED, true) 79 | SetPedDefaultComponentVariation(PC_PED) 80 | SetModelAsNoLongerNeeded(Config.model) 81 | lib.playAnim(PC_PED, 'mp_prison_break', 'hack_loop', 8.0, -8.0, -1, 1, 0.0, 0, 0, 0) 82 | targetLocalEntity(PC_PED, { 83 | { 84 | icon = 'fa-solid fa-money-check-dollar', 85 | label = 'View Paycheck', 86 | action = function() 87 | lib.playAnim(cache.ped, 'friends@laf@ig_5', 'nephew', 8.0, -8.0, -1, 49, 0, false, false, false) 88 | if lib.progressCircle({ 89 | duration = 2500, 90 | position = 'bottom', 91 | label = 'Viewing paycheck..', 92 | useWhileDead = true, 93 | canCancel = false, 94 | disable = { move = true, car = true, mouse = false, combat = true, }, 95 | }) then 96 | ClearPedTasks(cache.ped) 97 | viewPaycheck() 98 | end 99 | end, 100 | }, 101 | }, 4.5) 102 | end 103 | 104 | local function paycheckZone() 105 | initZone = lib.points.new({ coords = Config.coords.xyz, distance = 50, onEnter = spawnPed, onExit = removePed, }) 106 | end 107 | 108 | function OnPlayerLoaded() 109 | paycheckZone() 110 | end 111 | 112 | function OnPlayerUnload() 113 | if initZone then initZone:remove() end 114 | removePed() 115 | end 116 | 117 | AddEventHandler('onResourceStop', function(resourceName) 118 | if GetCurrentResourceName() == resourceName then 119 | if initZone then initZone:remove() end 120 | removePed() 121 | end 122 | end) 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | [ox_lib](https://github.com/overextended/ox_lib/releases) 4 | 5 | ## Showcase 6 | 7 | [showcase](https://streamable.com/t7czpi) 8 | 9 | # QBCore Install. 10 | 11 | **IF USING THE LATEST QBCORE UPDATE THAT MOVED SOCIETY FUNDS TO QB-BANKING, Go to qb-core/server/functions.lua and replace PaycheckInterval() code with mine below.** 12 | 13 | ```lua 14 | function PaycheckInterval() 15 | if next(QBCore.Players) then 16 | for _, Player in pairs(QBCore.Players) do 17 | if Player then 18 | local payment = QBShared.Jobs[Player.PlayerData.job.name]['grades'][tostring(Player.PlayerData.job.grade.level)].payment 19 | if not payment then payment = Player.PlayerData.job.payment end 20 | if Player.PlayerData.job and payment > 0 and (QBShared.Jobs[Player.PlayerData.job.name].offDutyPay or Player.PlayerData.job.onduty) then 21 | if QBCore.Config.Money.PayCheckSociety then 22 | local account = exports['qb-banking']:GetAccountBalance(Player.PlayerData.job.name) 23 | if account ~= 0 then -- Checks if player is employed by a society 24 | if account < payment then -- Checks if company has enough money to pay society 25 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('error.company_too_poor'), 'error') 26 | else 27 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, payment) 28 | exports['qb-banking']:RemoveMoney(Player.PlayerData.job.name, payment, 'Employee Paycheck') 29 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', {value = payment})) 30 | end 31 | else 32 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, payment) 33 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', {value = payment})) 34 | end 35 | else 36 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, payment) 37 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', {value = payment})) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | SetTimeout(QBCore.Config.Money.PayCheckTimeOut * (60 * 1000), PaycheckInterval) 44 | end 45 | ``` 46 | 47 | **IF STILL USING QB-MANAGEMENT TO HANDLE SOCIETY FUNDS, Go to qb-core/server/functions.lua and replace PaycheckInterval() code with mine below.** 48 | 49 | ```lua 50 | function PaycheckInterval() 51 | if next(QBCore.Players) then 52 | for _, Player in pairs(QBCore.Players) do 53 | if Player then 54 | local payment = QBShared.Jobs[Player.PlayerData.job.name]['grades'][tostring(Player.PlayerData.job.grade.level)].payment 55 | if not payment then payment = Player.PlayerData.job.payment end 56 | if Player.PlayerData.job and payment > 0 and (QBShared.Jobs[Player.PlayerData.job.name].offDutyPay or Player.PlayerData.job.onduty) then 57 | if QBCore.Config.Money.PayCheckSociety then 58 | local account = exports['qb-management']:GetAccount(Player.PlayerData.job.name) 59 | if account ~= 0 then -- Checks if player is employed by a society 60 | if account < payment then -- Checks if company has enough money to pay society 61 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('error.company_too_poor'), 'error') 62 | else 63 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, payment) 64 | exports['qb-management']:RemoveMoney(Player.PlayerData.job.name, payment) 65 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', {value = payment})) 66 | end 67 | else 68 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, payment) 69 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', {value = payment})) 70 | end 71 | else 72 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, payment) 73 | TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', {value = payment})) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | SetTimeout(QBCore.Config.Money.PayCheckTimeOut * (60 * 1000), PaycheckInterval) 80 | end 81 | ``` 82 | 83 | # ESX Install. 84 | 85 | **Go to es_extended/server/paycheck.lua and replace the StartPayCheck() function with mine below.** 86 | 87 | ```lua 88 | function StartPayCheck() 89 | CreateThread(function() 90 | while true do 91 | Wait(Config.PaycheckInterval) 92 | 93 | for player, xPlayer in pairs(ESX.Players) do 94 | local job = xPlayer.job.grade_name 95 | local salary = xPlayer.job.grade_salary 96 | 97 | if salary > 0 then 98 | if job == 'unemployed' then -- unemployed 99 | exports.randol_paycheck:AddToPaycheck(xPlayer.identifier, salary) 100 | TriggerClientEvent('esx:showAdvancedNotification', player, TranslateCap('bank'), TranslateCap('received_paycheck'), TranslateCap('received_help', salary), 101 | 'CHAR_BANK_MAZE', 9) 102 | elseif Config.EnableSocietyPayouts then -- possibly a society 103 | TriggerEvent('esx_society:getSociety', xPlayer.job.name, function(society) 104 | if society ~= nil then -- verified society 105 | TriggerEvent('esx_addonaccount:getSharedAccount', society.account, function(account) 106 | if account.money >= salary then -- does the society money to pay its employees? 107 | exports.randol_paycheck:AddToPaycheck(xPlayer.identifier, salary) 108 | account.removeMoney(salary) 109 | 110 | TriggerClientEvent('esx:showAdvancedNotification', player, TranslateCap('bank'), TranslateCap('received_paycheck'), 111 | TranslateCap('received_salary', salary), 'CHAR_BANK_MAZE', 9) 112 | else 113 | TriggerClientEvent('esx:showAdvancedNotification', player, TranslateCap('bank'), '', TranslateCap('company_nomoney'), 'CHAR_BANK_MAZE', 1) 114 | end 115 | end) 116 | else -- not a society 117 | exports.randol_paycheck:AddToPaycheck(xPlayer.identifier, salary) 118 | TriggerClientEvent('esx:showAdvancedNotification', player, TranslateCap('bank'), TranslateCap('received_paycheck'), TranslateCap('received_salary', salary), 119 | 'CHAR_BANK_MAZE', 9) 120 | end 121 | end) 122 | else -- generic job 123 | exports.randol_paycheck:AddToPaycheck(xPlayer.identifier, salary) 124 | TriggerClientEvent('esx:showAdvancedNotification', player, TranslateCap('bank'), TranslateCap('received_paycheck'), TranslateCap('received_salary', salary), 125 | 'CHAR_BANK_MAZE', 9) 126 | end 127 | end 128 | end 129 | end 130 | end) 131 | end 132 | ``` 133 | 134 | QBOX Install - Navigate to this line: https://github.com/Qbox-project/qbx_core/blob/main/config/server.lua#L131 135 | 136 | ```lua 137 | sendPaycheck = function (player, payment) 138 | exports.randol_paycheck:AddToPaycheck(player.PlayerData.citizenid, payment) 139 | Notify(player.PlayerData.source, locale('info.received_paycheck', payment)) 140 | end, 141 | ``` 142 | 143 | # Export 144 | 145 | The export below can be used to insert money into the paycheck rather than adding it into a player's bank/cash. You must implement these yourself. 146 | 147 | Example: QBCore 148 | 149 | ```lua 150 | local Player = QBCore.Functions.GetPlayer(source) 151 | local amount = 450 152 | exports.randol_paycheck:AddToPaycheck(Player.PlayerData.citizenid, amount) 153 | ``` 154 | 155 | Example: ESX 156 | 157 | ```lua 158 | local xPlayer = ESX.GetPlayerFromId(source) 159 | local amount = 450 160 | exports.randol_paycheck:AddToPaycheck(xPlayer.identifier, amount) 161 | ``` --------------------------------------------------------------------------------