NX CreativeNX CreativeDocs
Scriptsnx_employeemanagement

Exports

Public exports registered by nx_employeemanagement.

nx_employeemanagement registers one public client export and three public server exports. The server exports let your other scripts post a row to a company's ledger, check a rank permission, and run a business's payroll. Hiring, firing, invoicing and day-to-day payroll go through the tablet UI.

Server

Company transactions

A wholesale app, a shop, a fuel station or any other script can add a row to a company's transaction list. The row shows in the tablet's Society Funds ledger and in nx_computer's Employees app under Company funds.

exports.nx_employeemanagement:AddCompanyTransactionserver

Post one row to a company's ledger. By default it only records the row, for when your script has already moved the money. Pass move = true to have the money moved too, through the same society account backend the tablet uses. A debit the account can't cover then fails cleanly and no row is written.

Parameters

  • jobstring— Job name of the company, for example "mechanic". Must be a framework job with grades, or a custom job with a society account. "unemployed" is refused.
  • tx.amountnumber— Positive amount. Rounded to Config.Payroll.MoneyDecimals (whole numbers on ESX by default). Capped by Config.Exports.Transactions.MaxAmount.
  • tx.labelstring— What the row says, up to 120 characters. Line breaks and tabs become spaces. tx.description is accepted as an alias.
  • tx.typestring— purchase, expense, fee, tax or withdraw (money out); sale, income or deposit (money in); refund, transfer or other (set tx.direction). Defaults to other.
  • tx.direction'in'|'out'|nil— Overrides the direction the type implies. Required for refund, transfer and other.
  • tx.referencestring|nil— Your own id for the row, such as an order number. Up to 64 characters from A-Z, 0-9 and - _ . : # /. A second call with the same reference from the same resource returns the first row and moves nothing, so retries are safe.
  • tx.actornumber|string|nil— The player behind it: a server id (resolved to their identifier and character name) or a framework identifier.
  • tx.actorNamestring|nil— Name shown on the row, up to 64 characters. Overrides the name resolved from tx.actor.
  • moveboolean|nil— true moves the money too. Default false: record only.

Returns

true, { id, balance, moved, duplicate, direction, amount } | false, errorCode
-- Wholesale app: the company pays for an order from its own account.
-- The money moves and the row is written together.
local ok, res = exports.nx_employeemanagement:AddCompanyTransaction('mechanic', {
    amount    = 1250,
    label     = ('Wholesale order #%s'):format(orderId),
    type      = 'purchase',
    reference = ('wholesale-%s'):format(orderId), -- safe to retry
    actor     = source,                           -- the employee who placed it
}, true)

if not ok then
    -- res is the error code, e.g. 'insufficient_funds'
    return TriggerClientEvent('ox_lib:notify', source, {
        type = 'error', description = 'The company cannot afford this order.'
    })
end
print(('order logged as row %d, company balance now %s'):format(res.id, res.balance))

If your script already moved the money itself, leave move out and only the row is written:

exports.nx_employeemanagement:AddCompanyTransaction('burgershot', {
    amount = 86.50,
    label  = 'Delivery fee: Vespucci supply run',
    type   = 'fee',
})

What each mode does

move = false (default)move = true
Ledger rowwrittenwritten
Company balanceuntouchedcredited or debited
Not enough moneynot checkedfalse, 'insufficient_funds', no row
Account usednonethe society backend in Config.Payroll.Society.Backend (nx_realbanking, esx_addonaccount, qb-banking, qb-management, Renewed-Banking, fd_banking, or the database table)

The row is written first and the money moves second. If the move fails, the row is deleted again, so a failed debit never leaves a ledger line.

With nx_realbanking as the backend, the label is passed as the bank transaction's reason and { source = 'employees', resource, category, reference } as its metadata. nx_realbanking keeps a transaction row only for accounts it owns itself. A society account stored in esx_addonaccount or qb-management shows the same balance in the bank, and this ledger is where the rows live.

Rows posted this way do not count toward Config.Payroll.Limits.WithdrawPerDayNonOwner. That cap is for bosses taking cash out, and a supplier order is not a cash withdrawal.

Any server resource can call this export unless you list the ones you trust in Config.Exports.Transactions.AllowedResources. With move = true it moves company money, so never call it straight from a client event without your own check that the player is allowed to spend for that company.

Error codes

