(showWeaponOwnerSearch = false)}
+/>
+
+{#if showAddWeaponModal}
+
+
+ { if (e.target === e.currentTarget) showAddWeaponModal = false; }}>
+
+
+
+
+ Weapon Name
+
+
+
+ Serial Number
+
+ 0}>
+ If this serial number already exists in the database, the existing record will be updated with the new information.
+
+
+
+ Owner
+
+
+
+ Notes
+
+
+
+
+
+
+{/if}
+
{#if selectedWeapon}
@@ -224,17 +379,28 @@
{#each weaponHistory as entry}
- {entry.owner || 'Unknown'}
+
+ {entry.owner_name ?? entry.owner ?? 'Unknown'}
+ {#if entry.owner_name && entry.owner}
+ ({entry.owner})
+ {/if}
+
- {entry.weapon_model || ''}
+ - Model: {entry.weapon_model || ''}
{entry.weapon_class ? ` · ${entry.weapon_class}` : ''}
+ {#if entry.information}
+ - Notes: {entry.information}
+ {/if}
{new Date(entry.created_at).toLocaleDateString()}
{#if entry.reason}
{entry.reason}
{/if}
+ {#if entry.changed_by_name ?? entry.changed_by}
+ logged by {entry.changed_by_name ?? entry.changed_by}
+ {/if}
{/each}
@@ -254,6 +420,11 @@
+ {#if canAddWeapon}
+
+ {/if}
@@ -415,6 +586,10 @@
cursor: not-allowed;
}
+ /* Add Weapon button */
+ .add-weapon-btn { display: flex; align-items: center; gap: 3px; background: rgba(59,130,246,0.06); border: 1px solid rgba(59,130,246,0.1); border-radius: 3px; padding: 4px 10px; color: rgba(147,197,253,0.7); font-size: 9px; font-weight: 600; cursor: pointer; transition: all 0.12s; text-transform: none; letter-spacing: 0; }
+ .add-weapon-btn:hover { background: rgba(59,130,246,0.12); color: rgba(147,197,253,0.9); }
+
/* ===== List Panel ===== */
.list-panel {
flex: 1;
@@ -754,4 +929,49 @@
color: rgba(255, 255, 255, 0.35);
font-size: 10px;
}
+
+ .add-weapon-description { font-size: 10px; color: rgba(255,255,255,0.35); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; max-height: 0; opacity: 0; transition: max-height 0.8s ease, opacity 0.8s ease; }
+ .add-weapon-description.visible { max-height: 40px; opacity: 1; }
+
+ /* Modal shared */
+ .modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 999; }
+ .modal { background: var(--card-dark-bg); border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 6px; width: min(540px, 92vw); max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); }
+ .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); }
+ .modal-header h3 { margin: 0; font-size: 12px; font-weight: 600; color: rgba(255, 255, 255, 0.85); }
+ .close-btn { display: flex; align-items: center; justify-content: center; background: transparent; color: rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.06); padding: 4px; border-radius: 3px; cursor: pointer; transition: all 0.1s; }
+ .close-btn:hover { color: rgba(255, 255, 255, 0.7); border-color: rgba(255, 255, 255, 0.1); }
+
+ .modal-body { padding: 14px 16px; overflow-y: auto; }
+ .modal-top { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
+ .modal-name { color: rgba(255, 255, 255, 0.85); font-size: 14px; font-weight: 700; }
+ .modal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
+ .modal-field { display: flex; flex-direction: column; gap: 2px; }
+ .field-label { color: rgba(255, 255, 255, 0.35); font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; }
+ .field-value { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 500; }
+ .modal-notes { background: transparent; border: none; border-top: 1px solid rgba(255, 255, 255, 0.04); border-radius: 0; padding: 10px 0 0; }
+ .notes-body { margin: 4px 0 0; font-size: 11px; line-height: 1.5; color: rgba(255, 255, 255, 0.5); }
+
+ .modal-footer { display: flex; justify-content: space-between; align-items: center; gap: 6px; padding: 10px 16px; border-top: 1px solid rgba(255, 255, 255, 0.06); }
+ .modal-footer-left { display: flex; gap: 6px; }
+ .modal-footer-right { display: flex; gap: 6px; }
+ .resolve-btn { display: flex; align-items: center; gap: 4px; background: rgba(34, 197, 94, 0.06); color: rgba(74, 222, 128, 0.7); border: 1px solid rgba(34, 197, 94, 0.1); padding: 4px 10px; border-radius: 3px; font-size: 10px; font-weight: 500; cursor: pointer; transition: all 0.1s; }
+ .resolve-btn:hover { background: rgba(34, 197, 94, 0.12); color: rgba(74, 222, 128, 0.9); }
+ .delete-btn { display: flex; align-items: center; gap: 4px; background: transparent; color: rgba(248, 113, 113, 0.5); border: 1px solid rgba(239, 68, 68, 0.1); padding: 4px 10px; border-radius: 3px; font-size: 10px; font-weight: 500; cursor: pointer; transition: all 0.1s; }
+ .delete-btn:hover { background: rgba(239, 68, 68, 0.08); color: rgba(252, 165, 165, 0.8); }
+ .action-btn { background: rgba(var(--accent-rgb), 0.06); color: rgba(var(--accent-text-rgb), 0.7); border: 1px solid rgba(var(--accent-rgb), 0.1); padding: 4px 10px; border-radius: 3px; font-size: 10px; font-weight: 500; cursor: pointer; transition: all 0.1s; }
+ .action-btn:hover { background: rgba(var(--accent-rgb), 0.12); color: rgba(var(--accent-text-rgb), 0.9); }
+ .cancel-btn { background: transparent; color: rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 3px; padding: 4px 10px; font-size: 10px; font-weight: 500; cursor: pointer; transition: all 0.1s; }
+ .cancel-btn:hover { color: rgba(255, 255, 255, 0.7); border-color: rgba(255, 255, 255, 0.1); }
+ .primary-btn { background: rgba(16, 185, 129, 0.06); color: rgba(52, 211, 153, 0.7); border: 1px solid rgba(16, 185, 129, 0.1); border-radius: 3px; padding: 4px 12px; font-size: 10px; font-weight: 600; cursor: pointer; transition: all 0.1s; }
+ .primary-btn:hover { background: rgba(16, 185, 129, 0.12); color: rgba(110, 231, 183, 0.9); }
+
+ /* Form */
+ .form-body { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+ .form-group { display: flex; flex-direction: column; gap: 3px; }
+ .form-full { grid-column: 1 / -1; }
+ .form-input { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 3px; padding: 5px 8px; color: rgba(255, 255, 255, 0.8); font-size: 11px; transition: border-color 0.1s; font-family: inherit; }
+ .form-input:focus { outline: none; border-color: rgba(255, 255, 255, 0.1); }
+ .form-input::placeholder { color: rgba(255, 255, 255, 0.2); }
+ .form-select { padding-right: 22px; font-size: 10px; }
+ textarea.form-input { resize: vertical; min-height: 60px; }
From 183a7e1b2c343e56632a00b02862e2b76cf8b6c9 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 13:52:26 +0200
Subject: [PATCH 003/144] added "weapons_add" into permissions
added "weapons_add" as a new permission
---
web/src/components/ContentArea.svelte | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/components/ContentArea.svelte b/web/src/components/ContentArea.svelte
index e92adcd2..2811e09d 100644
--- a/web/src/components/ContentArea.svelte
+++ b/web/src/components/ContentArea.svelte
@@ -109,7 +109,7 @@
citizens: ["citizens_search"],
bolos: ["bolos_view", "bolos_create"],
vehicles: ["vehicles_search"],
- weapons: ["weapons_search"],
+ weapons: ["weapons_search", "weapons_add"],
cases: ["cases_view", "cases_create"],
evidence: ["evidence_view", "evidence_create"],
reports: ["reports_view", "reports_create"],
@@ -208,7 +208,7 @@
{:else if activeComponent === "vehicles"}
{:else if activeComponent === "weapons"}
-
+
{:else if activeComponent === "cases"}
{:else if String(activeComponent) === "evidence"}
From 8956edc942c1810110bd9fa514a1658ab8b4e15d Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 13:53:59 +0200
Subject: [PATCH 004/144] changed search function to allow spaces/big/small
letters
changed search function to allow spaces/big/small letters
---
web/src/components/report-editor/VehiclesManager.svelte | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/web/src/components/report-editor/VehiclesManager.svelte b/web/src/components/report-editor/VehiclesManager.svelte
index b615f7c3..c41b8e45 100644
--- a/web/src/components/report-editor/VehiclesManager.svelte
+++ b/web/src/components/report-editor/VehiclesManager.svelte
@@ -31,7 +31,7 @@
const value = (e.target as HTMLInputElement).value;
searchQuery = value;
if (searchTimeout) clearTimeout(searchTimeout);
- if (!value.trim()) {
+ if (value.trim().length < 2) {
searchResults = [];
return;
}
@@ -40,6 +40,7 @@
async function performSearch(query: string) {
if (!query) return;
+ const normalizedQuery = query.toUpperCase().replace(/\s+/g, '');
isSearching = true;
try {
if (isEnvBrowser()) {
@@ -50,7 +51,7 @@
} else {
const results = await fetchNui(
NUI_EVENTS.REPORT.SEARCH_VEHICLES_FOR_REPORT,
- { query },
+ { query: normalizedQuery },
[],
);
searchResults = Array.isArray(results) ? results : [];
From 9c49e0dc1d4060d58fa3eeabc59fd6115acf3cda Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 13:55:52 +0200
Subject: [PATCH 005/144] added "weapons_add" into permissions
added "weapons_add" into permissions
---
web/src/components/management/ManagementPermissions.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/components/management/ManagementPermissions.svelte b/web/src/components/management/ManagementPermissions.svelte
index bade9e00..ebfc1f1f 100644
--- a/web/src/components/management/ManagementPermissions.svelte
+++ b/web/src/components/management/ManagementPermissions.svelte
@@ -11,7 +11,7 @@
/** Individual permissions hidden for EMS within visible categories */
const EMS_HIDDEN_PERMISSIONS = ['cameras_view', 'management_tracking'];
const DOJ_VISIBLE_CATEGORIES = ['citizens', 'cases', 'evidence', 'charges', 'management'];
- const DOJ_HIDDEN_PERMISSIONS = ['management_bulletins', 'management_activity', 'management_tags', 'management_tracking', 'management_settings', 'citizens_edit_licenses'];
+ const DOJ_HIDDEN_PERMISSIONS = ['management_bulletins', 'management_activity', 'management_tags', 'management_tracking', 'management_settings', 'citizens_edit_licenses', 'weapons_add'];
let visibleCategories = $derived(
jobType === 'ems'
From 06d2dc474d20049917317288b343fb6697100f74 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:02:37 +0200
Subject: [PATCH 006/144] added check for fingerprint and dna
added fingerprint and dna into getCitizens callback
---
server/backend/citizens.lua | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/server/backend/citizens.lua b/server/backend/citizens.lua
index 8cde669c..1571fc23 100644
--- a/server/backend/citizens.lua
+++ b/server/backend/citizens.lua
@@ -100,7 +100,9 @@ ps.registerCallback(resourceName .. ':server:getCitizens', function(source, page
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.gender')) AS gender,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.birthdate')) AS dateofbirth,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.phone')) AS phone,
- JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.label')) AS job
+ JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.label')) AS job,
+ JSON_UNQUOTE(JSON_EXTRACT(p.metadata, '$.fingerprint')) AS fingerprint,
+ JSON_UNQUOTE(JSON_EXTRACT(p.metadata, '$.dna')) AS dna
FROM players AS p
LEFT JOIN mdt_profiles AS mp
ON CONVERT(p.citizenid USING utf8mb4) COLLATE utf8mb4_general_ci = CONVERT(mp.citizenid USING utf8mb4) COLLATE utf8mb4_general_ci
@@ -176,6 +178,8 @@ ps.registerCallback(resourceName .. ':server:getCitizens', function(source, page
v.dob = v.dateofbirth
v.phone = v.phone
v.image = profilePics[v.citizenid] or nil
+ v.fingerprint = v.fingerprint or nil
+ v.dna = v.dna or nil
v.occupations = { v.job }
v.properties = propCounts[v.citizenid] or 0
v.vehicles = vehCounts[v.citizenid] or 0
@@ -216,14 +220,15 @@ ps.registerCallback(resourceName .. ':server:searchCitizens', function(source, q
-- Build a complex search query that searches across multiple fields and returns same data as getCitizens
local sqlQuery = [[
SELECT DISTINCT
- mp.id,
- p.citizenid,
+ mp.id, p.citizenid,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.firstname')) AS firstname,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.lastname')) AS lastname,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.gender')) AS gender,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.birthdate')) AS dateofbirth,
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.phone')) AS phone,
- JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.label')) AS job
+ JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.label')) AS job,
+ JSON_UNQUOTE(JSON_EXTRACT(p.metadata, '$.fingerprint')) AS fingerprint,
+ JSON_UNQUOTE(JSON_EXTRACT(p.metadata, '$.dna')) AS dna
FROM players AS p
LEFT JOIN mdt_profiles AS mp ON p.citizenid COLLATE utf8mb4_general_ci = mp.citizenid COLLATE utf8mb4_general_ci
WHERE
@@ -232,13 +237,15 @@ ps.registerCallback(resourceName .. ':server:searchCitizens', function(source, q
LOWER(CONCAT(JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.firstname')), ' ', JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.lastname')))) LIKE ? OR
LOWER(p.citizenid) LIKE ? OR
LOWER(JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.phone'))) LIKE ? OR
- LOWER(JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.label'))) LIKE ?
+ LOWER(JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.label'))) LIKE ? OR
+ LOWER(JSON_UNQUOTE(JSON_EXTRACT(p.metadata, '$.fingerprint'))) LIKE ? OR
+ LOWER(JSON_UNQUOTE(JSON_EXTRACT(p.metadata, '$.dna'))) LIKE ?
LIMIT ?
]]
- local searchLimit = Config.Pagination and Config.Pagination.CitizenSearch or 20
local result = safeQuery(sqlQuery, {
- searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchLimit
+ searchTerm, searchTerm, searchTerm, searchTerm,
+ searchTerm, searchTerm, searchTerm, searchTerm
})
if not result or #result == 0 then return {} end
From eb4a9847f2db26f4a142b8561ff88c2abd0b9c4f Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:08:53 +0200
Subject: [PATCH 007/144] added 'weapons_add' as a new permission
added 'weapons_add' as a new permission
---
server/backend/management.lua | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/backend/management.lua b/server/backend/management.lua
index 458d2f47..5b3e3511 100644
--- a/server/backend/management.lua
+++ b/server/backend/management.lua
@@ -60,7 +60,7 @@ local function getAllPermissions()
'citizens_search', 'citizens_edit_licenses',
'bolos_view', 'bolos_create',
'vehicles_search', 'vehicles_edit_dmv',
- 'weapons_search',
+ 'weapons_search', 'weapons_add',
'cases_view', 'cases_create', 'cases_edit', 'cases_delete',
'evidence_view', 'evidence_create', 'evidence_transfer', 'evidence_upload',
'reports_view', 'reports_create', 'reports_delete',
From 0b788b32889b6a7cb90c6a6552b03f5123da23ff Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:10:04 +0200
Subject: [PATCH 008/144] added 'weapons_add' as a new permission and added new
events
added 'weapons_add' as a new permission and added new events
---
web/src/constants/management.ts | 1 +
web/src/constants/nuiEvents.ts | 2 ++
2 files changed, 3 insertions(+)
diff --git a/web/src/constants/management.ts b/web/src/constants/management.ts
index 1c222a0e..21d6db63 100644
--- a/web/src/constants/management.ts
+++ b/web/src/constants/management.ts
@@ -87,6 +87,7 @@ export const PERMISSION_CATEGORIES: PermissionCategory[] = [
icon: "security",
permissions: [
{ key: "weapons_search", label: "Search Weapons", description: "Search and view weapon records" },
+ { key: "weapons_add", label: "Add Weapons", description: "Register a new gun manually" },
],
},
{
diff --git a/web/src/constants/nuiEvents.ts b/web/src/constants/nuiEvents.ts
index e5175863..c049bc06 100644
--- a/web/src/constants/nuiEvents.ts
+++ b/web/src/constants/nuiEvents.ts
@@ -96,7 +96,9 @@ export const NUI_EVENTS = {
SEARCH_WEAPONS: "searchWeapons",
GET_WEAPON: "getWeapon",
GET_WEAPON_HISTORY: "getWeaponOwnershipHistory",
+ SAVE_WEAPON_INFO: 'saveWeaponInfo',
UPDATE_WEAPON: "updateWeapon",
+ GET_WEAPON_CONFIG: "getWeaponConfig",
},
CHARGE: {
GET_CHARGES: "getCharges",
From 278c2ba9556288399a91f2da7a4db7e106e74c13 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:11:30 +0200
Subject: [PATCH 009/144] added 'weapons_add' as a new permission
added 'weapons_add' as a new permission
---
web/src/services/managementService.svelte.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/services/managementService.svelte.ts b/web/src/services/managementService.svelte.ts
index 4fefd62d..c4290af7 100644
--- a/web/src/services/managementService.svelte.ts
+++ b/web/src/services/managementService.svelte.ts
@@ -37,7 +37,7 @@ export function createManagementService() {
// Dev mock: roles with escalating permissions (labels match whatever the job grades are)
const basicPerms = ["citizens_search", "bolos_view", "vehicles_search", "weapons_search", "reports_view", "warrants_view", "charges_view"];
const midPerms = [...basicPerms, "reports_create", "cases_view", "evidence_view", "bolos_create", "dispatch_attach", "dispatch_route"];
- const seniorPerms = [...midPerms, "cases_create", "cases_edit", "evidence_create", "evidence_transfer", "reports_delete", "warrants_issue", "cameras_view", "bodycams_view", "citizens_edit_licenses", "vehicles_edit_dmv"];
+ const seniorPerms = [...midPerms, "cases_create", "cases_edit", "evidence_create", "evidence_transfer", "reports_delete", "warrants_issue", "cameras_view", "bodycams_view", "citizens_edit_licenses", "vehicles_edit_dmv", "weapons_add"];
const commandPerms = [...seniorPerms, "cases_delete", "evidence_upload", "warrants_close", "charges_edit", "management_activity", "management_bulletins", "roster_manage_officers", "roster_manage_certifications"];
roles = [
{ key: "0", label: "Grade 0", permissions: basicPerms, isBoss: false },
From ae01a2c89b21df1069e6afc29647103e0708471b Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:15:14 +0200
Subject: [PATCH 010/144] added new permission, changed ownership history
added new permission, changed ownership history
---
server/backend/weapons.lua | 32 +++++++++++++++++++++++++-------
1 file changed, 25 insertions(+), 7 deletions(-)
diff --git a/server/backend/weapons.lua b/server/backend/weapons.lua
index aa6d9e79..0fcd59a7 100644
--- a/server/backend/weapons.lua
+++ b/server/backend/weapons.lua
@@ -160,21 +160,29 @@ ps.registerCallback('ps-mdt:server:getWeapons', function(source)
return { weapons = newData, bolos = weaponBolo }
end)
-ps.registerCallback(resourceName .. ':server:getWeaponOwnershipHistory', function(source, serial)
+ps.registerCallback(resourceName .. ':server:getWeaponOwnershipHistory', function(source, payload)
local src = source
- if not CheckAuth(src) then return end
+ if not CheckAuth(src) then return {} end
+
+ payload = payload or {}
+ local serial = payload.serial
+
if not serial or serial == '' then return {} end
local rows = MySQL.query.await([[
- SELECT id, serial, owner, weapon_model, weapon_class, information, changed_by, reason, created_at
- FROM mdt_weapon_ownership_history
- WHERE serial = ?
- ORDER BY created_at DESC
+ SELECT h.id, h.serial, h.owner, h.weapon_model, h.weapon_class, h.information, h.changed_by, h.reason, h.created_at,
+ p.fullname AS owner_name,
+ cb.fullname AS changed_by_name
+ FROM mdt_weapon_ownership_history h
+ LEFT JOIN mdt_profiles p ON p.citizenid = h.owner
+ LEFT JOIN mdt_profiles cb ON cb.citizenid = h.changed_by
+ WHERE h.serial = ?
+ ORDER BY h.created_at DESC
]], { serial })
+
return rows or {}
end)
--- Save/Edit Weapon Info (from NUI)
ps.registerCallback(resourceName .. ':server:saveWeaponInfo', function(source, payload)
local src = source
if not CheckAuth(src) then return { success = false, message = 'Unauthorized' } end
@@ -191,6 +199,11 @@ ps.registerCallback(resourceName .. ':server:saveWeaponInfo', function(source, p
return { success = false, message = 'Missing serial number' }
end
+ -- Ensure profile exists before insert/update to avoid FK constraint error
+ -- if owner and owner ~= '' then
+ -- EnsureProfileExists(owner)
+ -- end
+
local existing = MySQL.single.await('SELECT id FROM mdt_weapons WHERE serial = ? LIMIT 1', { serial })
if existing then
@@ -199,6 +212,11 @@ ps.registerCallback(resourceName .. ':server:saveWeaponInfo', function(source, p
SET information = ?, owner = ?, weaponClass = ?, weaponModel = ?
WHERE serial = ?
]], { notes, owner, weapClass, weapModel, serial })
+
+ MySQL.insert.await([[
+ INSERT INTO mdt_weapon_ownership_history (serial, owner, weapon_model, weapon_class, information, changed_by, reason)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ ]], { serial, owner, weapModel, weapClass, notes, ps.getIdentifier(src), 'ownership_transfer' })
else
MySQL.insert.await([[
INSERT INTO mdt_weapons (serial, scratched, owner, information, weaponClass, weaponModel)
From 3f43405f5388970cdc33dcf130d7db42d2bd5d3a Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Sat, 25 Apr 2026 17:10:34 +0200
Subject: [PATCH 011/144] removed unwanted console.logs, changed notes and
removed weaponOptions (now in config)
removed unwanted console.logs, changed notes and removed weaponOptions (now in config)
---
web/src/pages/Citizens.svelte | 1 -
web/src/pages/Weapons.svelte | 13 ++-----------
2 files changed, 2 insertions(+), 12 deletions(-)
diff --git a/web/src/pages/Citizens.svelte b/web/src/pages/Citizens.svelte
index bbf379ef..8f263738 100644
--- a/web/src/pages/Citizens.svelte
+++ b/web/src/pages/Citizens.svelte
@@ -118,7 +118,6 @@
return citizens.filter(({ firstName, lastName, cid, phone, fingerprint, dna }) =>
[firstName, lastName, cid, phone, fingerprint, dna].some((val) =>
val?.toLowerCase().includes(query),
- console.log(query, dna)
),
);
});
diff --git a/web/src/pages/Weapons.svelte b/web/src/pages/Weapons.svelte
index 8cf68ead..160d1fc6 100644
--- a/web/src/pages/Weapons.svelte
+++ b/web/src/pages/Weapons.svelte
@@ -167,14 +167,6 @@
{ id: 3, serial: 'WPN-55194', scratched: false, owner: 'Sarah Williams', information: 'Licensed for personal protection', weaponClass: 'Pistol', weaponModel: 'WEAPON_COMBATPISTOL', name: 'Combat Pistol', image: '', type: 'Handgun', seenIn: 0, flags: [], tint: 'Default' },
{ id: 4, serial: 'WPN-10477', scratched: false, owner: 'David Chen', information: 'Hunting rifle, valid license', weaponClass: 'Rifle', weaponModel: 'WEAPON_MUSKET', name: 'Musket', image: '', type: 'Rifle', seenIn: 2, flags: ['Bolo'], tint: 'Default' },
];
- weaponOptions = [
- { model: "weapon_heavypistol", label: "Schwere Pistole" },
- { model: "weapon_sniperrifle", label: "Jagdgewehr" },
- { model: "weapon_ceramicpistol", label: "Keramikpistole" },
- { model: "weapon_doubleaction", label: "Doppellauf-Revolver" },
- { model: "weapon_navyrevolver", label: "Marine-Revolver" },
- { model: "weapon_musket", label: "Muskete" },
- ];
loading = false;
return;
}
@@ -184,7 +176,6 @@
fetchNui(NUI_EVENTS.WEAPON.GET_WEAPONS),
fetchNui<{ weapons: { model: string; label: string }[] }>(NUI_EVENTS.WEAPON.GET_WEAPON_CONFIG, {}, { weapons: [] }),
]);
- console.log('configRes:', JSON.stringify(configRes));
weapons = Array.isArray(weaponsRes.weapons) ? weaponsRes.weapons : [];
weaponOptions = configRes.weapons ?? [];
} catch (error) {
@@ -211,7 +202,7 @@
if (!addWeaponForm.weaponModel.trim() || !addWeaponForm.serial.trim()) return;
if (isEnvBrowser()) {
- addWeaponForm = { weaponModel: "", serial: "", owner: "", ownerName: "", notes: "" };
+ addWeaponForm = { weaponModel: "", serial: "", owner: "", ownerName: "", notes: addWeaponForm.notes.trim() };
showAddWeaponModal = false;
return;
}
@@ -229,7 +220,7 @@
if (response?.success) {
globalNotifications.success("Weapon saved successfully");
refreshWeapons()
- addWeaponForm = { weaponModel: "", serial: "", owner: "", ownerName: "", notes: "" };
+ addWeaponForm = { weaponModel: "", serial: "", owner: "", ownerName: "", notes: addWeaponForm.notes.trim() };
showAddWeaponModal = false;
} else {
globalNotifications.error(response?.message || "Failed to save weapon");
From 871589ea5fc27b5209fddf0d04a773cc299fabf1 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:19:48 +0200
Subject: [PATCH 012/144] added `flags` JSON DEFAULT NULL,
added `flags` JSON DEFAULT NULL,
---
sql/qbcore.sql | 1 +
sql/qbx.sql | 1 +
2 files changed, 2 insertions(+)
diff --git a/sql/qbcore.sql b/sql/qbcore.sql
index e7a47ba4..27d40e0d 100644
--- a/sql/qbcore.sql
+++ b/sql/qbcore.sql
@@ -566,6 +566,7 @@ CREATE TABLE IF NOT EXISTS `mdt_weapons` (
`information` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`weaponClass` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`weaponModel` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `flags` JSON DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `unique_serial` (`serial`),
KEY `FK_mdt_weapons_mdt_profiles` (`owner`),
diff --git a/sql/qbx.sql b/sql/qbx.sql
index 3cd9d15a..b148f9c4 100644
--- a/sql/qbx.sql
+++ b/sql/qbx.sql
@@ -566,6 +566,7 @@ CREATE TABLE IF NOT EXISTS `mdt_weapons` (
`information` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`weaponClass` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`weaponModel` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `flags` JSON DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `unique_serial` (`serial`),
KEY `FK_mdt_weapons_mdt_profiles` (`owner`),
From 6a7b918aeec1c65b1a41b423ecec27496a8b9d1c Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:21:06 +0200
Subject: [PATCH 013/144] changes to Weapons.svelte
changes to Weapons.svelte
---
web/src/pages/Weapons.svelte | 216 +++++++++++++++++++++++++++++++----
1 file changed, 196 insertions(+), 20 deletions(-)
diff --git a/web/src/pages/Weapons.svelte b/web/src/pages/Weapons.svelte
index 90c14e9d..00dd63c3 100644
--- a/web/src/pages/Weapons.svelte
+++ b/web/src/pages/Weapons.svelte
@@ -6,6 +6,11 @@
import { globalNotifications } from "../services/notificationService.svelte";
import Pagination from "../components/Pagination.svelte";
+ interface WeaponFlag {
+ type: string;
+ info: string;
+ }
+
interface Weapon {
id: number;
serial: string;
@@ -18,7 +23,7 @@
image: string;
type: string;
seenIn: number;
- flags: string[];
+ flags: WeaponFlag[];
tint: string;
}
@@ -41,6 +46,13 @@
let weaponHistory = $state([]);
let historyLoading = $state(false);
+ let newFlag = $state({ type: "", info: "" });
+ const PRESET_FLAGS = ["Stolen", "Wanted"];
+
+ const FLAG_TEMPLATES: Record = {
+ "Stolen": "Reported stolen on [DATE] by [NAME].",
+ "Wanted": "Wanted in connection with [CASE/REASON] as of [DATE].",
+ };
let weaponPage = $state(1);
let weaponPerPage = $state(25);
@@ -66,14 +78,15 @@
weaponPage = 1;
});
- function getFlagClass(flag: string): string {
- switch (flag) {
- case "Active Warrant":
- case "Dangerous":
+ function onFlagTypeChange() {
+ newFlag.info = FLAG_TEMPLATES[newFlag.type] ?? "";
+ }
+
+ function getFlagClass(flag: WeaponFlag): string {
+ switch (flag.type) {
+ case "Stolen":
+ case "Wanted":
return "pill pill-red";
- case "Bolo":
- case "Flight Risk":
- return "pill pill-orange";
default:
return "pill pill-grey";
}
@@ -106,7 +119,55 @@
}
}
+ async function addFlag() {
+ if (!newFlag.type || !selectedWeapon) return;
+
+ const currentFlags = selectedWeapon.flags ?? [];
+ if (currentFlags.some(f => f.type === newFlag.type)) return;
+
+ const updated = [...currentFlags, { type: newFlag.type, info: newFlag.info.trim() }];
+
+ if (isEnvBrowser()) {
+ selectedWeapon = { ...selectedWeapon, flags: updated };
+ newFlag = { type: "", info: "" };
+ return;
+ }
+
+ const response = await fetchNui<{ success: boolean }>(
+ NUI_EVENTS.WEAPON.SAVE_WEAPON_FLAGS,
+ { serial: selectedWeapon.serial, flags: updated },
+ );
+ if (response?.success) {
+ selectedWeapon = { ...selectedWeapon, flags: updated };
+ newFlag = { type: "", info: "" };
+ } else {
+ globalNotifications.error("Failed to save flag");
+ }
+ }
+
+ async function removeFlag(type: string) {
+ if (!selectedWeapon) return;
+ const currentFlags = selectedWeapon.flags ?? [];
+ const updated = currentFlags.filter(f => f.type !== type);
+
+ if (isEnvBrowser()) {
+ selectedWeapon = { ...selectedWeapon, flags: updated };
+ return;
+ }
+
+ const response = await fetchNui<{ success: boolean }>(
+ NUI_EVENTS.WEAPON.SAVE_WEAPON_FLAGS,
+ { serial: selectedWeapon.serial, flags: updated },
+ );
+ if (response?.success) {
+ selectedWeapon = { ...selectedWeapon, flags: updated };
+ } else {
+ globalNotifications.error("Failed to remove flag");
+ }
+ }
+
function closeWeapon() {
+ refreshWeapons()
selectedWeapon = null;
weaponHistory = [];
}
@@ -114,10 +175,10 @@
onMount(async () => {
if (isEnvBrowser()) {
weapons = [
- { id: 1, serial: 'WPN-48291', scratched: false, owner: 'Marcus Johnson', information: 'Registered service weapon', weaponClass: 'Pistol', weaponModel: 'WEAPON_PISTOL', name: 'Pistol', image: '', type: 'Handgun', seenIn: 3, flags: [], tint: 'Default' },
- { id: 2, serial: 'WPN-73820', scratched: true, owner: 'Unknown', information: 'Serial scratched off - found at crime scene', weaponClass: 'SMG', weaponModel: 'WEAPON_SMG', name: 'SMG', image: '', type: 'Submachine Gun', seenIn: 1, flags: ['Dangerous'], tint: 'Army' },
+ { id: 1, serial: 'WPN-48291', scratched: false, owner: 'Marcus Johnson', information: 'Registered service weapon', weaponClass: 'Pistol', weaponModel: 'WEAPON_PISTOL', name: 'Pistol', image: '', type: 'Handgun', seenIn: 3, flags: [{ type: "Stolen", info: "reported Stolen" }], tint: 'Default' },
+ { id: 2, serial: 'WPN-73820', scratched: true, owner: 'Unknown', information: 'Serial scratched off - found at crime scene', weaponClass: 'SMG', weaponModel: 'WEAPON_SMG', name: 'SMG', image: '', type: 'Submachine Gun', seenIn: 1, flags: [{ type: "Stolen", info: "reported Stolen" }], tint: 'Army' },
{ id: 3, serial: 'WPN-55194', scratched: false, owner: 'Sarah Williams', information: 'Licensed for personal protection', weaponClass: 'Pistol', weaponModel: 'WEAPON_COMBATPISTOL', name: 'Combat Pistol', image: '', type: 'Handgun', seenIn: 0, flags: [], tint: 'Default' },
- { id: 4, serial: 'WPN-10477', scratched: false, owner: 'David Chen', information: 'Hunting rifle, valid license', weaponClass: 'Rifle', weaponModel: 'WEAPON_MUSKET', name: 'Musket', image: '', type: 'Rifle', seenIn: 2, flags: ['Bolo'], tint: 'Default' },
+ { id: 4, serial: 'WPN-10477', scratched: false, owner: 'David Chen', information: 'Hunting rifle, valid license', weaponClass: 'Rifle', weaponModel: 'WEAPON_MUSKET', name: 'Musket', image: '', type: 'Rifle', seenIn: 2, flags: [{ type: "Wanted", info: "Found Bullets - Report #1337" }], tint: 'Default' },
];
loading = false;
return;
@@ -164,7 +225,7 @@
Scratched
{/if}
{#each selectedWeapon.flags as flag}
- {flag}
+ {flag.type}
{/each}
@@ -202,16 +263,45 @@
{/if}
- {#if selectedWeapon.flags && selectedWeapon.flags.length}
-
-
Flags
-
+
+
Flags
+
+ {#if selectedWeapon.flags?.length}
+
{#each selectedWeapon.flags as flag}
- {flag}
+
+ {flag.type}
+ {#if flag.info}
+ — {flag.info}
+ {/if}
+
+
{/each}
+ {/if}
+
+
+
+ { if (e.key === 'Enter') addFlag(); }}
+ />
+
- {/if}
+
Ownership History
@@ -284,8 +374,8 @@
{weapon.type}
{weapon.tint || 'Default'}
- {#each weapon.flags as flag}
- {flag}
+ {#each weapon.flags || [] as flag}
+ {flag.type}
{/each}
@@ -693,12 +783,98 @@
padding: 16px;
}
+ /* ===== Tags/Flags ===== */
.flags-row {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
+ .tag-remove {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ color: inherit;
+ opacity: 0.4;
+ cursor: pointer;
+ padding: 0;
+ transition: opacity 0.1s;
+ line-height: 1;
+ }
+
+ .tag-remove:hover {
+ opacity: 1;
+ }
+
+ .flag-add-row {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ margin-top: 8px;
+ }
+
+ .flag-add-row .form-select {
+ width: 120px;
+ flex-shrink: 0;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 3px;
+ padding: 5px 8px;
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 10px;
+ font-family: inherit;
+ }
+
+ .flag-add-row .form-select:focus {
+ outline: none;
+ border-color: rgba(255, 255, 255, 0.1);
+ }
+
+ .flag-add-row .form-input {
+ flex: 1;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 3px;
+ padding: 5px 8px;
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 11px;
+ font-family: inherit;
+ }
+
+ .flag-add-row .form-input:focus {
+ outline: none;
+ border-color: rgba(255, 255, 255, 0.1);
+ }
+
+ .flag-add-row .form-input::placeholder {
+ color: rgba(255, 255, 255, 0.2);
+ }
+
+ .add-tag-btn {
+ background: rgba(16, 185, 129, 0.06);
+ color: rgba(52, 211, 153, 0.7);
+ border: 1px solid rgba(16, 185, 129, 0.1);
+ border-radius: 3px;
+ padding: 4px 10px;
+ font-size: 10px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.1s;
+ white-space: nowrap;
+ }
+
+ .add-tag-btn:hover:not(:disabled) {
+ background: rgba(16, 185, 129, 0.12);
+ color: rgba(110, 231, 183, 0.9);
+ }
+
+ .add-tag-btn:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ }
+
/* ===== History ===== */
.history-list {
display: flex;
From 3934eda1b90eb04566cf89762aa31d429c205e87 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:21:44 +0200
Subject: [PATCH 014/144] added RegisterNUICallback
added RegisterNUICallback
---
client/backend/weapons.lua | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/client/backend/weapons.lua b/client/backend/weapons.lua
index fb7fa3c4..79532dae 100644
--- a/client/backend/weapons.lua
+++ b/client/backend/weapons.lua
@@ -14,6 +14,12 @@ RegisterNUICallback('getWeaponBolos', function(data, cb)
cb(result)
end)
+RegisterNUICallback('saveWeaponFlags', function(data, cb)
+ local flags = type(data.flags) == 'table' and data.flags or json.decode(data.flags) or {}
+ local result = ps.callback(resourceName .. ':server:saveWeaponFlags', data.serial, flags)
+ cb(result or { success = false })
+end)
+
RegisterNUICallback('getWeaponOwnershipHistory', function(data, cb)
if not MDTOpen then cb({}) return end
if not data or not data.serial then
From a20df2a4798d0db10249f32b9c90e0608935986a Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:22:16 +0200
Subject: [PATCH 015/144] added ps-mdt:server:saveWeaponFlags
added ps-mdt:server:saveWeaponFlags
---
server/backend/weapons.lua | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/server/backend/weapons.lua b/server/backend/weapons.lua
index aa6d9e79..903199bb 100644
--- a/server/backend/weapons.lua
+++ b/server/backend/weapons.lua
@@ -141,6 +141,7 @@ ps.registerCallback('ps-mdt:server:getWeapons', function(source)
name = (QBCore and QBCore.Shared and QBCore.Shared.Weapons and QBCore.Shared.Weapons[GetHashKey(v.weaponModel)] and QBCore.Shared.Weapons[GetHashKey(v.weaponModel)].label) or v.weaponModel,
image = 'https://docs.fivem.net/weapons/' .. v.weaponModel:upper() .. '.png',
type = class[modelLower] and class[modelLower].type or 'unknown',
+ flags = v.flags and json.decode(v.flags) or {},
}
table.insert(newData, weaponInfo)
end
@@ -174,6 +175,30 @@ ps.registerCallback(resourceName .. ':server:getWeaponOwnershipHistory', functio
return rows or {}
end)
+ps.registerCallback(resourceName .. ':server:saveWeaponFlags', function(source, serial, flags)
+ local src = source
+ if not CheckAuth(src) then return { success = false } end
+ if not serial or serial == '' then return { success = false } end
+
+ local decoded
+ if type(flags) == 'table' then
+ decoded = flags
+ elseif type(flags) == 'string' then
+ decoded = json.decode(flags) or {}
+ else
+ decoded = {}
+ end
+
+ local encoded = json.encode(decoded)
+ local affected = MySQL.update.await('UPDATE mdt_weapons SET flags = ? WHERE serial = ?', { encoded, serial })
+
+ if affected and affected > 0 then
+ return { success = true }
+ else
+ return { success = false, message = 'Database error' }
+ end
+end)
+
-- Save/Edit Weapon Info (from NUI)
ps.registerCallback(resourceName .. ':server:saveWeaponInfo', function(source, payload)
local src = source
From 7bfa4854e28f8604a9618f51357bc6497b7cf45b Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:22:56 +0200
Subject: [PATCH 016/144] Added `SAVE_WEAPON_FLAGS` to `NUI_EVENTS.WEAPON`
Added `SAVE_WEAPON_FLAGS` to `NUI_EVENTS.WEAPON`
---
web/src/constants/nuiEvents.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/web/src/constants/nuiEvents.ts b/web/src/constants/nuiEvents.ts
index e5175863..99a39b3a 100644
--- a/web/src/constants/nuiEvents.ts
+++ b/web/src/constants/nuiEvents.ts
@@ -97,6 +97,7 @@ export const NUI_EVENTS = {
GET_WEAPON: "getWeapon",
GET_WEAPON_HISTORY: "getWeaponOwnershipHistory",
UPDATE_WEAPON: "updateWeapon",
+ SAVE_WEAPON_FLAGS: "saveWeaponFlags",
},
CHARGE: {
GET_CHARGES: "getCharges",
From eedc11c68fbb5e48979045e564a99e588b920679 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Sat, 2 May 2026 00:25:21 +0200
Subject: [PATCH 017/144] changes from files to link, added hover for picture
changes from files to link, added hover for picture to make it bigger
---
.../report-editor/EvidenceManager.svelte | 642 +++++++++++-------
1 file changed, 402 insertions(+), 240 deletions(-)
diff --git a/web/src/components/report-editor/EvidenceManager.svelte b/web/src/components/report-editor/EvidenceManager.svelte
index 0949dbce..535cf8ec 100644
--- a/web/src/components/report-editor/EvidenceManager.svelte
+++ b/web/src/components/report-editor/EvidenceManager.svelte
@@ -1,17 +1,20 @@
+
+{#if addImgOpen}
+
+
+
{ if (e.target === e.currentTarget) addImgOpen = false; }}
+ >
+
+
+{/if}
+
+
+{#if lightboxOpen}
+
+
+
{ if (e.target === e.currentTarget) lightboxOpen = false; }}
+ >
+
+
+
+

+
+
+
+{/if}
+
+
+ {#if galleryOpen}
+
+
+
{ if (e.target === e.currentTarget) galleryOpen = false; }}>
+
e.stopPropagation()}>
+
+
+
+ {#if selectedProfile?.image && !citizenImageBroken}
+
Profile Photo
+
+
+
+
+

{ galleryOpen = false; openLightbox(selectedProfile!.image!); }} />
+
+
+
Gallery
+ {/if}
+
+ {#if galleryImages.length === 0}
+
No gallery images
+ {:else}
+
+ {#each galleryImages as img}
+
+
+
+

{ galleryOpen = false; openLightbox(img); }} />
+
+
+ {/each}
+
+ {/if}
+
+
+
+ {/if}
+
+
+ {#if galleryAddOpen}
+
+
+
{ if (e.target === e.currentTarget) galleryAddOpen = false; }}>
+
e.stopPropagation()}>
+
+
+
+
+
+ {/if}
+
+
+ {#if lightboxOpen}
+
+
+
lightboxOpen = false}>
+
e.stopPropagation()}>
+
+

+
+
+ {/if}
+
{#if vehicleDetail || vehicleDetailLoading}
@@ -992,6 +1095,7 @@
{/if}
+
{#if showIssueLicenseModal}
(showIssueLicenseModal = false)}>
@@ -1016,11 +1120,12 @@
{/if}
+
{:else}
-
+
@@ -1032,14 +1137,7 @@
No citizen records available.
{:else}
{#each filteredCitizens as citizen (citizen.id)}
@@ -1087,11 +1185,81 @@
{/if}
-
-
+
+ /* Gallery & Lightbox */
+ .gallery-card { width: min(560px, 92vw); max-height: 80vh; display: flex; flex-direction: column; }
+ .gallery-body { padding: 12px; overflow-y: auto; flex: 1; min-height: 0; }
+ .gallery-section-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: rgba(255,255,255,0.25); margin-bottom: 6px; }
+ .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
+ .gallery-item { position: relative; aspect-ratio: 1; border-radius: 4px; overflow: hidden; border: 1px solid rgba(255,255,255,0.06); }
+ .gallery-item:hover .gallery-thumb { transform: scale(1.04); }
+ .gallery-thumb { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.2s ease; cursor: zoom-in; }
+ .gallery-delete-btn { position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; background: rgba(239,68,68,0.8); border: none; border-radius: 50%; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.15s; }
+ .gallery-item:hover .gallery-delete-btn { opacity: 1; }
+ .gallery-add-btn { display: flex; align-items: center; gap: 4px; background: rgba(16,185,129,0.06); border: 1px solid rgba(16,185,129,0.1); border-radius: 3px; padding: 3px 8px; color: rgba(52,211,153,0.7); font-size: 10px; font-weight: 600; cursor: pointer; transition: all 0.1s; }
+ .gallery-add-btn:hover { background: rgba(16,185,129,0.12); color: rgba(110,231,183,0.9); }
+
+ .lightbox-overlay { background: rgba(0,0,0,0.85); }
+ .lightbox-card { position: relative; max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; padding-top: 40px; }
+ .lightbox-close { position: absolute; top: 0; right: 0; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.12); border-radius: 4px; color: rgba(255,255,255,0.6); cursor: pointer; padding: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.1s; z-index: 10; }
+ .lightbox-close:hover { background: rgba(255,255,255,0.2); color: #fff; }
+ .lightbox-img { max-width: 90vw; max-height: calc(90vh - 40px); object-fit: contain; display: block; border-radius: 4px; }
+
\ No newline at end of file
From c960ae00c759d31fa95caef6327344492d725460 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Sat, 2 May 2026 00:31:23 +0200
Subject: [PATCH 022/144] added new callbacks
added new callbacks
---
web/src/constants/nuiEvents.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/web/src/constants/nuiEvents.ts b/web/src/constants/nuiEvents.ts
index e5175863..b51c306e 100644
--- a/web/src/constants/nuiEvents.ts
+++ b/web/src/constants/nuiEvents.ts
@@ -83,6 +83,8 @@ export const NUI_EVENTS = {
ADD_SUSPECT_FINGERPRINT: "addSuspectFingerprint",
UPDATE_CITIZEN_DNA: "updateCitizenDNA",
UPDATE_CITIZEN_FINGERPRINT: "updateCitizenFingerprint",
+ ADD_CITIZEN_GALLERY: 'addCitizenGallery',
+ REMOVE_CITIZEN_GALLERY: 'removeCitizenGallery',
},
VEHICLE: {
GET_VEHICLES: "getVehicles",
From 6702b3243c33ac8d90e6b34487904d167caf76f3 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Sat, 2 May 2026 00:32:21 +0200
Subject: [PATCH 023/144] removed log entry for profile pic
removed log entry for profile pic
- fivemanage doesnt support links, only base64, so removed check for fivemanageupload
---
server/fivemanage.lua | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/server/fivemanage.lua b/server/fivemanage.lua
index c1c3a012..80b71280 100644
--- a/server/fivemanage.lua
+++ b/server/fivemanage.lua
@@ -140,15 +140,15 @@ ps.registerCallback(resourceName .. ':server:triggerSuspectMugshot', function(so
end)
-- Upload a profile photo for a suspect via base64 (from MDT UI)
-ps.registerCallback(resourceName .. ':server:uploadSuspectPhoto', function(source, citizenid, base64Image)
- if not citizenid or not base64Image then
+ps.registerCallback(resourceName .. ':server:uploadSuspectPhoto', function(source, citizenid, imageUrl)
+ if not citizenid or not imageUrl then
return { success = false, message = 'Missing data' }
end
- local imageUrl, uploadError = FiveManageUpload(base64Image, 'suspect_' .. citizenid .. '.png')
- if not imageUrl then
- return { success = false, message = 'Upload failed: ' .. (uploadError or 'Unknown error') }
- end
+ -- local imageUrl, uploadError = FiveManageUpload(base64Image, 'suspect_' .. citizenid .. '.png')
+ -- if not imageUrl then
+ -- return { success = false, message = 'Upload failed: ' .. (uploadError or 'Unknown error') }
+ -- end
-- Ensure profile exists
if not EnsureProfileExists(citizenid) then
From 2adfc22b89f3ac81585f931cebd1bfbd0712a165 Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Sat, 2 May 2026 00:33:14 +0200
Subject: [PATCH 024/144] added title and images
added title and images, fixed evidence in reports
---
web/src/pages/ReportEditor.svelte | 79 ++++++++++---------------------
1 file changed, 26 insertions(+), 53 deletions(-)
diff --git a/web/src/pages/ReportEditor.svelte b/web/src/pages/ReportEditor.svelte
index 96ffe6c1..9f2f9376 100644
--- a/web/src/pages/ReportEditor.svelte
+++ b/web/src/pages/ReportEditor.svelte
@@ -15,7 +15,6 @@
import VehiclesManager from "../components/report-editor/VehiclesManager.svelte";
import ReportTextEditor from "../components/report-editor/ReportTextEditor.svelte";
import PersonSearchModal from "../components/report-editor/PersonSearchModal.svelte";
- import ImageUploadModal from "../components/report-editor/ImageUploadModal.svelte";
// Import services
import { createReportService } from "../services/reportService.svelte";
@@ -249,7 +248,11 @@
...r,
evidence: r.evidence.map((e) => ({
...e,
- images: e.images.filter((img) => !img.startsWith("data:")),
+ images: e.images.filter((img) =>
+ typeof img === "string"
+ ? !img.startsWith("data:")
+ : !img.url?.startsWith("data:")
+ ),
})),
};
}
@@ -408,7 +411,16 @@
// Initialize report
const initializedReport =
- await reportService.initializeReport(reportId);
+ await reportService.initializeReport(reportId);
+
+ initializedReport.evidence = (initializedReport.evidence ?? []).map((e: any) => ({
+ ...e,
+ title: e.title ?? '',
+ images: (e.images ?? []).map((img: any) =>
+ typeof img === 'string' ? img : (img.url ?? '')
+ )
+ }));
+
report = initializedReport;
// Load persisted data for this instance after base initialization
@@ -702,7 +714,6 @@
let photoUploadInput: HTMLInputElement | undefined = $state();
let photoUploadCitizenId: string = $state("");
let isUploadingPhoto: boolean = $state(false);
- let isUploadingEvidence: boolean = $state(false);
function openPhotoUpload(suspect: Report["involved"]["suspects"][number]) {
if (!suspect.citizenid) return;
@@ -753,41 +764,6 @@
input.value = "";
}
- async function uploadImageHandler(file: File, evidenceId: string) {
- if (isUploadingEvidence) return;
- isUploadingEvidence = true;
-
- try {
- const numericId = Number(evidenceId);
- if (numericId && !isNaN(numericId)) {
- // Evidence already saved to DB - upload directly
- const imageUrl = await evidenceService.uploadImage(file, evidenceId);
- const evidenceIndex = report.evidence.findIndex((e) => e.id === evidenceId);
- if (evidenceIndex !== -1) {
- report.evidence[evidenceIndex] = evidenceService.addImageToEvidence(
- report.evidence[evidenceIndex],
- imageUrl,
- );
- }
- } else {
- // Evidence is in-memory (not yet saved) - compress and store as data URL
- const dataUrl = await compressImage(file);
- const evidenceIndex = report.evidence.findIndex((e) => e.id === evidenceId);
- if (evidenceIndex !== -1) {
- report.evidence[evidenceIndex] = evidenceService.addImageToEvidence(
- report.evidence[evidenceIndex],
- dataUrl,
- );
- }
- }
- reportEditorUI.closeImageUpload();
- } catch (error: any) {
- showStatus(error?.message || "Failed to upload image", "error");
- } finally {
- isUploadingEvidence = false;
- }
- }
-
async function linkEvidenceToCase(evidenceId: string, caseId: string) {
const numericEvidenceId = Number(evidenceId);
const numericCaseId = Number(caseId);
@@ -846,7 +822,17 @@
isPersistenceEnabled = false;
persistence.cancelDebouncedSave();
- await reportService.saveReport(report);
+ const reportToSave = {
+ ...report,
+ evidence: report.evidence.map(e => ({
+ ...e,
+ images: (e.images ?? []).map((img: any) => ({
+ url: typeof img === 'string' ? img : (img.url ?? ''),
+ }))
+ }))
+ } as unknown as Report;
+
+ await reportService.saveReport(reportToSave);
// Clear persisted data
persistence.clearPersistedData();
@@ -1022,8 +1008,6 @@
onAddEvidence={handlers.handleAddEvidence}
onRemoveEvidence={handlers.handleRemoveEvidence}
onUpdateEvidence={handlers.handleUpdateEvidence}
- onOpenImageUpload={(evidenceId) =>
- reportEditorUI.openImageUpload(evidenceId)}
onRemoveImage={(evidenceId, imageIndex) => {
report = reportService.removeImageFromEvidence(
report,
@@ -1091,17 +1075,6 @@
onClose={() => reportEditorUI.closeVictimSearch()}
/>
-
{
- if (reportEditorUI.state.selectedEvidenceId) {
- uploadImageHandler(file, reportEditorUI.state.selectedEvidenceId);
- }
- }}
- onClose={() => reportEditorUI.closeImageUpload()}
-/>
-
Date: Sat, 2 May 2026 00:34:04 +0200
Subject: [PATCH 025/144] added title and images
added title and images, fixed evidence
From 4aebbbadf3ff3e8ba535d67e3dec9d5c9d854abc Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Sat, 2 May 2026 00:35:00 +0200
Subject: [PATCH 026/144] added title and images, fixed evidence
added title and images, fixed evidence
---
web/src/services/reportService.svelte.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/web/src/services/reportService.svelte.ts b/web/src/services/reportService.svelte.ts
index 87e4c481..119388ad 100644
--- a/web/src/services/reportService.svelte.ts
+++ b/web/src/services/reportService.svelte.ts
@@ -85,6 +85,7 @@ export function createReportService() {
...report,
id: report.id ?? report.reportId,
};
+
const response = await fetchNui<{ success: boolean; message?: string; error?: string; reportId?: string }>(
NUI_EVENTS.REPORT.SAVE_REPORT,
reportToSave,
From 0c54f1450c142bb89b3f241ac34ff46c1e5f672f Mon Sep 17 00:00:00 2001
From: LeSiiN <103898231+LeSiiN@users.noreply.github.com>
Date: Mon, 4 May 2026 01:50:25 +0200
Subject: [PATCH 027/144] added little icon in vehicles overview and added
edit/zoom button
added little icon in vehicles overview and added edit/zoom button
---
web/src/pages/Vehicles.svelte | 199 ++++++++++++++++++++++++++++++----
1 file changed, 175 insertions(+), 24 deletions(-)
diff --git a/web/src/pages/Vehicles.svelte b/web/src/pages/Vehicles.svelte
index f0dd52a0..d75a4964 100644
--- a/web/src/pages/Vehicles.svelte
+++ b/web/src/pages/Vehicles.svelte
@@ -48,6 +48,48 @@
let vehicleDetailLoading = $state(false);
let vehicleDetailError = $state(null);
let vehicleSaving = $state(false);
+ let imageModalOpen = $state(false);
+ let imageUrlInput = $state("");
+ let imageSaving = $state(false);
+ let vehicleLightboxOpen = $state(false);
+ let vehicleImageBroken = $state(false)
+ $effect(() => {
+ if (selectedVehicle) vehicleImageBroken = false;
+ });
+
+ function openVehicleLightbox() {
+ if (!selectedVehicle?.image || selectedVehicle.image.startsWith('https://docs.fivem.net')) return;
+ vehicleLightboxOpen = true;
+ }
+
+ function openImageModal() {
+ imageUrlInput = selectedVehicle?.image?.startsWith('https://docs.fivem.net') ? "" : (selectedVehicle?.image ?? "");
+ imageModalOpen = true;
+ }
+
+ async function saveVehicleImage() {
+ const url = imageUrlInput.trim();
+ if (!url || !selectedVehicle || imageSaving) return;
+ imageSaving = true;
+ try {
+ const response = await fetchNui(NUI_EVENTS.VEHICLE.UPDATE_VEHICLE, {
+ plate: selectedVehicle.plate,
+ image: url,
+ });
+ if (response?.success) {
+ selectedVehicle = { ...selectedVehicle, image: url };
+ vehicleList = vehicleList.map(v => v.plate === selectedVehicle?.plate ? { ...v, image: url } : v);
+ globalNotifications.success("Vehicle image updated");
+ imageModalOpen = false;
+ } else {
+ globalNotifications.error(response?.message || "Failed to update image");
+ }
+ } catch {
+ globalNotifications.error("Failed to update image");
+ }
+ imageSaving = false;
+ }
+
let vehicleForm = $state({
points: 0,
status: "valid",
@@ -304,11 +346,22 @@
- {#if selectedVehicle.image}
-

- {:else}
-
+
+ {#if selectedVehicle.image && !selectedVehicle.image.startsWith('https://docs.fivem.net') && !vehicleImageBroken}
+
+
+

vehicleImageBroken = true}
+ />
{/if}
+
Owner
@@ -417,6 +470,52 @@
{/if}
+ {#if imageModalOpen}
+
+
+
{ if (e.target === e.currentTarget) imageModalOpen = false; }}>
+
e.stopPropagation()}>
+
+
+
Image URL
+
{ if (e.key === 'Enter') saveVehicleImage(); if (e.key === 'Escape') imageModalOpen = false; }}
+ />
+
+
+ Use FiveManage for permanent links.
+
+
+
+
+
+ {/if}
+ {#if vehicleLightboxOpen}
+
+
+
vehicleLightboxOpen = false}>
+
e.stopPropagation()}>
+
+

+
+
+ {/if}
{:else}
@@ -441,6 +540,7 @@