> For the complete documentation index, see [llms.txt](https://wiki.qfdevelopers.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.qfdevelopers.com/scripts/editor-4/configuration.md).

# Configuration

## Languages

The language of the tablet is set inside `Config.NUI`.

* `en-US`: English
* `fr-FR`: French
* `pl-PL`: Polish
* `de-DE`: German
* `es-ES`: Spanish
* `pt-PT`: Portuguese
* `ja-JP`: Japanese
* `it-IT`: Italian
* `ko-KR`: Korean

```lua
Config.NUI = {
    defaultLangCode = "en-US",
    -- Languages offered in the tablet's own language switcher.
    -- Remove entries you do not want players to be able to pick.
    langs = {
        { code = "en-US", name = "English", iconUrl = "https://.../gb.webp" },
        { code = "pl-PL", name = "Polski",  iconUrl = "https://.../pl.webp" },
    }
}
```

### Commands & Keybinds <a href="#user-content-commands-keybinds" id="user-content-commands-keybinds"></a>

**General**

* `/qf_mdt_police_v2` (DELETE): Toggle the MDT Dashboard.
* `/qf_mdt_police_v2badge` (B): Show/Hide Officer Badge. `/badge` works as a short alias.
* `/bodycam`: Toggle the bodycam overlay. `/showbodycam` and `/hidebodycam` force one state.

**Dispatch**

* `/qf_mdt_police_v2_fastgps` (E): Set the GPS to the latest dispatch alert.
* `/qf_mdt_police_v2_dispatch` (HOME): Open the MDT straight on the dispatch page.
* `/panic`: Put a critical 10-13 on the board at your exact position. Takes an optional note and can be bound to a key.
* `/location`: Put a routine 10-20 on the board. Same options.
* `/911`: The civilian emergency line. `/911 anon <what happened>` hides the caller.

{% hint style="warning" %}
`/panic`, `/location` and `/911` are **opt-in** and stay unregistered until `Config.Dispatch.OfficerCommands` and `Config.Dispatch.CitizenCalls` exist in your config. Plenty of servers already own those command names.
{% endhint %}

**Camera Mode**

* `/qf_mdt_police_v2_capturephoto` (ENTER): Start capturing a photo.
* `/qf_mdt_police_v2_blocknuifocus` (L-ALT): Block the NUI focus.
* `/qf_mdt_police_v2_cameratakephoto` (SPACE): Take the photo.
* `/qf_mdt_police_v2_cameratogglefocus` (L-ALT): Toggle mouse focus.
* `/qf_mdt_police_v2_cameratoggleflashlight` (F): Toggle camera flashlight.
* `/qf_mdt_police_v2_cameratoggleside` (R): Switch between Front/Back camera.

**Radar**

* `/qf_mdt_police_v2radartoggle` (U): Toggle the entire Radar system.
* **Front Radar Settings**
  * `/qf_mdt_police_v2radarshowfront` (F5): Show Front Radar.
  * `/qf_mdt_police_v2radarhidefront` (F6): Hide Front Radar
  * `/qf_mdt_police_v2radartoggleinteractfront` (PGUP): Lock/Unlock Front Radar target
* **Back Radar Settings**
  * `/qf_mdt_police_v2radarshowback` (F7): Show Back Radar.
  * `/qf_mdt_police_v2radarhideback` (F8): Hide Back Radar
  * `/qf_mdt_police_v2radartoggleinteractback` (PGDN): Lock/Unlock Back Radar target

{% hint style="info" %} <mark style="color:$info;">**Note:**</mark> <mark style="color:$info;">Both the keys and the command names come from</mark> <mark style="color:$info;">`Config.ToggleMDT`</mark><mark style="color:$info;">,</mark> <mark style="color:$info;">`Config.Camera`</mark><mark style="color:$info;">,</mark> <mark style="color:$info;">`Config.Badge`</mark><mark style="color:$info;">,</mark> <mark style="color:$info;">`Config.Radar`</mark> <mark style="color:$info;">and</mark> <mark style="color:$info;">`Config.Dispatch`</mark> <mark style="color:$info;">in</mark> <mark style="color:$info;">`config/config.lua`</mark> <mark style="color:$info;">— the names above are the defaults.</mark>
{% endhint %}

#### Running several tablets side by side (`Config.Base`) <a href="#user-content-config-base" id="user-content-config-base"></a>

Every module stores its data in its own MySQL table, named after the resource (`qf_mdt_police_v2_*`). `Config.Base` lets you point any module at a **different** table, which is how you run more than one tablet at the same time — e.g. `qf_mdt_police_v2` plus a sheriff copy — each with its own events, exports and faction logic, while sharing selected data.

```lua
-- Make the sheriff tablet share the police fines, tags, records, warrants,
-- cases and evidences, but keep its own officers, announcements and radio codes.
Config.Base = {
    fines            = "qf_mdt_police_v2_fines",
    tags             = "qf_mdt_police_v2_tags",
    entityNotes      = "qf_mdt_police_v2_entity_notes",
    mugshots         = "qf_mdt_police_v2_mugshots",
    warrants         = "qf_mdt_police_v2_warrants",
    warrantOfficers  = "qf_mdt_police_v2_warrant_officers",
    warrantCitizens  = "qf_mdt_police_v2_warrant_citizens",
    cases            = "qf_mdt_police_v2_cases",
    caseOfficers     = "qf_mdt_police_v2_case_officers",
    caseCitizens     = "qf_mdt_police_v2_case_citizens",
    evidences        = "qf_mdt_police_v2_evidences",
    evidenceOfficers = "qf_mdt_police_v2_evidence_officers",
    evidenceCitizens = "qf_mdt_police_v2_evidence_citizens",
}
```

{% hint style="warning" %}
Anything you leave out keeps its own default table, so that data stays private to the faction. When a table **is** shared, every tablet pointing at it must use the same value, and related tables have to be shared together:

* `warrants` ↔ `warrantOfficers` + `warrantCitizens`
* `cases` ↔ `caseOfficers` + `caseCitizens`
* `evidences` ↔ `evidenceOfficers` + `evidenceCitizens`
  {% endhint %}

### Several departments on one tablet (`Config.Jobs`) <a href="#user-content-config-jobs" id="user-content-config-jobs"></a>

One resource can serve LSPD, Highway Patrol and BCSO at the same time, each with its own name, colour, ranks, society and radio codes.

With a single job nothing changes and there is nothing to configure:

```lua
Config.Jobs = { ['police'] = true }
```

To describe a department, replace `true` with a table. Every field is optional and falls back to the matching global setting, so you only write down what actually differs.

```lua
Config.Jobs = {
    ['police'] = {
        primary = true,              -- owns every row written before this update
    },
    ['hwp'] = {
        fractionName     = "HWP",
        fractionFullName = "Highway Patrol",
        title            = "MDT HWP",
        color            = { 250, 170, 20 },
        society          = { name = "society_hwp", jobname = "hwp", label = "HWP" },
        grades           = { { id = 0, name = "Trooper" }, { id = 1, name = "Sergeant" } },
    },
}
```

The full list of fields is documented above `Config.Jobs` in `config/config.lua`: `fractionName`, `fractionFullName`, `title`, `color`, `primaryColor`, `logo`, `grades`, `requiredGrades`, `gpsLostMinGrade`, `createCallGrades`, `society`, `officers`, `billingLimits`, `bodyCam`, `dispatchUnits`, `vehicles`, `stations`, `vCodesFaction`, `sharedWith`.

{% hint style="warning" %}
A department whose ladder is not 0–4 needs its own `requiredGrades`. A threshold written for five ranks lands somewhere else entirely on ten.

```lua
['hwp'] = {
    grades         = { { id = 0, name = "Trooper" }, { id = 1, name = "Sergeant" } },
    requiredGrades = { officers = { hire = 1, fire = 1 } },
},
```

{% endhint %}

#### What is shared and what is not <a href="#user-content-jobs-scope" id="user-content-jobs-scope"></a>

| Module                                                            | Scope                                                                 |
| ----------------------------------------------------------------- | --------------------------------------------------------------------- |
| Citizens, fines, tags, notes, mugshots, warrants, cases, evidence | shared by every department                                            |
| Announcements, radio codes, gallery, audit log                    | per department (`Config.PerJobData`)                                  |
| Dispatch — calls, patrols, units, actions, convoy, archive        | shared by default (`Config.PerJobData.dispatch`)                      |
| Officer list, officers on the map, "No GPS" list                  | own department only (`sharedWith` opens it up)                        |
| Society and invoices                                              | per department — a job without its own `society` gets `society_<job>` |

#### Own units and vehicles <a href="#user-content-jobs-units-vehicles" id="user-content-jobs-units-vehicles"></a>

A department can bring its own unit callsigns and its own vehicle list. Both are offered when creating a patrol, an action unit or a convoy unit, and both fall back to the global setting when a department does not define them.

```lua
Config.Jobs = {
    ['police'] = {
        primary = true,
        dispatchUnits = { 'pu1', 'pu2', 'mary' },
        vehicles = {
            { name = "vic11", label = "Crown Victoria 2011", unit = { "pu1", "pu2" } },
            { name = "tahoe", label = "Chevrolet Tahoe",     unit = { "pu1", "pu2" } },
        },
    },
    ['hwp'] = {
        dispatchUnits = { 'hp1', 'hp2' },
        vehicles = {
            { name = "dodge",   label = "Charger Highway Patrol" },
            { name = "police3", label = "Interceptor" },
        },
    },
    ['bcso'] = {},   -- no entry, so it uses Config.Dispatch.Units and Config.Vehicles
}
```

An entry with no `unit` key is offered to every unit. The label is read from the **department's own** list, so two departments can run the same model under different names.

{% hint style="warning" %}
The `unit` keys on a department's vehicles have to match that department's own `dispatchUnits`. Give Highway Patrol the callsigns `hp1` and `hp2` but leave its vehicles pointing at `pu1`, and the form has nothing to offer for either unit. The tablet says so rather than showing an empty dropdown, but the fix is in the config.
{% endhint %}

{% hint style="info" %}
Which vehicles are drawn on the live map is a separate question. A car counts as a service vehicle if it appears in **any** department's list, so a Highway Patrol cruiser still shows up for an LSPD dispatcher. Who sees whom is decided by `sharedWith`, not by the vehicle list.
{% endhint %}

#### Letting departments see each other (`sharedWith`) <a href="#user-content-sharedwith" id="user-content-sharedwith"></a>

`sharedWith` is **directional**. This lets LSPD see Highway Patrol officers, not the other way round:

```lua
Config.Jobs = {
    ['police'] = { sharedWith = { 'hwp' } },
    ['hwp']    = {},
}
```

Set it on both sides for mutual visibility. `sharedWith = true` means "see every job in `Config.Jobs`".

```lua
Config.Jobs = {
    ['police'] = { sharedWith = { 'bcso' } },
    ['bcso']   = { sharedWith = { 'police' } },
    ['hwp']    = {},
}
```

You can also write it as a map, which makes an exclusion visible at a glance:

```lua
['police'] = { sharedWith = { bcso = true, hwp = true, sasp = true, park = false } },
```

#### Splitting the dispatch (`Config.PerJobData.dispatch`) <a href="#user-content-perjobdata-dispatch" id="user-content-perjobdata-dispatch"></a>

```lua
Config.PerJobData = {
    announcements = true,
    radioCodes    = true,
    gallery       = true,
    auditLogs     = true,

    dispatch      = false,
}
```

`false` is one shared dispatch board for every department, which is how the tablet has always worked. It also **defaults to false when the key is missing**, so updating without touching your config changes nothing.

Set it to `true` and each department gets its own calls, patrols, units board, actions board, convoy and archive.

**Calls are split by who raised them.** A call created from the tablet, or raised with `/panic` or `/location`, belongs to the department that raised it. Automatic alerts, `/911` and calls coming from another resource carry no department and stay visible to everyone, because nobody raised them on behalf of one.

{% hint style="warning" %}
**Calls and patrols read `sharedWith` differently, and the reason matters.**

A call is read-only for anyone who did not raise it, so one-way `sharedWith` works there as it does everywhere else — LSPD can see BCSO calls without BCSO seeing theirs.

A patrol is something you **join and leave**. One-way sharing cannot be expressed: the moment LSPD could see a BCSO patrol, it could also join it, kick people from it and delete it. So patrols, units, actions and the convoy follow `sharedWith` **only when it is set on both sides**. A department that shares one way keeps its own board.
{% endhint %}

Grouping is transitive. `police ↔ bcso` plus `bcso ↔ hwp` puts all three on one board, even without a direct entry between `police` and `hwp`.

<details>

<summary>Five departments — worked examples</summary>

**All five share everything**

```lua
Config.Jobs = {
    ['police'] = { sharedWith = true },
    ['bcso']   = { sharedWith = true },
    ['hwp']    = { sharedWith = true },
    ['sasp']   = { sharedWith = true },
    ['park']   = { sharedWith = true },
}
```

**Four share, the fifth stays separate**

Do **not** use `sharedWith = true` here — it means "see everyone", including the department you want isolated. List them out:

```lua
Config.Jobs = {
    ['police'] = { sharedWith = { 'bcso', 'hwp', 'sasp' } },
    ['bcso']   = { sharedWith = { 'police', 'hwp', 'sasp' } },
    ['hwp']    = { sharedWith = { 'police', 'bcso', 'sasp' } },
    ['sasp']   = { sharedWith = { 'police', 'bcso', 'hwp' } },
    ['park']   = {},
}
```

If you give those four `sharedWith = true` and leave `park` empty, the patrol boards still come out right — grouping needs both sides and `park` does not reciprocate. But the four **will** see `park`'s calls, one way, which is almost certainly not what you wanted and is easy to miss.

</details>

***

#### 1. Configuring Ranks (Grades) <a href="#user-content-1-configuring-ranks-grades" id="user-content-1-configuring-ranks-grades"></a>

In config.lua, the `Config.Grades` table controls the hierarchy visible in the MDT. This **must** match your framework's job grades (ESX/QBCore/QBOX) to ensure correct label display.

```lua
-- config.lua
Config.Grades = {    
    {id = 0, name = "Cadet"},     
    {id = 1, name = "Officer I"},    
    {id = 2, name = "Officer II"},    
    {id = 3, name = "Sergeant"},    
    {id = 4, name = "Lieutenant"},    
    {id = 5, name = "Chief"}
}
-- Ensure 'id' corresponds to the grade integer in your database.
```

#### 2. Customizing Fines <a href="#user-content-2-customizing-fines" id="user-content-2-customizing-fines"></a>

You can add new categories and fines in `Config.FineList`.

```lua
-- config.lua
Config.FineList = {    
    {        
        id = "1", -- Unique Category ID        
        label = "Traffic Laws",        
        items = {            
            {                    
                id = "1",                 
                label = "Speeding (> 50 km/h)",                 
                fine = 500,                 
                jail = 0 -- Optional: Jail time in months/minutes            
            },            
            {                
                id = "2",                 
                label = "Reckless Driving",                 
                fine = 1000,                 
                jail = 5            
            }        
        }    
    }
}
```

#### 3. Dispatch Colors & Vehicles <a href="#user-content-3-dispatch-colors--vehicles" id="user-content-3-dispatch-colors--vehicles"></a>

The dispatch system uses specific color codes for vehicles. These are defined in `Config.Dispatch.Colors`. When creating custom alerts, you can use these or standard RGB values.

### Dispatch <a href="#user-content-dispatch" id="user-content-dispatch"></a>

Everything below lives under `Config.Dispatch` in `config/config.lua`. Every option falls back to a sensible default, so a server that copies nothing across still runs.

#### Call life cycle <a href="#user-content-dispatch-lifecycle" id="user-content-dispatch-lifecycle"></a>

A call moves `new → accepted (en route) → on scene → closed`, and closing asks for a reason that stays in the archive. The card lists **who** took it and with which unit, rather than a bare counter.

```lua
Lifecycle = {
    enabled = true,
    requireCloseReason = true,
    onlyAssignedCanClose = true,
    autoCloseAfter = 1800,  -- seconds of silence before an accepted call closes itself, 0 = never

    CloseReasons = { 'confirmed', 'unfounded', 'report', 'duplicate' },
},
```

Reason labels come from the translations — `confirmed` reads `dispatchCloseReasonConfirmed`, and an entry with no matching key falls back to the raw name, so adding your own never shows a blank option.

{% hint style="info" %}
Set `enabled = false` to keep the plain react counter exactly as it behaved in 1.9.x. The old `dispatch/alerts/react` endpoint still works either way and maps onto the new assignment.
{% endhint %}

**There is no officer limit.** Anyone can join a call, however many are already on it. The `maxOfficers` argument still exists on the export for compatibility, but nothing enforces it.

#### Priorities <a href="#user-content-dispatch-priorities" id="user-content-dispatch-priorities"></a>

One priority drives the chip colour, the list order, the blip size on the map and the sound played on arrival.

```lua
Priorities = {
    ['low']      = { weight = 1, color = { 34, 197, 94 },  sound = 'alert.mp3', repeatSound = 1, volumeScale = 0.6, blipScale = 0.8 },
    ['medium']   = { weight = 2, color = { 234, 179, 8 },  sound = 'alert.mp3', repeatSound = 1, volumeScale = 0.8, blipScale = 0.9 },
    ['high']     = { weight = 3, color = { 249, 115, 22 }, sound = 'alert.mp3', repeatSound = 1, volumeScale = 1.0, blipScale = 1.1 },
    ['critical'] = { weight = 4, color = { 239, 68, 68 },  sound = 'alert.mp3', repeatSound = 2, volumeScale = 1.0, blipScale = 1.3 },
},

DefaultPriority = 'medium',  -- used when an alert arrives without one
```

`repeatSound` plays the same file more than once, which is how a `10-13` ends up sounding different from a `10-55` without shipping extra audio.

#### Making one call impossible to miss <a href="#user-content-dispatch-emphasis" id="user-content-dispatch-emphasis"></a>

```lua
Emphasis = {
    enabled = true,
    alertTypes = { 'OfficerPanic' },
},
```

An alert type on this list gets the loudest treatment the tablet has: a breathing red frame on the on-screen alert, a larger pin with a second ring on the map, and a marked edge on the card in the list. An integration can also set `emphasis = true` on a single call.

{% hint style="warning" %}
Keep this list short. It works because it is rare — put every critical alert on it and an officer stops seeing any of them.
{% endhint %}

#### Creating a call from the tablet <a href="#user-content-dispatch-createcall" id="user-content-dispatch-createcall"></a>

```lua
CreateCall = {
    enabled = true,
    defaultPriority = 'medium',
    cooldown = 10000,      -- ms between two calls from the same officer
    allowedGrades = {},    -- empty = anyone with a job from Config.Jobs
    allowMapPick = true,   -- pick the location by clicking the live map

    Icons = { ... },       -- Font Awesome names offered in the form
},
```

The code is picked from your own radio codes. Grade gates are checked **on the server as well**, not only by hiding the button, so a modified client gains nothing by sending the request anyway.

#### Caller details and attachments <a href="#user-content-dispatch-calls" id="user-content-dispatch-calls"></a>

```lua
Calls = {
    callerInfo = true,       -- show who called
    allowAnonymous = true,   -- let a caller stay anonymous
    attachments = true,      -- allow a photo on a call
    attachmentHosts = { ... },
},
```

Turning `callerInfo` off strips caller details **server-side**, on the active board and in the archive, so they never reach a client at all.

#### Archive <a href="#user-content-dispatch-history" id="user-content-dispatch-history"></a>

```lua
History = {
    enabled = true,
    keepDays = 14,           -- archived calls older than this are deleted, 0 = keep forever
    pageSize = 25,
    defaultSort = 'newest',  -- or 'oldest'
},
```

Closed calls go to `qf_mdt_police_v2_dispatch_calls` and `qf_mdt_police_v2_dispatch_call_officers`. Both tables are created automatically on first start — there is no SQL file to import.

The archive is searchable by title, code and street, filterable by priority and closing reason, and sortable newest or oldest first. The order is resolved in SQL, so *oldest* really reaches the far end of the archive rather than reversing the page you can already see.

#### Quick actions and the sidebar counter <a href="#user-content-dispatch-quickactions" id="user-content-dispatch-quickactions"></a>

```lua
QuickActions = {
    enabled = true,
    accept  = { enabled = true, key = 'Z' },
    dismiss = { enabled = true, key = 'O' },
    expand  = { enabled = true, key = 'J' },
},

SidebarBadge = {
    enabled = true,
    count = 'new',  -- 'new' = only calls nobody took yet, 'active' = every open call
},
```

Quick actions work on the newest on-screen alert without opening the tablet. GPS stays on the existing `FastGPS` key.

#### Officer commands — `/panic` and `/location` <a href="#user-content-dispatch-officercommands" id="user-content-dispatch-officercommands"></a>

```lua
OfficerCommands = {
    enabled = true,

    panic = {
        enabled = true,
        command = 'panic',
        key = '',                  -- e.g. 'F9' to bind it
        code = '10-13',
        priority = 'critical',
        alertType = 'OfficerPanic',
        cooldown = 30,
    },

    location = {
        enabled = true,
        command = 'location',
        key = '',
        code = '10-20',
        priority = 'low',
        alertType = 'OfficerLocation',
        cooldown = 20,
    },

    maxNoteLength = 128,
},
```

Both take an optional note — `/panic pinned down behind the car`. Name, badge and position are read **on the server**, so nothing about them can be spoofed from the client.

#### Emergency line for civilians — `/911` <a href="#user-content-dispatch-citizencalls" id="user-content-dispatch-citizencalls"></a>

```lua
CitizenCalls = {
    enabled = true,
    command = '911',
    code = '911',
    priority = 'high',
    alertType = 'CitizenCall',

    minLength = 4,
    maxLength = 200,

    cooldown = 120,          -- seconds between two calls from the same person
    maxPerSession = 0,       -- hard cap per connection, 0 = no cap
    duplicateWindow = 600,   -- seconds in which the same text from the same person is dropped
    nearbyRadius = 60.0,     -- another call within this radius and window counts as the same event
    nearbyWindow = 90,

    anonymous = false,
    allowAnonymousFlag = true,   -- lets the caller choose: /911 anon <what happened>
    anonymousKeyword = 'anon',

    blockedJobs = {},        -- jobs that may not use the civilian line
},
```

A call from `/911` carries what a citizen could actually know — what they saw and roughly where they are — and none of the officer-side fields. Rate limiting works on four axes at once, so a crowd witnessing one event produces one call rather than fifteen.

{% hint style="danger" %}
**`/panic`, `/location` and `/911` are opt-in.** They are only registered when `Config.Dispatch.OfficerCommands` and `Config.Dispatch.CitizenCalls` exist in your config.

Every other new option falls back to a default, but a command name is different: plenty of servers already have a `/panic` or a `/911` from a phone or panic-button resource, and updating this tablet must not take it from them. Copy the sections across to switch the commands on, and rename them there if they clash.
{% endhint %}

***

### Server-side Exports <a href="#user-content-server-side-exports" id="user-content-server-side-exports"></a>

Use these exports in your other resources to interact with the MDT backend.

#### `AddLog` <a href="#user-content-addlog" id="user-content-addlog"></a>

Adds an entry to the Audit Log. Useful for tracking evidence locker access, armory usage, or boss menu actions.

**Syntax:**

```lua
exports.qf_mdt_police_v2:AddLog(source, tag, text, color, fields, args)
```

**Parameters:**

| Parameter | Type   | Description                                                     |
| --------- | ------ | --------------------------------------------------------------- |
| `source`  | number | Player Source ID.                                               |
| `tag`     | string | Category tag (e.g., 'armory', 'evidence', 'boss').              |
| `text`    | string | Log message.                                                    |
| `color`   | string | Color theme ('blue', 'red', 'green', 'orange').                 |
| `fields`  | table  | (Optional) Key-value pairs for extra metadata.                  |
| `args`    | table  | (Optional) Arguments for translation if `text` is a locale key. |

**Example (Armory Usage):**

```lua
-- Triggered when a player withdraws a weapon
local weaponName = "WEAPON_PISTOL"
local ammoCount = 50

exports.qf_mdt_police_v2:AddLog(    
    source,     
    "armory",     
    "Withdrew weapon from armory",     
    "red",     
    {         
        weapon = weaponName,         
        ammo = ammoCount,        
        serialNumber = "WX-12345"     
    }
)
```

#### `CreateCall` <a href="#user-content-createcall" id="user-content-createcall"></a>

Raises a call from another resource. Everything except the title and coordinates is optional.

```lua
local id = exports.qf_mdt_police_v2:CreateCall({
    title = 'Robbery in progress',
    x = 425.1, y = -979.5, z = 30.7,   -- or coords = vector3(...)

    code        = '10-90',
    description = 'Silent alarm at the Fleeca on Vespucci',
    priority    = 'high',              -- low | medium | high | critical, also 1-4
    icon        = 'fa-sack-dollar',    -- pin on the live map
    street      = 'Vespucci Blvd',
    alertType   = 'Robbery',           -- key from Config.Map.AlertSettings.Types
    attachment  = 'https://...',       -- https only
    emphasis    = false,               -- true = the panic treatment
    job         = 'police',            -- only this department sees it, see Config.PerJobData.dispatch
})
```

**Returns** the id of the call, or `nil` when it never reached the board.

{% hint style="warning" %}
A `nil` return is not an error — a free zone or the deduplication window can drop a call on purpose. Check it before you store the id, because `CloseCall` on an id that was never created will fail later.
{% endhint %}

#### `CreateCitizenCall` <a href="#user-content-createcitizencall" id="user-content-createcitizencall"></a>

Raises a call on behalf of somebody who is not police — a phone app, a panic button, an alarm. Pass the player's server id and the caller block is filled in from the framework.

```lua
exports.qf_mdt_police_v2:CreateCitizenCall(playerSource, {
    title       = 'Emergency call',
    description = 'Man with a gun outside the store',
    priority    = 'high',
    anonymous   = false,
})
```

Coordinates default to the caller's position. The phone number comes from `EDITABLE.GetPhoneNumber`, and is only looked up when the call is not anonymous.

#### `CloseCall` <a href="#user-content-closecall" id="user-content-closecall"></a>

Closes an active call from another resource and writes it to the archive.

```lua
exports.qf_mdt_police_v2:CloseCall(id, 'confirmed')
```

***

### Client-side Exports <a href="#user-content--client-side-exports" id="user-content--client-side-exports"></a>

Use these exports to integrate the MDT UI and systems with other scripts (e.g., Robbery scripts, Radar systems).

#### `CreateDispatchAlert` <a href="#user-content-createdispatchalert" id="user-content-createdispatchalert"></a>

Triggers a dispatch notification for all on-duty police.

**Syntax:**

```lua
exports.qf_mdt_police_v2:CreateDispatchAlert(coords, title, description, code, colorRGB, maxOfficers, duration)
```

**Parameters:**

| Parameter     | Type    | Description                                                                                                     |
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `coords`      | vector3 | Location of the alert.                                                                                          |
| `title`       | string  | Alert Header.                                                                                                   |
| `description` | string  | Detailed info (e.g., street name, vehicle info).                                                                |
| `code`        | string  | 10-Code (e.g., '10-90').                                                                                        |
| `colorRGB`    | table   | `{r, g, b}` format.                                                                                             |
| `maxOfficers` | number  | Ignored since 1.10.0. Calls have no officer limit, but the slot stays so `duration` does not shift. Pass `nil`. |
| `duration`    | number  | (Optional) Time in ms the alert stays on screen.                                                                |

**Example (Bank Robbery):**

```lua
-- Triggered when a player starts hacking a vault
local plyPed = PlayerPedId()
local coords = GetEntityCoords(plyPed)
local streetName = GetStreetNameFromHashKey(GetStreetNameAtCoord(coords.x, coords.y, coords.z))

exports.qf_mdt_police_v2:CreateDispatchAlert(
    coords,    
    "Bank Robbery in Progress",    
    "Silent alarm triggered at " .. streetName .. " Fleeca Bank.",    
    "10-90",    
    {255, 0, 0}, 
    nil, -- former maxOfficers slot, ignored
    10000
)
```

#### `showOfficerBadge` <a href="#user-content-showofficerbadge" id="user-content-showofficerbadge"></a>

Displays the badge ID card on screen.

**Syntax:**

```lua
exports.qf_mdt_police_v2:showOfficerBadge(data)
```

**Data Structure:**

```lua
{    
    name = "Officer Name",    
    badge = "Badge Number",    
    gradeLabel = "Rank Name",    
    mugshot = "URL to image (optional)",    
    licenses = {        
        { label = "Weapon License", active = true },        
        { label = "Driving License", active = false }    
    }
}
```

**Example:**

```lua
exports.qf_mdt_police_v2:showOfficerBadge(
{    
    name = "Robert Smith",    
    badge = "9921",    
    gradeLabel = "Chief of Police",    
    licenses = {        
        { label = "Advanced Driving", active = true },        
        { label = "SWAT Tactics", active = true }    
    }
})
```

#### `AddHeistZone` (Map) <a href="#user-content-addheistzone-map" id="user-content-addheistzone-map"></a>

Adds a visual zone to the live map, useful for ongoing robberies.

**Syntax:**

```lua
exports.qf_mdt_police_v2:AddHeistZone(zoneData)
```

**Example:**

```lua
exports.qf_mdt_police_v2:AddHeistZone({    
    id = "pacific_standard",    
    name = "Pacific Standard Robbery",    
    position = { x = 255.2, y = 210.0 }, 
    radius = 60.0,    street = "Vinewood Blvd",    
    thumbnailUrl = "https://example.com/bank-image.jpg"
})
```

#### `RemoveHeistZone` <a href="#user-content-removeheistzone" id="user-content-removeheistzone"></a>

Removes a heist zone when the event is over.

**Example:**

```lua
exports.qf_mdt_police_v2:RemoveHeistZone("pacific_standard")
```

***

### Camera & Radar Exports <a href="#user-content--camera--radar-exports" id="user-content--camera--radar-exports"></a>

#### Bodycam <a href="#user-content-bodycam" id="user-content-bodycam"></a>

Control the immersion overlays.

```lua
exports.qf_mdt_police_v2:showBodycam()

exports.qf_mdt_police_v2:hideBodycam()

exports.qf_mdt_police_v2:toggleBodycam()
```

#### Radar <a href="#user-content-radar" id="user-content-radar"></a>

Control the vehicle radar system programmatically.

```lua
-- Force show / hide a radar side ("front" or "back")
exports.qf_mdt_police_v2:showRadar("front")
exports.qf_mdt_police_v2:hideRadar("front")

-- Lock or unlock the current target of a radar side
exports.qf_mdt_police_v2:toggleRadarInteractMode("front")

-- Manually update radar data (e.g. for a tutorial or scripted event)
exports.qf_mdt_police_v2:setRadarData("front", {    
    plate = "FAKE-123",    model = "Buffalo",    speed = 150,    speedDisplay = "km/h"
})
```

#### Other exports <a href="#user-content-other-exports" id="user-content-other-exports"></a>

| Export                                           | Description                                                                 |
| ------------------------------------------------ | --------------------------------------------------------------------------- |
| `GetMugShotBase64(ped, transparent)`             | Renders a mugshot of the ped and returns it as a base64 string.             |
| `showDispatchAlert(data)`                        | Draws a dispatch alert on screen without going through the dispatch system. |
| `updateReactions(data)`                          | Updates the unit reactions shown on an alert that is already on screen.     |
| `ShowHeistZones(zones)` / `SetHeistZones(zones)` | Replaces the whole list of heist zones on the live map at once.             |
| `SetShootingZones(zones)`                        | Replaces the whole list of shots-fired zones on the live map.               |
| `AddShootingZone(zone)`                          | Adds a single shots-fired zone. Same shape as `AddHeistZone`.               |
| `RemoveShootingZone(id)`                         | Removes a shots-fired zone by id.                                           |

### Editable functions <a href="#user-content-editable-functions" id="user-content-editable-functions"></a>

These live in `config/server/editable.lua`, which is **yours to edit** and is not encrypted. Each one ships with a working default.

#### `EDITABLE.GetPhoneNumber(xPlayer)` <a href="#user-content-getphonenumber" id="user-content-getphonenumber"></a>

Fills the **Phone** line on a call raised by a citizen. It ships reading the number the framework itself stores — `users.phone_number` on ESX, `charinfo.phone` on QB and QBox.

Point it at your own phone resource if you run a standalone one:

```lua
function EDITABLE.GetPhoneNumber(xPlayer)
    return exports['lb-phone']:GetEquippedPhoneNumber(xPlayer.source)
end
```

Return `nil` and the tablet simply leaves the field out, which is the honest result for a character who owns no phone. It is never called for an anonymous call.

#### Department labels on outgoing text <a href="#user-content-editable-job-labels" id="user-content-editable-job-labels"></a>

On a server with several departments, a citizen fined by the Sheriff's Office should not be told "Police Fine". These four decide what leaves the tablet:

| Function                                               | Used for                                                       |
| ------------------------------------------------------ | -------------------------------------------------------------- |
| `EDITABLE.GetJobLabel(jobName)`                        | the department label the other three build on                  |
| `EDITABLE.FormatCitizenNotification(jobName, message)` | notifications a citizen receives about a fine, jail or warrant |
| `EDITABLE.GetFineReason(jobName)`                      | the reason your billing resource shows on the invoice          |
| `EDITABLE.GetJailReason(jobName)`                      | the reason your prison resource records                        |

Defaults produce `[LSPD] You received 1 fine(s) totaling $500` and `LSPD - Police Fine`. The label comes from `fractionName`, falling back to `society.label` and then `Config.Society.label`.

#### `EDITABLE.SendLog(logname, message, jobName)` <a href="#user-content-sendlog" id="user-content-sendlog"></a>

Posts to the Discord webhook configured in `Config.Webhooks`. Passing `jobName` puts the department in the embed heading and title, so logs from two departments can be told apart. Leave it out and the embed looks exactly as it did before.

***

### Dispatch Integrations & Custom Alerts

#### Compatibility

Enable 3rd party script compatibility by editing `config/config.lua`.

```lua
-- Enable external dispatch scripts in config/config.lua
Config.Dispatch = {
    Integration = {
        rcore_dispatch = true, -- Set to true to automatically bridge alerts
        core_dispatch = false,
        opto_dispatch = false,
        tk_dispatch = false,
        _0r_dispatch = false,
        frkn_police_dispatch = false
    },

    -- Set to false if your external dispatch already draws its own alerts
    -- and you end up seeing every alert twice.
    ShowOnScreenAlerts = true
}
```

#### Custom Alerts

Control and send dispatch alerts to the MDT programmatically.

```lua
-- Send a custom dispatch alert manually from any server script
TriggerEvent('qf_mdt_police_v2/server/addCustomDispatch', {
    id = "custom_robbery_123",        -- (Optional) Unique ID, system generates one if empty
    title = "Store Robbery",          -- Alert title/name
    code = "10-90",                   -- Dispatch code
    color = {255, 0, 0},              -- RGB color table or HEX string (e.g., "#FF0000")
    street = "Route 68",              -- (Optional) Street name or location description
    x = 100.5,                        -- X coordinate of the alert
    y = -213.2,                       -- Y coordinate of the alert
    z = 32.5,                         -- Z coordinate of the alert
    duration = 5000                   -- (Optional) Duration the alert stays on screen in ms (default: 5000)
})
```

#### 1. Client-Side Export <a href="#user-content-1-client-side-export" id="user-content-1-client-side-export"></a>

You can use the following export directly in any client-side script.

```lua
-- @param coords vector3 - The coordinates of the incident
-- @param title string - The title of the dispatch alert
-- @param description string - The detailed description
-- @param code string - The dispatch code (e.g., "10-13")
-- @param colorRGB table - An array containing RGB values (e.g., { 251, 45, 55 })
-- @param maxOfficers number - Ignored since 1.10.0. Pass nil; the slot stays so duration does not shift
-- @param duration number - The duration the alert shows on screen in milliseconds (e.g., 5000)

local coords = GetEntityCoords(PlayerPedId())
exports['qf_mdt_police_v2']:CreateDispatchAlert(    
    coords,     
    "Officer Down",     
    "An officer has been injured and requires immediate assistance.",     
    "10-13",     
    { 251, 45, 55 },     
    nil, -- former maxOfficers slot, ignored
    5000
)
```

#### 2. Server-Side Event (or Client `TriggerServerEvent`) <a href="#user-content-2-server-side-event-or-client-triggerserverevent" id="user-content-2-server-side-event-or-client-triggerserverevent"></a>

If you need to trigger a dispatch from the server, or prefer passing a data table, you can use the built-in server event.

```lua
local dispatchData = {    
    x = 100.0,           -- [Required] Coordinate X    
    y = -200.0,          -- [Required] Coordinate Y    
    z = 30.0,            -- [Required] Coordinate Z    
    title = "Robbery",   -- [Required] Dispatch Title    
    code = "10-90",      -- [Required] Dispatch Code        
    color = {255, 0, 0}, -- [Optional] RGB table or HEX string (e.g., "#FF0000"). Defaults to blue.    
    street = "Route 68", -- [Optional] Street name. Defaults to empty.    
    duration = 5000,     -- [Optional] Screen duration in ms. Defaults to 5000.    
    id = "custom_id_1"   -- [Optional] A custom unique ID if you need to manage it later
}

-- If triggering from a SERVER script:
TriggerEvent('qf_mdt_police_v2/server/addCustomDispatch', dispatchData)

-- If triggering from a CLIENT script:
TriggerServerEvent('qf_mdt_police_v2/server/addCustomDispatch', dispatchData)
```

## Resetting Officer Hours <a href="#user-content-4-resetting-officer-hours-police" id="user-content-4-resetting-officer-hours-police"></a>

The Police MDT tracks on-duty hours for all officers. These hours can be manually reset via a server callback. The system verifies if the player executing the reset has the appropriate job permissions defined in `Config.Jobs`.

* **Callback Name:** `qf_mdt_police_v2/resetOfficerHours`
* **Target Identifier:** `id` (Character Identifier / CitizenID / License)

**Code Example**

```lua
local officerId = "POLICE_001"

CALLBACK.TriggerServerCallback('qf_mdt_police_v2/resetOfficerHours', function(result)    
    if result.success then        
        print("Officer hours have been reset successfully.")    
    else        
        print("Error: " .. result.error)    
    end
end, officerId)
```

## Duty Status Change Hook <a href="#user-content-6-duty-status-change-hook-police" id="user-content-6-duty-status-change-hook-police"></a>

We have added a server-side hook that triggers whenever an officer changes their duty status via the MDT (e.g., clicking "Available" or "Unavailable"). This is useful for synchronizing the MDT state with other job-related systems or triggering custom events.

* **Location:** `config/server/editable.lua`
* **Function:** `EDITABLE.OnStatusChange(source, status)`

**Usage Example**

This hook is defined in the editable configuration file and can be used to trigger framework-specific duty events:

```lua
function EDITABLE.OnStatusChange(source, status)
    -- Triggered for status: "available", "unavailable"
    
    if status == "available" then
        -- Example: Triggering a custom onDuty event
        TriggerEvent('examplepolicejob:onDuty', source)
    elseif status == "unavailable" then
        -- Example: Triggering a custom offDuty event
        TriggerEvent('examplepolicejob:offDuty', source)
    end
end
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://wiki.qfdevelopers.com/scripts/editor-4/configuration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