CodeMeaning
not_allowed_callerNot called from a server resource, or the resource isn't in AllowedResources
ledger_disabledConfig.Exports.Transactions.Enabled = false
ledger_unavailableThe ledger columns are missing (the database migration failed on start; check the server console)
invalid_argsjob or tx has the wrong type, or tx.direction is not in/out
unknown_jobNo such job, or unemployed
society_unavailableA custom job with no society account, or the society backend is unreachable
invalid_amountNot a positive finite number
limit_exceededOver MaxAmount
invalid_labelEmpty, over 120 characters, or not valid UTF-8
invalid_typetx.type is not one of the listed types
direction_requiredrefund, transfer or other without tx.direction
invalid_referenceOver 64 characters or contains other characters
rate_limitedOver Config.RateLimits.ledger for this resource and job
insufficient_fundsmove = true and the company can't cover the debit
db_errorDatabase failure; nothing was written

Every posted row also fires the server event nx_employeemanagement:server:societyChanged with { job, id, direction, amount, category, resource, moved }. Rows are also written to the audit log and to the Discord webhook when one is configured.

Rank permissions

exports.nx_employeemanagement:HasRankPermissionserver

Whether a grade of a job holds a permission key from the Permissions tab. Boss grades always do. A non-boss grade has it only when it was granted there, and never while Config.Permissions.Enabled is false. Bad arguments return false, never nil.

Parameters

  • jobstring— Job name.
  • gradenumber— Grade level.
  • keystring— A key from Config.Permissions.Keys: 'employees', 'management', 'payroll', 'bossActions' or 'society'.

Returns

boolean
-- Only let grades with the society grant pay supplier invoices
local job = player.job
if not exports.nx_employeemanagement:HasRankPermission(job.name, job.grade, 'society') then
    return false, 'not_authorized'
end

Payroll runs

exports.nx_employeemanagement:RunPayrollserver

Pay every employee of one business, current or former, for the hours they are owed, right now. It is the same run AutoPay does on payday, with the same society debit, offline queueing and pay notifications. Any server resource can call it, so do your own permission check first (an admin command, a boss check).

Parameters

  • jobstring— Job name of the business. Must be a job in tablet payroll mode.

Returns

true, summary | false, reason
-- Admin command: pay out the mechanics now.
-- restricted = true, so only players with the command.paymechanics ACE can run it.
RegisterCommand('paymechanics', function()
    local ok, res = exports.nx_employeemanagement:RunPayroll('mechanic')
    if not ok then
        return print('payroll not run: ' .. res) -- e.g. 'no_employees'
    end
    print(('paid %d, queued %d, total %s (%s)'):format(res.paid, res.queued, res.total, res.status))
end, true)

On success summary holds:

FieldMeaning
runIdId of the payroll run
jobThe job that was paid
statusdone, partial (some employees could not be paid) or failed
employeesEmployees who had hours owed
paidPaid straight into an online player's account
queuedPaid to an offline player, delivered on their next login
settledOwed hours that came to $0 and were closed
insufficientNot paid because the society account ran short
failedNot paid because of an error
skippedNothing to pay after all
totalTotal amount paid and queued
ReasonMeaning
invalid_jobNot a valid job name, or unemployed
payroll_disabledConfig.Payroll.Enabled = false
framework_modeThe job is in 'framework' payroll mode, so the framework pays it
no_employeesNobody in this job has hours owed
errorThe run hit an error; details are in the server console

Employees who could not be paid keep their owed hours, so the next run or payday pays them.

Client

exports.nx_employeemanagement:useJobTabletclient

Open the employee management tablet UI for the local player. Boss access is resolved through Config.IsBossRank against the per-job Config.AllowedRanks table, and granular rank grants are applied on top. Non-boss employees see the self-service dashboard when Config.SelfService.Enabled is true. Bound to the jobtablet inventory item through the ox_inventory client export hook. The export opens the UI in every Config.Tablet.access.mode, including 'zone'. It is a no-op while Config.LBTablet.Enabled is true.

-- Open the tablet from any client script
exports.nx_employeemanagement:useJobTablet()

The same entry point is used by:

  • The jobtablet inventory item (via client.export in ox_inventory, or via CreateUseableItem on QBCore).
  • The /employeelist command and its Config.UIKey keybind.
  • The server-side nx_employeemanagement:openTablet event that the inventory bridge fires when the item is used on QB-style inventories.

LB Tablet mode

While Config.LBTablet.Enabled is true, the app inside LB Tablet is the only way to open the UI, and this export returns without doing anything. LB Tablet exposes no export for opening a specific app from Lua, so there is no equivalent call to forward to. Existing callers get a no-op rather than an error, so nothing breaks when you switch modes.

NX Docs