Kartik Scripts

Tablet Extensions

Your own resources can add panels, pages and actions to the DOJ tablet without touching kartik-dojjob. A licensing, permits or business registry script can show its records on a citizen's profile and let DOJ staff act on them from the tablet.

One registration can give:

  • a panel on the citizen profile (People page)
  • its own page in the tablet menu, under More, with a search box
  • actions with a small form (dropdowns, dates, numbers, text, a citizen picker). Every action asks for a legal reason and can be linked to a court case. Your handler runs on the server, in your resource.

Everything is server side. The full reference is also at the top of bridge/integrations/server/extensions.lua (open file).

Who sees what

  • permission on the extension: the DOJ permission a grade needs to see the panel and page (default casesView).
  • permission on each action: the DOJ permission a grade needs to use it, for example licenses, permits or verdicts.
  • Grades are set in the Admin Panel and on the tablet's Staff page, see Permissions. A grade that can see the panel but has none of the action permissions gets a read-only view.
  • Optional canView(src) and canUse(src, ctx) functions let your resource add its own rules (for example only judges for judicial orders).
  • Buttons a player may not use are not sent to their tablet, and every action is checked again on the server before your handler runs.
  • The DOJ tablet is for DOJ staff only. Other departments can read your data with your own exports or their MDT.

Register

Register when your resource starts, and again when kartik-dojjob restarts:

local function register()
    exports['kartik-dojjob']:RegisterExtension('licensing', {
        label = 'Licensing',
        description = 'Licences, permits and judicial restrictions',
        icon = 'licence',          -- licence | permit | business | shield | file | gavel | car | weapon | scroll
        permission = 'casesView',  -- needed to see it

        -- panel on the citizen profile; return nil or false to hide it for this citizen
        profile = function(src, citizenid)
            return { records = GetRecordsFor(citizenid), empty = 'No licences on file' }
        end,

        -- optional own page; query is the search text ('' when empty)
        section = function(src, query)
            return { records = SearchRecords(query) }
        end,

        actions = {
            suspend = {
                label = 'Judicial suspension',
                permission = 'verdicts',
                scope = 'record',
                danger = true,
                requireCase = true,
                fields = {
                    { name = 'until', label = 'Suspended until', type = 'date', required = true },
                },
                handler = function(src, ctx)
                    -- look ctx.recordId up in YOUR data and check it belongs to ctx.citizenid
                    local ok = SuspendRecord(ctx.recordId, ctx.citizenid, ctx.values['until'], ctx.reason, ctx.caseId, ctx.official.name)
                    if not ok then return false, 'This licence is not active' end
                    return true, 'Licence suspended'
                end,
            },
        },
    })
end

AddEventHandler('onResourceStart', function(resource)
    if resource == 'kartik-dojjob' then register() end
end)

CreateThread(function()
    if GetResourceState('kartik-dojjob') == 'started' then register() end
end)

RegisterExtension returns true, or false, error (invalid_id, invalid_definition, invalid_permission, nothing_to_show). Problems are also printed in the server console. Registering the same id again replaces it. Registrations are removed when your resource stops.

Extension options

OptionTypeNotes
labelstringTitle of the panel and page
descriptionstring / nilSmall line under the title
iconstring / nillicence, permit, business, shield, file, gavel, car, weapon or scroll
permissionstringDOJ permission key needed to see it. Default casesView
canViewfunction(src) / nilReturn false to hide it from this player
profilefunction(src, citizenid) / nilData for the citizen profile panel
sectionfunction(src, query) / nilData for the extension's own page
actionstable / nil{ [actionId] = action }, up to 12

An extension needs profile, section or both.

Action options

OptionTypeNotes
labelstringButton text
permissionstringRequired. DOJ permission key needed to use it
scopestring'citizen': button on the profile panel. 'record': button on a record (list the action id in the record's actions). 'section': button on the extension's page. Default 'citizen'
dangerbooleanRed button
requireReasonbooleanDefault true: a legal reason must be written
requireCasebooleanDefault false: a case number is always optional, this makes it required
confirmstring / nilAsk "are you sure" with this text first
fieldstable / nilForm fields, see below (up to 20)
canUsefunction(src, ctx) / nilReturn false to refuse this player
handlerfunction(src, ctx)Required. Return true, message when done or false, message to refuse. The message is shown to the player

Fields

OptionNotes
nameKey in ctx.values (letters, numbers, _)
labelField label
typetext, textarea, number, date, select, checkbox or citizen
requiredtrue to make it required
optionsFor select: { { value = 'c1', label = 'Class 1' }, ... } or { 'c1', 'c2' }
min, maxFor number
placeholder, defaultOptional

Values arrive cleaned: date is a unix time (seconds), number a number, checkbox a boolean, select one of its options, citizen a citizen id that exists. Anything else in the form is dropped.

ctx

FieldNotes
actionAction id
citizenidThe profile's citizen, the record's citizenid on a page, or the first citizen field
recordIdThe record's id (record actions)
valuesForm values by field name
reasonLegal reason, or nil when not required and left empty
caseId, caseLinked court case (case = { id, title }), checked to exist
official{ source, name, identifier, job, grade, gradeLabel } of the DOJ member
atUnix time

The tablet sends citizenid and recordId, so always look the record up in your own data and check it belongs to that citizen before changing it.

When the handler returns true, the action goes to the DOJ audit log (and your log webhook). When a case was given, a line is added to the case transcript, for example Licensing - Judicial suspension (DL-1042): reason.

Data your profile / section returns

return {
    records = {
        {
            id = 'DL-1042',                 -- comes back as ctx.recordId
            title = 'Driver licence',
            subtitle = 'Class C',
            status = 'suspended',           -- active | suspended | revoked | expired | pending | approved | denied, or your own word
            tone = nil,                     -- optional colour: ok | minor | moderate | critical | neutral | accent
            citizenid = 'ABC12345',         -- optional: on a page, links to the citizen's profile
            citizen = 'John Doe',
            caseId = 12,                    -- optional: links to the case
            fields = { { label = 'Issued', value = '2026-10-01' }, { label = 'Issued by', value = 'COC' } },
            history = { { at = 1760000000, text = 'Suspended, case #12', by = 'Judge Smith' } },
            actions = { 'suspend', 'lift' }, -- record actions shown on this record
        },
    },
    summary = { { label = 'Businesses', value = '2' } }, -- optional chips under the title
    empty = 'No licences on file',                        -- optional
}

Up to 100 records, 20 fields and 30 history lines per record are shown.

Refresh and remove

exports['kartik-dojjob']:RefreshExtension()            -- open tablets reload extension panels and pages
exports['kartik-dojjob']:UnregisterExtension('licensing') -- only the resource that registered it

Call RefreshExtension() after your own data changes (for example when another department issues a licence) so DOJ tablets that are open show it straight away. Tablets also refresh after every extension action.

On this page