diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5be087cf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 + +[*.lua] +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff05c893..af429ea9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea .vscode node_modules -dist -build \ No newline at end of file +/dist +/fxmanifest.lua +.yarn.installed diff --git a/web/.prettierrc b/.prettierrc similarity index 75% rename from web/.prettierrc rename to .prettierrc index cb37e3c4..9a003aaf 100644 --- a/web/.prettierrc +++ b/.prettierrc @@ -5,5 +5,6 @@ "tabWidth": 2, "semi": true, "bracketSpacing": true, - "trailingComma": "es5" + "trailingComma": "es5", + "plugins": [] } diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..ec1595e5 --- /dev/null +++ b/bun.lock @@ -0,0 +1,623 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "ox_mdt", + "dependencies": { + "@communityox/ox_core": "^1.5.8", + "@communityox/ox_lib": "^3.32.1", + "@communityox/oxmysql": "^1.4.3", + "@emotion/react": "^11.10.6", + "@mantine/core": "^6.0.19", + "@mantine/dates": "^6.0.19", + "@mantine/form": "^6.0.19", + "@mantine/hooks": "^6.0.19", + "@mantine/modals": "^6.0.19", + "@mantine/tiptap": "^6.0.19", + "@tabler/icons-react": "^2.31.0", + "@tanstack/query-core": "^4.33.0", + "@tanstack/react-query": "^4.33.0", + "@tiptap/extension-highlight": "^2.1.6", + "@tiptap/extension-link": "^2.1.6", + "@tiptap/extension-placeholder": "^2.1.6", + "@tiptap/extension-text-align": "^2.1.6", + "@tiptap/extension-underline": "^2.1.6", + "@tiptap/pm": "^2.1.6", + "@tiptap/react": "^2.1.6", + "@tiptap/starter-kit": "^2.1.6", + "@types/leaflet": "^1.9.3", + "@types/node": "^18.16.2", + "dayjs": "^1.11.9", + "fast-printf": "^1.6.9", + "jotai": "^2.3.1", + "jotai-optics": "^0.3.1", + "jotai-tanstack-query": "^0.7.1", + "leaflet": "^1.9.4", + "mantine-datatable": "^2.9.12", + "optics-ts": "^2.4.1", + "prettier": "^2.8.8", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", + "react-router-dom": "^6.15.0", + }, + "devDependencies": { + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.0.4", + "typescript": "^4.9.5", + "vite": "^4.4.9", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + + "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@communityox/ox_core": ["@communityox/ox_core@1.5.8", "", { "dependencies": { "@biomejs/biome": "^1.9.4", "@communityox/ox_lib": "^3.29.0", "@nativewrappers/fivem": "^0.0.86", "@nativewrappers/server": "^0.0.86", "mariadb": "^3.4.1" } }, "sha512-Yz3yYfNZ/QNhWy9rHAFH25zMlNZepR56NxHhr1A5hTitVHeWGtNyvePIyBehdEo8lPEb4FSXlnSe+H0cdUnxYw=="], + + "@communityox/ox_lib": ["@communityox/ox_lib@3.32.1", "", { "dependencies": { "@nativewrappers/fivem": "^0.0.103", "csstype": "^3.1.3", "fast-printf": "^1.6.9", "typescript": "^5.4.2" } }, "sha512-WH8LXA0HLS4IuUzhfTyFQptJ5/lHHN/Xm0xAJ/guGGxd0q8JC8pDr7t5yD84ZOzKczOqMKsDkvEZmHvqUA0gGA=="], + + "@communityox/oxmysql": ["@communityox/oxmysql@1.4.3", "", {}, "sha512-PWaG9JKpJea/n6QocAyZR/q5ZGiBM+tzxo3gXp5tg8pDqFqkS/s7eBkS0W02lZbw/FrhcIhEDztoUI9rHXxMdQ=="], + + "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], + + "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], + + "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + + "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + + "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], + + "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], + + "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], + + "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + + "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], + + "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], + + "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react": ["@floating-ui/react@0.19.2", "", { "dependencies": { "@floating-ui/react-dom": "^1.3.0", "aria-hidden": "^1.1.3", "tabbable": "^6.0.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@1.3.0", "", { "dependencies": { "@floating-ui/dom": "^1.2.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@mantine/core": ["@mantine/core@6.0.22", "", { "dependencies": { "@floating-ui/react": "^0.19.1", "@mantine/styles": "6.0.22", "@mantine/utils": "6.0.22", "@radix-ui/react-scroll-area": "1.0.2", "react-remove-scroll": "^2.5.5", "react-textarea-autosize": "8.3.4" }, "peerDependencies": { "@mantine/hooks": "6.0.22", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-6kv0eY7n565fyjgS20qUYeCSxg3f1TJ5vurzbP1HHtFXXKSY0bYoqqDoHipFCt6NxsPQGeiC6cC0c/IWIlxoKQ=="], + + "@mantine/dates": ["@mantine/dates@6.0.22", "", { "dependencies": { "@mantine/utils": "6.0.22" }, "peerDependencies": { "@mantine/core": "6.0.22", "@mantine/hooks": "6.0.22", "dayjs": ">=1.0.0", "react": ">=16.8.0" } }, "sha512-RwZzaRtyCdwXWrszjoDFUrYdy2s6sAZgXZzp+ytp0KJDm63+H+4ri1Qkv7bWKVBgrTP7alsxCIGHV2weEOZKog=="], + + "@mantine/form": ["@mantine/form@6.0.22", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.5" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-M73HwndjrbPekswcs/DI8ez7E4etWsuE25omdMDhhUTsTMPD/k/WB38yr7AMqI4nCuy2kGwQ5KL70s0fnkFfKw=="], + + "@mantine/hooks": ["@mantine/hooks@6.0.22", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-e10//QTN2sAmC4Ryeu5X5L/TsxnrjXMOaGq3dxFPIPsCSwLzyxqySfjzVViWmoPWAj0Ak9MvE2MHFjzmOpA80w=="], + + "@mantine/modals": ["@mantine/modals@6.0.22", "", { "dependencies": { "@mantine/utils": "6.0.22" }, "peerDependencies": { "@mantine/core": "6.0.22", "@mantine/hooks": "6.0.22", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1k4bc6eFBGwKLaySXqAFCxyAHKv+p5bOh06RaYx9S9KyospR61M8Nw6BE3q7CdQRH2MQp8vgVue+h6y7pcWFFQ=="], + + "@mantine/styles": ["@mantine/styles@6.0.22", "", { "dependencies": { "clsx": "1.1.1", "csstype": "3.0.9" }, "peerDependencies": { "@emotion/react": ">=11.9.0", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-Rud/IQp2EFYDiP4csRy2XBrho/Ct+W2/b+XbvCRTeQTmpFy/NfAKm/TWJa5zPvuv/iLTjGkVos9SHw/DteESpQ=="], + + "@mantine/tiptap": ["@mantine/tiptap@6.0.22", "", { "dependencies": { "@mantine/utils": "6.0.22" }, "peerDependencies": { "@mantine/core": "6.0.22", "@mantine/hooks": "6.0.22", "@tabler/icons-react": ">=2.1.0", "@tiptap/extension-link": "^2.0.0-beta.202", "@tiptap/react": "^2.0.0-beta.202", "react": ">=16.8.0" } }, "sha512-Mh+0sP15QCr01e2XenEmqtDoLleN26ksgsatTt+y8zy+ouGcDWfUH1IYe0yIewkA3H0U6grIiWfZcYHT59vLKg=="], + + "@mantine/utils": ["@mantine/utils@6.0.22", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-RSKlNZvxhMCkOFZ6slbYvZYbWjHUM+PxDQnupIOxIdsTZQQjx/BFfrfJ7kQFOP+g7MtpOds8weAetEs5obwMOQ=="], + + "@nativewrappers/fivem": ["@nativewrappers/fivem@0.0.86", "", {}, "sha512-NBGw8jQYBIVURfH/jbjz+6NDO+2MTyT2sR5EK7mBhMpHE/eXzIKUpRQgXB5ksxg/+A0KKWmyyGvARbqOLOb9LA=="], + + "@nativewrappers/server": ["@nativewrappers/server@0.0.86", "", {}, "sha512-WyY9pWHbmL3X4J1EHUkjWjX2mSyxOhJzDCDnLP5FVEbvtII+AV0MRuMFWvhWVSA7Im8Y8v2IQ0oNnQ8u6NM3sg=="], + + "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + + "@radix-ui/number": ["@radix-ui/number@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/number": "1.0.0", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-direction": "1.0.0", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@react-leaflet/core": ["@react-leaflet/core@2.1.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg=="], + + "@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="], + + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@tabler/icons": ["@tabler/icons@2.47.0", "", {}, "sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA=="], + + "@tabler/icons-react": ["@tabler/icons-react@2.47.0", "", { "dependencies": { "@tabler/icons": "2.47.0", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-iqly2FvCF/qUbgmvS8E40rVeYY7laltc5GUjRxQj59DuX0x/6CpKHTXt86YlI2whg4czvd/c8Ce8YR08uEku0g=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.19", "", {}, "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA=="], + + "@tanstack/react-query": ["@tanstack/react-query@4.42.2", "", { "dependencies": { "@tanstack/query-core": "4.41.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-KFqT8wb/d0bHbzvJEuD/vEU8lUSC5cZDurOMtAR3G+mUABqVWvVswQ/norjFXedjQe+nfQQ9CWrOabWcRP3TKA=="], + + "@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], + + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw=="], + + "@tiptap/extension-bold": ["@tiptap/extension-bold@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-bR7J5IwjCGQ0s3CIxyMvOCnMFMzIvsc5OVZKscTN5UkXzFsaY6muUAIqtKxayBUucjtUskm5qZowJITCeCb1/A=="], + + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@2.27.2", "", { "dependencies": { "tippy.js": "^6.3.7" }, "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-VkwlCOcr0abTBGzjPXklJ92FCowG7InU8+Od9FyApdLNmn0utRYGRhw0Zno6VgE9EYr1JY4BRnuSa5f9wlR72w=="], + + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-gmFuKi97u5f8uFc/GQs+zmezjiulZmFiDYTh3trVoLRoc2SAHOjGEB7qxdx7dsqmMN7gwiAWAEVurLKIi1lnnw=="], + + "@tiptap/extension-code": ["@tiptap/extension-code@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-7X9AgwqiIGXoZX7uvdHQsGsjILnN/JaEVtqfXZnPECzKGaWHeK/Ao4sYvIIIffsyZJA8k5DC7ny2/0sAgr2TuA=="], + + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-KgvdQHS4jXr79aU3wZOGBIZYYl9vCB7uDEuRFV4so2rYrfmiYMw3T8bTnlNEEGe4RUeAms1i4fdwwvQp9nR1Dw=="], + + "@tiptap/extension-document": ["@tiptap/extension-document@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-CFhAYsPnyYnosDC4639sCJnBUnYH4Cat9qH5NZWHVvdgtDwu8GZgZn2eSzaKSYXWH1vJ9DSlCK+7UyC3SNXIBA=="], + + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-oEu/OrktNoQXq1x29NnH/GOIzQZm8ieTQl3FK27nxfBPA89cNoH4mFEUmBL5/OFIENIjiYG3qWpg6voIqzswNw=="], + + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@2.27.2", "", { "dependencies": { "tippy.js": "^6.3.7" }, "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-GUN6gPIGXS7ngRJOwdSmtBRBDt9Kt9CM/9pSwKebhLJ+honFoNA+Y6IpVyDvvDMdVNgBchiJLs6qA5H97gAePQ=="], + + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-/c9VF1HBxj+AP54XGVgCmD9bEGYc5w5OofYCFQgM7l7PB1J00A4vOke0oPkHJnqnOOyPlFaxO/7N6l3XwFcnKA=="], + + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-kSRVGKlCYK6AGR0h8xRkk0WOFGXHIIndod3GKgWU49APuIGDiXd8sziXsSlniUsWmqgDmDXcNnSzPcV7AQ8YNg=="], + + "@tiptap/extension-heading": ["@tiptap/extension-heading@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-iM3yeRWuuQR/IRQ1djwNooJGfn9Jts9zF43qZIUf+U2NY8IlvdNsk2wTOdBgh6E0CamrStPxYGuln3ZS4fuglw=="], + + "@tiptap/extension-highlight": ["@tiptap/extension-highlight@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-ZjlktDdMjruMJFAVz0TbQf0v92Jqkc7Ri1iZJqBXuLid+r+GxUzl2CVAV7qq5yagkGQgvAG+WGsMk880HgR3MA=="], + + "@tiptap/extension-history": ["@tiptap/extension-history@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-+hSyqERoFNTWPiZx4/FCyZ/0eFqB9fuMdTB4AC/q9iwu3RNWAQtlsJg5230bf/qmyO6bZxRUc0k8p4hrV6ybAw=="], + + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-WGWUSgX+jCsbtf9Y9OCUUgRZYuwjVoieW5n6mAUohJ9/6gc6sGIOrUpBShf+HHo6WD+gtQjRd+PssmX3NPWMpg=="], + + "@tiptap/extension-italic": ["@tiptap/extension-italic@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-1OFsw2SZqfaqx5Fa5v90iNlPRcqyt+lVSjBwTDzuPxTPFY4Q0mL89mKgkq2gVHYNCiaRkXvFLDxaSvBWbmthgg=="], + + "@tiptap/extension-link": ["@tiptap/extension-link@2.27.2", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-bnP61qkr0Kj9Cgnop1hxn2zbOCBzNtmawxr92bVTOE31fJv6FhtCnQiD6tuPQVGMYhcmAj7eihtvuEMFfqEPcQ=="], + + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-eJNee7IEGXMnmygM5SdMGDC8m/lMWmwNGf9fPCK6xk0NxuQRgmZHL6uApKcdH6gyNcRPHCqvTTkhEP7pbny/fg=="], + + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-M7A4tLGJcLPYdLC4CI2Gwl8LOrENQW59u3cMVa+KkwG1hzSJyPsbDpa1DI6oXPC2WtYiTf22zrbq3gVvH+KA2w=="], + + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-elYVn2wHJJ+zB9LESENWOAfI4TNT0jqEN34sMA/hCtA4im1ZG2DdLHwkHIshj/c4H0dzQhmsS/YmNC5Vbqab/A=="], + + "@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-IjsgSVYJRjpAKmIoapU0E2R4E2FPY3kpvU7/1i7PUYisylqejSJxmtJPGYw0FOMQY9oxnEEvfZHMBA610tqKpg=="], + + "@tiptap/extension-strike": ["@tiptap/extension-strike@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-HHIjhafLhS2lHgfAsCwC1okqMsQzR4/mkGDm4M583Yftyjri1TNA7lzhzXWRFWiiMfJxKtdjHjUAQaHuteRTZw=="], + + "@tiptap/extension-text": ["@tiptap/extension-text@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-Xk7nYcigljAY0GO9hAQpZ65ZCxqOqaAlTPDFcKerXmlkQZP/8ndx95OgUb1Xf63kmPOh3xypurGS2is3v0MXSA=="], + + "@tiptap/extension-text-align": ["@tiptap/extension-text-align@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-0Pyks6Hu+Q/+9+5/osoSv0SP6jIerdWMYbi13aaZLsJoj3lBj5WNaE11JtAwSFN5sx0IbqhDSlp1zkvRnzgZ8g=="], + + "@tiptap/extension-text-style": ["@tiptap/extension-text-style@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-Omk+uxjJLyEY69KStpCw5fA9asvV+MGcAX2HOxyISDFoLaL49TMrNjhGAuz09P1L1b0KGXo4ml7Q3v/Lfy4WPA=="], + + "@tiptap/extension-underline": ["@tiptap/extension-underline@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-gPOsbAcw1S07ezpAISwoO8f0RxpjcSH7VsHEFDVuXm4ODE32nhvSinvHQjv2icRLOXev+bnA7oIBu7Oy859gWQ=="], + + "@tiptap/pm": ["@tiptap/pm@2.27.2", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA=="], + + "@tiptap/react": ["@tiptap/react@2.27.2", "", { "dependencies": { "@tiptap/extension-bubble-menu": "^2.27.2", "@tiptap/extension-floating-menu": "^2.27.2", "@types/use-sync-external-store": "^0.0.6", "fast-deep-equal": "^3", "use-sync-external-store": "^1" }, "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0EAs8Cpkfbvben1PZ34JN2Nd79Dhioynm2jML27DBbf1VWPk+FFWFGTMLUT0bu+Np5iVxio8fqV9t0mc4D6thA=="], + + "@tiptap/starter-kit": ["@tiptap/starter-kit@2.27.2", "", { "dependencies": { "@tiptap/core": "^2.27.2", "@tiptap/extension-blockquote": "^2.27.2", "@tiptap/extension-bold": "^2.27.2", "@tiptap/extension-bullet-list": "^2.27.2", "@tiptap/extension-code": "^2.27.2", "@tiptap/extension-code-block": "^2.27.2", "@tiptap/extension-document": "^2.27.2", "@tiptap/extension-dropcursor": "^2.27.2", "@tiptap/extension-gapcursor": "^2.27.2", "@tiptap/extension-hard-break": "^2.27.2", "@tiptap/extension-heading": "^2.27.2", "@tiptap/extension-history": "^2.27.2", "@tiptap/extension-horizontal-rule": "^2.27.2", "@tiptap/extension-italic": "^2.27.2", "@tiptap/extension-list-item": "^2.27.2", "@tiptap/extension-ordered-list": "^2.27.2", "@tiptap/extension-paragraph": "^2.27.2", "@tiptap/extension-strike": "^2.27.2", "@tiptap/extension-text": "^2.27.2", "@tiptap/extension-text-style": "^2.27.2", "@tiptap/pm": "^2.27.2" } }, "sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="], + + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + + "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + + "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], + + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], + + "clsx": ["clsx@1.1.1", "", {}, "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-printf": ["fast-printf@1.6.10", "", {}, "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "jotai": ["jotai@2.16.2", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-DH0lBiTXvewsxtqqwjDW6Hg9JPTDnq9LcOsXSFWCAUEt+qj5ohl9iRVX9zQXPPHKLXCdH+5mGvM28fsXMl17/g=="], + + "jotai-optics": ["jotai-optics@0.3.2", "", { "peerDependencies": { "jotai": ">=1.11.0", "optics-ts": "*" } }, "sha512-RH6SvqU5hmkVqnHmaqf9zBXvIAs4jLxkDHS4fr5ljuBKHs8+HQ02v+9hX7ahTppxx6dUb0GGUE80jQKJ0kFTLw=="], + + "jotai-tanstack-query": ["jotai-tanstack-query@0.7.2", "", { "peerDependencies": { "@tanstack/query-core": "*", "jotai": ">=1.11.0" } }, "sha512-acwJJf4HKgs4c0mtgRJEvdL7jqQnKcT0ARvs33weGysLpQ8L1S3SqPPoMeHuLDz6vREcocsVFRZ5RsB7rJJHZQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "mantine-datatable": ["mantine-datatable@2.9.14", "", { "peerDependencies": { "@mantine/core": ">=6 <=6.0.17 || >=6.0.19 < 7", "@mantine/hooks": ">=6 <=6.0.17 || >=6.0.19 < 7", "react": ">=18" } }, "sha512-ujYUwNTqsr7ngKICoMjTIolqGm40jNiD0du+y4lY2Faqt7/3iksUdAAzPrYRM+QlHyyznsX3aHKLEjw19/VASA=="], + + "mariadb": ["mariadb@3.4.5", "", { "dependencies": { "@types/geojson": "^7946.0.16", "@types/node": "^24.0.13", "denque": "^2.1.0", "iconv-lite": "^0.6.3", "lru-cache": "^10.4.3" } }, "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ=="], + + "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], + + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "optics-ts": ["optics-ts@2.4.1", "", {}, "sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ=="], + + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "prosemirror-changeset": ["prosemirror-changeset@2.3.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ=="], + + "prosemirror-collab": ["prosemirror-collab@1.3.1", "", { "dependencies": { "prosemirror-state": "^1.0.0" } }, "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ=="], + + "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="], + + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="], + + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.0", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ=="], + + "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], + + "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="], + + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], + + "prosemirror-markdown": ["prosemirror-markdown@1.13.3", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ=="], + + "prosemirror-menu": ["prosemirror-menu@1.2.5", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ=="], + + "prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="], + + "prosemirror-schema-basic": ["prosemirror-schema-basic@1.2.4", "", { "dependencies": { "prosemirror-model": "^1.25.0" } }, "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ=="], + + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], + + "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], + + "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], + + "prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="], + + "prosemirror-transform": ["prosemirror-transform@1.11.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw=="], + + "prosemirror-view": ["prosemirror-view@1.41.5", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA=="], + + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-leaflet": ["react-leaflet@4.2.1", "", { "dependencies": { "@react-leaflet/core": "^2.1.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], + + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-textarea-autosize": ["react-textarea-autosize@8.3.4", "", { "dependencies": { "@babel/runtime": "^7.10.2", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], + + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + + "tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], + + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="], + + "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="], + + "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "vite": ["vite@4.5.14", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@communityox/ox_lib/@nativewrappers/fivem": ["@nativewrappers/fivem@0.0.103", "", {}, "sha512-x0W00Mx9ZN/rTS9XZc5Kf1hjahqRmlo9sPiuJP4kCYeQG4LDJyglXCsHcfNfygGq6WEblG1W2FLgm4MGDn/wHA=="], + + "@communityox/ox_lib/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "@mantine/styles/csstype": ["csstype@3.0.9", "", {}, "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw=="], + + "@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@4.41.1", "", {}, "sha512-XZvEw2OT+Nmi+ByQjURv3ckxRfzbYXSL6Hb60lgEn4GqUXz8HQTFdySvcSuCdxashqkBLrDvn9NwOhAbMTe9ow=="], + + "mariadb/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + + "mariadb/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/client/framework/ox_core.lua b/client/framework/ox_core.lua deleted file mode 100644 index 304f9d12..00000000 --- a/client/framework/ox_core.lua +++ /dev/null @@ -1,90 +0,0 @@ -local config = require "config" -local ox = {} -local localOfficer = {} - -ox.loadedEvent = 'ox:playerLoaded' -ox.logoutEvent = 'ox:playerLogout' -ox.setGroupEvent = 'ox:setGroup' - -local group, grade, label - -local function getGroupState(name) - return GlobalState['group.' .. name] --[[@as OxGroup]] -end - ----@param name string -local function getGroupLabel(name) - return getGroupState(name)?.label -end - ----@param name string -local function getGroupGrades(name) - return getGroupState(name)?.grades -end - ----@param group string ----@param grade number -local function getGradeLabel(group, grade) - return ('%s %s'):format(getGroupLabel(group), getGroupGrades(group)?[grade]) -end - -local player = Ox.GetPlayer() - -function ox.getGroupInfo() - group = player.get('activeGroup') - - if not group or not lib.array.includes(config.policeGroups, group) then return end - - grade = player.getGroup(group) - label = getGradeLabel(group, grade or 1) - - return group, grade, label -end - ----@param officer Officer -function ox.getGroupTitle(officer) - return getGradeLabel(officer.group, officer.grade) -end - -function ox.getOfficerData() - if player and player.charId then - ox.getGroupInfo() - - localOfficer.stateId = player.get("stateId") - localOfficer.firstName = player.get("firstName") - localOfficer.lastName = player.get("lastName") - localOfficer.group = group - localOfficer.title = label - localOfficer.grade = grade - end - - return localOfficer -end - -function ox.getPermissions() - local groupPermissions = GlobalState[('group.%s:permissions'):format(group)] - local permissions = {} - - for k, v in pairs(groupPermissions) do - k = tonumber(k) - - if k < grade then - for permission, access in pairs(v) do - permission = permission:match('^mdt%.(.*)') - - if permission then permissions[permission] = access end - end - end - end - - return permissions -end - -AddEventHandler(ox.loadedEvent, function() - player = Ox.GetPlayer() -end) - -ox.getGroupLabel = getGroupLabel -ox.getGroupGrades = getGroupGrades - -return ox diff --git a/client/main.lua b/client/main.lua deleted file mode 100644 index 77768081..00000000 --- a/client/main.lua +++ /dev/null @@ -1,359 +0,0 @@ -if not lib then return end - -local hasLoadedUi = false -local isMdtOpen = false -local config = require 'config' -local framework = require(('client.framework.%s'):format(config.framework)) -local player = framework.getOfficerData() - -local function getOfficersWithTitle(officers) - for i = 1, #officers do - officers[i].title = framework.getGroupTitle(officers[i]) - end - - return officers -end - -local tabletAnimDict = 'amb@world_human_seat_wall_tablet@female@base' -local tablet - -local function closeMdt(hideUi) - if not isMdtOpen then return end - - isMdtOpen = false - - if hideUi then - SendNUIMessage({ - action = 'setVisible', - data = false - }) - - SetNuiFocus(false, false) - end - - if IsEntityPlayingAnim(cache.ped, tabletAnimDict, 'base', 3) then - ClearPedSecondaryTask(cache.ped) - end - - if tablet then - if DoesEntityExist(tablet) then - Wait(300) - DeleteEntity(tablet) - end - - tablet = nil - end -end - -AddEventHandler(framework.loadedEvent, function() - player = framework.getOfficerData() -end) - -AddEventHandler(framework.logoutEvent, function() - hasLoadedUi = false - - if player.group then closeMdt(true) end -end) - -RegisterNetEvent(framework.setGroupEvent, function() - local lastGroup = player.group - - framework.getOfficerData() - - if not player.group and lastGroup or (lastGroup and lastGroup ~= player.group) then - closeMdt(true) - end -end) - -local function openMDT() - ---@type boolean?, string? - local isAuthorised, callSign = lib.callback.await('ox_mdt:openMDT', 500) - - if not isAuthorised then return end - - isMdtOpen = true - - if not IsEntityPlayingAnim(cache.ped, tabletAnimDict, 'base', 3) then - lib.requestAnimDict(tabletAnimDict) - TaskPlayAnim(cache.ped, tabletAnimDict, 'base', 6.0, 3.0, -1, 49, 1.0, false, false, false) - RemoveAnimDict(tabletAnimDict) - end - - if not tablet then - local model = lib.requestModel(`prop_cs_tablet`) - - if not model then return end - - local coords = GetEntityCoords(cache.ped) - tablet = CreateObject(model, coords.x, coords.y, coords.z, true, true, true) - AttachEntityToEntity(tablet, cache.ped, GetPedBoneIndex(cache.ped, 28422), 0.0, 0.0, 0.03, 0.0, 0.0, 0.0, true, - true, false, true, 0, true) - SetModelAsNoLongerNeeded(model) - end - - if not hasLoadedUi then - -- Maybe combine into a single callback? - local profileCards = lib.callback.await('ox_mdt:getCustomProfileCards') - local charges = lib.callback.await('ox_mdt:getAllCharges') - - SendNUIMessage({ - action = 'setInitData', - data = { - profileCards = profileCards, - locale = GetConvar('ox:locale', 'en'), - locales = lib.getLocales(), - charges = charges - } - }) - - hasLoadedUi = true - end - - player.unit = LocalPlayer.state.mdtUnitId - player.callSign = callSign - player.group = framework.getGroupInfo() - player.permissions = framework.getPermissions() - - SendNUIMessage({ - action = 'setVisible', - data = player - }) - - SetNuiFocus(true, true) -end - -exports('openMDT', openMDT) - -lib.addKeybind({ - defaultKey = 'm', - description = 'Open the Police MDT', - name = 'openMDT', - onPressed = openMDT -}) - -local callsAreFocused = false - -lib.addKeybind({ - defaultKey = 'GRAVE', - description = 'Toggle MDT calls focus', - name = 'focusCalls', - onPressed = function() - if callsAreFocused then - callsAreFocused = false - SetNuiFocus(false, false) - SetNuiFocusKeepInput(false) - return - end - - if IsNuiFocused() or IsPauseMenuActive() then return end - - callsAreFocused = true - - SetNuiFocus(true, true) - SetNuiFocusKeepInput(true) - SetCursorLocation(0.5, 0.5) - - while callsAreFocused do - DisablePlayerFiring(cache.playerId, true) - DisableControlAction(0, 1, true) - DisableControlAction(0, 2, true) - DisableControlAction(2, 199, true) - DisableControlAction(2, 200, true) - Wait(0) - end - end -}) - -AddEventHandler('onResourceStop', function(resource) - if resource == cache.resource then closeMdt() end -end) - -RegisterNuiCallback('hideMDT', function(_, cb) - cb(1) - SetNuiFocus(false, false) - closeMdt() -end) - -RegisterNuiCallback('getDepartmentsData', function(_, cb) - local groups = {} - - for i = 1, #config.policeGroups do - local name = config.policeGroups[i] - groups[name] = { - label = framework.getGroupLabel(name), - ranks = framework.getGroupGrades(name) - } - end - - cb(groups) -end) - ----@param event string ----@param clientCb? fun(data: any, cb: function) -local function serverNuiCallback(event, clientCb) - RegisterNuiCallback(event, function(data, cb) - local response = lib.callback.await('ox_mdt:' .. event, false, data) - if clientCb then return clientCb(response, cb) end - cb(response) - end) -end - - --- Dashboard -serverNuiCallback('getAnnouncements') -serverNuiCallback('getWarrants') -serverNuiCallback('createAnnouncement') -serverNuiCallback('editAnnouncement') -serverNuiCallback('deleteAnnouncement') -serverNuiCallback('getBOLOs') -serverNuiCallback('deleteBOLO') -serverNuiCallback('createBOLO') -serverNuiCallback('editBOLO') - --- Reports -serverNuiCallback('getCriminalProfiles') -serverNuiCallback('createReport') -serverNuiCallback('getReports') -serverNuiCallback('getReport') -serverNuiCallback('deleteReport') -serverNuiCallback('setReportTitle') -serverNuiCallback('getSearchOfficers') -serverNuiCallback('addCriminal') -serverNuiCallback('removeCriminal') -serverNuiCallback('saveCriminal') -serverNuiCallback('addOfficer') -serverNuiCallback('removeOfficer') -serverNuiCallback('addEvidence') -serverNuiCallback('removeEvidence') -serverNuiCallback('saveReportContents') -serverNuiCallback('getRecommendedWarrantExpiry') - --- Profiles -serverNuiCallback('getProfiles') -serverNuiCallback('getProfile') -serverNuiCallback('saveProfileImage') -serverNuiCallback('saveProfileNotes') - --- Dispatch -serverNuiCallback('attachToCall') -serverNuiCallback('completeCall') -serverNuiCallback('detachFromCall') ----@param data Calls ----@param cb fun(data: Calls) -serverNuiCallback('getCalls', function(data, cb) - -- Assign street names to data from the sever to be sent to UI - - for _, call in pairs(data) do - call.location = GetStreetNameFromHashKey(GetStreetNameAtCoord(call.coords[1], call.coords[2], 0)) - end - - cb(data) -end) -serverNuiCallback('getUnits') -serverNuiCallback('createUnit') -serverNuiCallback('joinUnit') -serverNuiCallback('leaveUnit') -serverNuiCallback('setCallUnits') -serverNuiCallback('getActiveOfficers') -serverNuiCallback('setUnitOfficers') -serverNuiCallback('setUnitType') -serverNuiCallback('setOfficerCallSign') -serverNuiCallback('setOfficerRank') -serverNuiCallback('fireOfficer') -serverNuiCallback('hireOfficer') -serverNuiCallback('fetchRoster', function(data, cb) - getOfficersWithTitle(data.officers) - - cb(data) -end) - - ----@param data table ----@param cb function -RegisterNuiCallback('setWaypoint', function(data, cb) - SetNewWaypoint(data[1], data[2]) - cb(1) -end) - ----@param data {id: number, call: Call} -RegisterNetEvent('ox_mdt:createCall', function(data) - data.call.id = data.id - data.call.location = GetStreetNameFromHashKey(GetStreetNameAtCoord(data.call.coords[1], data.call.coords[2], 0)) - - --todo: play more emergent sound for isEmergency - PlaySoundFrontend(-1, 'Near_Miss_Counter_Reset', 'GTAO_FM_Events_Soundset', false) - - SendNUIMessage({ - action = 'addCall', - data = data.call - }) -end) - ----@param data {id: number, call: Call} -RegisterNetEvent('ox_mdt:editCallUnits', function(data) - SendNUIMessage({ - action = 'editCallUnits', - data = data - }) -end) - ----@param data {id: number, coords: table} -RegisterNetEvent('ox_mdt:updateCallCoords', function(data) - SendNUIMessage({ - action = 'updateCallCoords', - data = data - }) -end) - ----@param data {id: number, units: Units} -RegisterNetEvent('ox_mdt:setCallUnits', function(data) - SendNUIMessage({ - action = 'setCallUnits', - data = data - }) -end) - ----@param data Units -RegisterNetEvent('ox_mdt:refreshUnits', function(data) - SendNUIMessage({ - action = 'refreshUnits', - data = data - }) -end) - -local blips = {} - ----@param data Officer[] -RegisterNetEvent('ox_mdt:updateOfficerPositions', function(data) - if not hasLoadedUi then return end - - for i = 1, #data do - local officer = data[i] - - if officer.stateId ~= player.stateid then - local blip = blips[officer.stateId] - - if not blip then - local name = ('police:%s'):format(officer.stateId) - blip = AddBlipForCoord(officer.position[1], officer.position[2], officer.position[3]) - blips[officer.stateId] = blip - - SetBlipSprite(blip, 1) - SetBlipDisplay(blip, 3) - SetBlipColour(blip, 42) - ShowFriendIndicatorOnBlip(blip, true) - AddTextEntry(name, ('%s %s (%s)'):format(officer.firstName, officer.lastName, officer.callSign)) - BeginTextCommandSetBlipName(name) - EndTextCommandSetBlipName(blip) - SetBlipCategory(blip, 7) - else - SetBlipCoords(blip, officer.position[1], officer.position[2], officer.position[3]) - end - end - end - - SendNUIMessage({ - action = 'updateOfficerPositions', - data = data - }) -end) diff --git a/data/config.json b/data/config.json new file mode 100644 index 00000000..ff44533e --- /dev/null +++ b/data/config.json @@ -0,0 +1,4 @@ +{ + "policeGroups": ["police", "dispatch"], + "item": false +} diff --git a/permissions.json b/data/permissions.json similarity index 100% rename from permissions.json rename to data/permissions.json diff --git a/fxmanifest.lua b/fxmanifest.lua deleted file mode 100644 index 12126260..00000000 --- a/fxmanifest.lua +++ /dev/null @@ -1,46 +0,0 @@ ---[[ FX Information ]]-- -fx_version 'cerulean' -use_experimental_fxv2_oal 'yes' -lua54 'yes' -game 'gta5' - ---[[ Resource Information ]]-- -name 'ox_mdt' -version '0.3.0' -description 'MDT' -author 'overextended' -repository 'https://github.com/communityox/ox_mdt' - ---[[ Manifest ]]-- -dependencies { - '/server:6279', - '/onesync', -} - -ox_libs { - 'locale', -} - -shared_scripts { - '@ox_lib/init.lua', - '@ox_core/lib/init.lua' -} - -client_scripts { - 'client/main.lua', -} - -server_scripts { - '@oxmysql/lib/MySQL.lua', - 'server/main.lua', -} - -ui_page 'web/build/index.html' - -files { - 'web/build/index.html', - 'web/build/**/*', - 'locales/*.json', - 'client/framework/*.lua', - 'config.lua' -} diff --git a/web/package.json b/package.json similarity index 65% rename from web/package.json rename to package.json index c923ed84..c1319a6b 100644 --- a/web/package.json +++ b/package.json @@ -1,15 +1,29 @@ { "name": "ox_mdt", - "private": true, - "version": "0.0.0", + "author": "Overextended", + "version": "0.3.0", + "license": "LGPL-3.0-or-later", + "description": "MDT system for ox_core", "type": "module", "scripts": { - "start": "vite", - "start:game": "vite build --watch", - "build": "tsc && vite build", - "preview": "vite preview" + "web:dev": "cd src/web && vite dev", + "web:watch": "cd src/web && vite build --watch", + "build": "bun run scripts/build.js --mode=production", + "watch": "bun run scripts/build.js --watch", + "prettier": "prettier ./src/**/*.ts --write" }, + "keywords": [ + "fivem" + ], + "repository": { + "type": "git", + "url": "https://github.com/communityox/ox_mdt.git" + }, + "bugs": "https://github.com/communityox/ox_mdt/issues", "dependencies": { + "@communityox/ox_core": "^1.5.8", + "@communityox/ox_lib": "^3.32.1", + "@communityox/oxmysql": "^1.4.3", "@emotion/react": "^11.10.6", "@mantine/core": "^6.0.19", "@mantine/dates": "^6.0.19", @@ -50,5 +64,8 @@ "@vitejs/plugin-react": "^4.0.4", "typescript": "^4.9.5", "vite": "^4.4.9" + }, + "engines": { + "node": ">=16.9.1" } } diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 00000000..4a32c6fa --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,48 @@ +//@ts-check + +import { exists, exec, getFiles } from './utils.js'; +import { createBuilder, createFxmanifest } from '@communityox/fx-utils'; + +const watch = process.argv.includes('--watch'); +const web = await exists('./src/web'); + +createBuilder( + watch, + { + dropLabels: !watch ? ['DEV'] : undefined, + }, + [ + { + name: 'server', + options: { + platform: 'node', + target: ['node16'], + format: 'cjs', + }, + }, + { + name: 'client', + options: { + platform: 'browser', + target: ['es2021'], + format: 'iife', + }, + }, + ], + async (outfiles) => { + if (web) await exec(`cd ./src/web && vite build ${watch ? '--watch' : ''}`); + + const files = await getFiles('dist/web', 'data'); + + await createFxmanifest({ + client_scripts: ['@ox_lib/init.lua', outfiles.client], + server_scripts: ['@oxmysql/lib/MySQL.lua', outfiles.server], + files: [...files, 'locales/*.json'], + dependencies: ['/server:7290', '/onesync', 'ox_core', 'ox_lib', 'oxmysql'], + metadata: { + ui_page: 'dist/web/index.html', + lua54: 'yes', + }, + }); + } +); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 00000000..c431f049 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,66 @@ +//@ts-check + +import { stat, readdir, readFile } from 'fs/promises'; +import { spawn } from 'child_process'; + +/** + * Check if a filepath is valid. + * @param path {string} + */ +export async function exists(path) { + try { + await stat(path); + return true; + } catch (err) {} + + return false; +} + +/** + * Spawn a child process and executes the command asynchronously. + * @param command {string} + */ +export function exec(command) { + return new Promise((resolve, reject) => { + const child = spawn(command, { stdio: 'inherit', shell: true }); + + child.on('exit', (code, signal) => { + if (code === 0) { + resolve({ code, signal }); + } else { + reject(new Error(`Command '${command}' exited with code ${code} and signal ${signal}`)); + } + }); + }); +} + +/** + * Recursively read the files in a directory and return the paths. + * @param args {string[]} + * @return {Promise} + */ +export async function getFiles(...args) { + const files = await Promise.all( + args.map(async (dir) => { + try { + const dirents = await readdir(`${dir}/`, { withFileTypes: true }); + const paths = await Promise.all( + dirents.map(async (dirent) => { + const path = `${dir}/${dirent.name}`; + return dirent.isDirectory() ? await getFiles(path) : path; + }) + ); + + return paths.flat(); + } catch (err) { + return []; + } + }) + ); + + return files.flat(); +} + +export async function getPackage() { + return JSON.parse(await readFile('package.json', 'utf8')); +} diff --git a/server/calls.lua b/server/calls.lua deleted file mode 100644 index 1ee47e40..00000000 --- a/server/calls.lua +++ /dev/null @@ -1,146 +0,0 @@ ----@type Calls -local activeCalls = {} - ----@type Calls -local completedCalls = {} - -local callId = 0 -local registerCallback = require 'server.utils.registerCallback' -local units = require 'server.units' -local officers = require 'server.officers' - ----@param data CallData -function createCall(data) - activeCalls[callId] = { - id = callId, - code = data.code, - offense = data.offense, - completed = false, - units = {}, - coords = {data.coords[1], data.coords[2]}, - blip = data.blip, - isEmergency = data.isEmergency, - time = os.time() * 1000, - location = '', - info = data.info - } - - officers.triggerEvent('ox_mdt:createCall', { id = callId, call = activeCalls[callId] }) - callId += 1 - - return callId - 1 -end - -exports('createCall', createCall) - ----@param callId number ----@param coords table -function updateCallCoords(callId, coords) - if not activeCalls[callId] then return end - - activeCalls[callId].coords = coords - - officers.triggerEvent('ox_mdt:updateCallCoords', { id = callId, coords = coords }) -end - -exports('updateCallCoords', updateCallCoords) - ---[[ -Citizen.SetTimeout(7500, function() - local coords = GetEntityCoords(GetPlayerPed(1)) - - local id = createCall({ - offense = 'Speeding', - code = '10-69', - blip = 51, - isEmergency = true, - info = { - {label = 'XYZ 123', icon = 'badge-tm'}, - {label = 'Dinka Blista', icon = 'car'} - }, - coords = {coords.x, coords.y} - }) - - local multiplier = 1 - SetInterval(function() - updateCallCoords(id, {coords.x + (multiplier*100), coords.y + (multiplier*100)}) - multiplier += 1 - end, 1500) -end) -]] - ----@param source number ----@param data 'active' | 'completed' -registerCallback('ox_mdt:getCalls', function(source, data) - return data == 'active' and activeCalls or completedCalls -end) - ----@param source number ----@param id number -registerCallback('ox_mdt:attachToCall', function(source, id) - local playerUnitId = Player(source).state.mdtUnitId --[[@as number]] - - if not playerUnitId or activeCalls[id].units[playerUnitId] then return false end - - activeCalls[id].units[playerUnitId] = units.getUnit(playerUnitId) - - -- Used to update a call notification - does not refresh calls list in the MDT - officers.triggerEvent('ox_mdt:editCallUnits', { id = id, units = activeCalls[id].units }) - - return true -end) - ----@param source number ----@param id number -registerCallback('ox_mdt:detachFromCall', function(source, id) - local playerUnitId = Player(source).state.mdtUnitId --[[@as number]] - if not playerUnitId then return false end - - if not activeCalls[id].units[playerUnitId] then return false end - - activeCalls[id].units[playerUnitId] = nil - - -- Used to update a call notification - does not refresh calls list in the MDT - officers.triggerEvent('ox_mdt:editCallUnits', { id = id, units = activeCalls[id].units }) - - return true -end) - ----@param source number ----@param id number -registerCallback('ox_mdt:completeCall', function(source, id) - if not activeCalls[id] then return end - - activeCalls[id].completed = os.time() - completedCalls[id] = activeCalls[id] - activeCalls[id] = nil - - return true -end, 'mark_call_completed') - ----@param source number ----@param data {id: number, units: string[]} -registerCallback('ox_mdt:setCallUnits', function(source, data) - local officer = officers.get(source) - - if not officer.group == 'dispatch' then return end - - activeCalls[data.id].units = {} - for i = 1, #data.units do - local unitId = data.units[i] - activeCalls[data.id].units[unitId] = units.getUnit(tostring(unitId)) - end - - officers.triggerEvent('ox_mdt:setCallUnits', { id = data.id, units = activeCalls[data.id].units }) - - return true -end) - --- Remove completed calls older than 1 hour every hour -lib.cron.new('0 */1 * * *', function() - for id, call in pairs(completedCalls) do - if os.time() - call.completed > 3600 then - completedCalls[id] = nil - end - end -end) diff --git a/server/charges.lua b/server/charges.lua deleted file mode 100644 index 9323ad92..00000000 --- a/server/charges.lua +++ /dev/null @@ -1,34 +0,0 @@ -local registerCallback = require 'server.utils.registerCallback' - -local chargeCategories = { - ['OFFENSES AGAINST PERSONS'] = 'Offenses Against Persons', - ['OFFENSES INVOLVING THEFT'] = 'Offenses Involving Theft', - ['OFFENSES INVOLVING FRAUD'] = 'Offenses Involving Fraud', - ['OFFENSES INVOLVING DAMAGE TO PROPERTY'] = 'Offenses Involving Damage To Property', - ['OFFENSES AGAINST PUBLIC ADMINISTRATION'] = 'Offenses Against Public Administration', - ['OFFENSES AGAINST PUBLIC ORDER'] = 'Offenses Against Public Order', - ['OFFENSES AGAINST HEALTH AND MORALS'] = 'Offenses Agaisnt Health And Morals', - ['OFFENSES AGAINST PUBLIC SAFETY'] = 'Offenses Against Public Safety', - ['OFFENSES INVOLVING THE OPERATION OF A VEHICLE'] = 'Offenses Involving The Operation Of A Vehicle', - ['OFFENSES INVOLVING THE WELL-BEING OF WILDLIFE'] = 'Offenses Involving The Well-Being Of Wildlife', -} - -local charges = {} - --- Init arrays for categories -for category in pairs(chargeCategories) do - charges[category] = {} -end - -MySQL.ready(function() - local dbCharges = MySQL.rawExecute.await('SELECT * FROM `ox_mdt_offenses`') - - for i = 1, #dbCharges do - local charge = dbCharges[i] - charges[charge.category][#charges[charge.category]+1] = charge - end -end) - -registerCallback('ox_mdt:getAllCharges', function() - return charges -end) \ No newline at end of file diff --git a/server/db.lua b/server/db.lua deleted file mode 100644 index 5f1ad2eb..00000000 --- a/server/db.lua +++ /dev/null @@ -1,325 +0,0 @@ -local db = {} -local config = require 'config' -local framework = require(('server.framework.%s'):format(config.framework)) -local profileCards = require 'server.profileCards' -local dbSearch = require 'server.utils.dbSearch' - ----@param search string -function db.searchCharacters(search) - return dbSearch(framework.getCharacters, search) -end - ----@param title string ----@param author string -function db.createReport(title, author) - return MySQL.prepare.await('INSERT INTO `ox_mdt_reports` (`title`, `author`) VALUES (?, ?)', { title, author }) --[[@as number?]] -end - ----@param id number -function db.selectReportById(id) - return MySQL.prepare.await('SELECT `id`, `title`, `description` FROM `ox_mdt_reports` WHERE `id` = ?', { id }) --[[@as MySQLRow]] -end - -local selectReports = 'SELECT `id`, `title`, `author`, DATE_FORMAT(`date`, "%Y-%m-%d %T") as date FROM `ox_mdt_reports`' -local selectReportsById = selectReports .. 'WHERE `id` = ?' - ----@param id number | string -function db.selectReportsById(id) - return MySQL.rawExecute.await(selectReportsById, { id }) -end - -local selectReportsPaginate = selectReports .. 'ORDER BY `id` DESC LIMIT 10 OFFSET ?' -local selectReportsFilter = selectReports .. ' WHERE MATCH (`title`, `author`, `description`) AGAINST (? IN BOOLEAN MODE) ORDER BY `id` DESC LIMIT 10 OFFSET ?' - ----@param page number ----@param search string -function db.selectReports(page, search) - local offset = (page - 1) * 10 - - if not search or search == '' then - return MySQL.rawExecute.await(selectReportsPaginate, { offset }) - end - - return dbSearch(function(parameters) - return MySQL.rawExecute.await(selectReportsFilter, parameters) - end, search, offset) -end - ----@param stateId string ----@param image string | nil -function db.updateProfileImage(stateId, image) - return MySQL.prepare.await('INSERT INTO `ox_mdt_profiles` (`stateid`, `image`, `notes`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `image` = ?', { stateId, image, nil, image }) -end - ----@param stateId string ----@param notes string -function db.updateProfileNotes(stateId, notes) - return MySQL.prepare.await('INSERT INTO `ox_mdt_profiles` (`stateid`, `image`, `notes`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `notes` = ?', { stateId, nil, notes, notes }) -end - ----@param callSign string -function db.selectOfficerCallSign(callSign) - return MySQL.prepare.await('SELECT `callSign` FROM `ox_mdt_profiles` WHERE callSign = ?', { callSign }) -end - ----@param stateId string ----@param callSign string -function db.updateOfficerCallSign(stateId, callSign) - return MySQL.prepare.await('INSERT INTO `ox_mdt_profiles` (`stateId`, `image`, `notes`, `callSign`) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE `callSign` = ?', { stateId, nil, nil, callSign, callSign }) -end - ----@param id number -function db.deleteReport(id) - return MySQL.prepare.await('DELETE FROM `ox_mdt_reports` WHERE `id` = ?', { id }) --[[@as number?]] -end - ----@param title string ----@param reportId number -function db.updateReportTitle(title, reportId) - return MySQL.prepare.await('UPDATE `ox_mdt_reports` SET `title` = ? WHERE `id` = ?', { title, reportId }) --[[@as number?]] -end - ----@param page number ----@param search string ----@return PartialProfileData[]? -function db.selectProfiles(page, search) - local offset = (page - 1) * 10 - return dbSearch(framework.getProfiles, search, offset) -end - ----@param reportId number -function db.selectOfficersInvolved(reportId) - return framework.getOfficersInvolved({ reportId }) -end - -function db.selectCriminalsInvolved(reportId) - local parameters = { reportId } - local criminals = framework.getCriminalsInvolved(parameters) or {} - local charges = framework.getCriminalCharges(parameters) or {} - - for _, criminal in pairs(criminals) do - ---@type SelectedCharge[] - criminal.charges = {} - local chargesN = 0 - - criminal.penalty = { - time = 0, - fine = 0, - reduction = criminal.reduction - } - - for _, charge in pairs(charges) do - if charge.label and charge.stateId == criminal.stateId then - - charge.stateId = nil - criminal.penalty.time += charge.time or 0 - criminal.penalty.fine += charge.fine or 0 - chargesN += 1 - criminal.charges[chargesN] = charge - end - end - - if criminal.warrantExpiry then - criminal.issueWarrant = true - end - - criminal.processed = criminal.processed or false - criminal.pleadedGuilty = criminal.pleadedGuilty or false - end - - return criminals -end - -function db.selectEvidence(reportId) - return MySQL.rawExecute.await('SELECT `label`, `image` FROM `ox_mdt_reports_evidence` WHERE reportId = ?', { reportId }) -end - ----@param reportId number ----@param criminal Criminal -function db.saveCriminal(reportId, criminal) - local queries = { - { 'DELETE FROM `ox_mdt_reports_charges` WHERE `reportid` = ? AND `stateId` = ?', { reportId, criminal.stateId } }, - { 'UPDATE IGNORE `ox_mdt_reports_criminals` SET `warrantExpiry` = ?, `processed` = ?, `pleadedGuilty` = ? WHERE `reportid` = ? AND `stateId` = ?', { criminal.issueWarrant and criminal.warrantExpiry or nil, criminal.processed, criminal.pleadedGuilty, reportId, criminal.stateId } }, - } - local queryN = #queries - - if next(criminal.charges) then - for _, v in pairs(criminal.charges) do - queryN += 1 - queries[queryN] = { 'INSERT INTO `ox_mdt_reports_charges` (`reportid`, `stateId`, `charge`, `count`, `time`, `fine`) VALUES (?, ?, ?, ?, ?, ?)', { reportId, criminal.stateId, v.label, v.count, v.time, v.fine } } - end - end - - return MySQL.transaction.await(queries) -end - -function db.removeCriminal(reportId, stateId) - return MySQL.prepare.await('DELETE FROM `ox_mdt_reports_criminals` WHERE `reportid` = ? AND `stateId` = ?', { reportId, stateId }) -end - ----@param reportId number ----@param stateId string -function db.addCriminal(reportId, stateId) - return MySQL.prepare.await('INSERT INTO `ox_mdt_reports_criminals` (`reportid`, `stateId`) VALUES (?, ?)', { reportId, stateId }) --[[@as number?]] -end - ----@param search string | number ----@return Profile? -function db.selectCharacterProfile(search) - local parameters = { search } - local profile = framework.getCharacterProfile(parameters) - - if not profile then return end - - local cards = profileCards.getAll() - - for i = 1, #cards do - local card = cards[i] - profile[card.id] = card.getData(profile) - end - - profile.relatedReports = MySQL.rawExecute.await('SELECT DISTINCT `id`, `title`, `author`, DATE_FORMAT(`date`, "%Y-%m-%d") as date FROM `ox_mdt_reports` a LEFT JOIN `ox_mdt_reports_charges` b ON b.reportid = a.id WHERE `stateId` = ?', parameters) or {} - - return profile -end - ----@param search string? ----@return Officer | Officer[] | nil -function db.searchOfficers(search) - return dbSearch(framework.getOfficers, search) -end - ----@param reportId number ----@param stateId number -function db.addOfficer(reportId, stateId) - return MySQL.prepare.await('INSERT INTO `ox_mdt_reports_officers` (`reportid`, `stateId`) VALUES (?, ?)', { reportId, stateId }) -end - ----@param reportId number ----@param stateId number -function db.removeOfficer(reportId, stateId) - return MySQL.prepare.await('DELETE FROM `ox_mdt_reports_officers` WHERE `reportid` = ? AND `stateId` = ?', { reportId, stateId }) -end - ----@param id number ----@param label string ----@param image string -function db.addEvidence(id, label, image) - return MySQL.prepare.await('INSERT INTO `ox_mdt_reports_evidence` (`reportid`, `label`, `image`) VALUES (?, ?, ?)', { id, label, image }) -end - ----@param id number ----@param label string ----@param image string -function db.removeEvidence(id, label, image) - return MySQL.prepare.await('DELETE FROM `ox_mdt_reports_evidence` WHERE `reportid` = ? AND `label` = ? AND `image` = ?', { id, label, image }) -end - ----@param id number ----@param value string -function db.updateReportContents(id, value) - return MySQL.prepare.await('UPDATE `ox_mdt_reports` SET `description` = ? WHERE `id` = ?', { value, id }) -end - ----@param page number -function db.selectAnnouncements(page) - return framework.getAnnouncements({ (page - 1) * 5 }) -end - ----@param id number -function db.selectAnnouncement(id) - return MySQL.prepare.await('SELECT * FROM `ox_mdt_announcements` WHERE `id` = ?', { id }) --[[@as Announcement]] -end - ----@param creator string ----@param contents string -function db.createAnnouncement(creator, contents) - return MySQL.prepare.await('INSERT INTO `ox_mdt_announcements` (`creator`, `contents`) VALUES (?, ?)', { creator, contents }) -end - ----@param id number ----@param contents string -function db.updateAnnouncementContents(id, contents) - return MySQL.prepare.await('UPDATE `ox_mdt_announcements` SET `contents` = ? WHERE `id` = ?', { contents, id }) -end - ----@param id number -function db.removeAnnouncement(id) - return MySQL.prepare.await('DELETE FROM `ox_mdt_announcements` WHERE `id` = ?', { id }) -end - ----@param page number -function db.selectBOLOs(page) - return framework.getBOLOs({ (page - 1) * 5 }) -end - ----@param id number -function db.selectBOLO(id) - return MySQL.prepare.await('SELECT * FROM `ox_mdt_bolos` WHERE `id` = ?', { id }) -end - ----@param creator string ----@param contents string -function db.createBOLO(creator, contents) - return MySQL.prepare.await('INSERT INTO `ox_mdt_bolos` (`creator`, `contents`) VALUES (?, ?)', { creator, contents }) --[[@as number]] -end - ----@param id number ----@param images string[] -function db.createBOLOImages(id, images) - local queries = {} - - for i = 1, #images do - local image = images[i] - queries[i] = { 'INSERT INTO `ox_mdt_bolos_images` (`boloId`, `image`) VALUES (?, ?)', { id, image } } - end - - return MySQL.transaction.await(queries) -end - -function db.deleteBOLO(id) - return MySQL.prepare.await('DELETE FROM `ox_mdt_bolos` WHERE id = ?', { id }) -end - -function db.updateBOLO(id, contents, images) - local queries = { - { 'DELETE FROM `ox_mdt_bolos_images` where `boloId` = ? ', { id } }, - } - - local queryN = #queries - - for i = 1, #images do - local image = images[i] - queryN += 1 - queries[queryN] = { 'INSERT INTO `ox_mdt_bolos_images` (`boloId`, `image`) VALUES (?, ?)', { id, image } } - end - - queries[queryN + 1] = { 'UPDATE `ox_mdt_bolos` SET `contents` = ? WHERE `id` = ?', { contents, id } } - - return MySQL.transaction.await(queries) -end - ----@param search string -function db.selectWarrants(search) - return dbSearch(framework.getWarrants, search) -end - -function db.createWarrant(reportId, stateId, expiry) - local warrantExists = MySQL.prepare.await('SELECT COUNT(1) FROM `ox_mdt_warrants` WHERE `reportId` = ? AND `stateId` = ?', { reportId, stateId }) > 0 - - if warrantExists then - return MySQL.prepare.await('UPDATE `ox_mdt_warrants` SET `expiresAt` = ? WHERE `reportId` = ? AND `stateId` = ?', { expiry, reportId, stateId }) - end - - return MySQL.prepare.await('INSERT INTO `ox_mdt_warrants` (`reportid`, `stateid`, `expiresAt`) VALUES (?, ?, ?)', { reportId, stateId, expiry }) -end - -function db.removeWarrant(reportId, stateId) - return MySQL.prepare.await('DELETE FROM `ox_mdt_warrants` WHERE `reportid` = ? AND `stateid` = ?', { reportId, stateId }) -end - -function db.removeOldWarrants() - return MySQL.prepare.await('DELETE FROM `ox_mdt_warrants` WHERE `expiresAt` < (NOW() - INTERVAL 1 HOUR)') -end - -return db diff --git a/server/framework/ox_core.lua b/server/framework/ox_core.lua deleted file mode 100644 index 57a57f55..00000000 --- a/server/framework/ox_core.lua +++ /dev/null @@ -1,502 +0,0 @@ -local officers = require 'server.officers' -local units = require 'server.units' -local registerCallback = require 'server.utils.registerCallback' -local config = require 'config' -local dbSearch = require 'server.utils.dbSearch' -local permissions = require 'server.permissions' - -for i = 1, #config.policeGroups do - local group = config.policeGroups[i] - - Ox.SetGroupPermission(group, 1, 'mdt.access', 'allow') - - for permission, grade in pairs(permissions) do - grade = type(grade) == 'number' and grade or grade[group] - - if grade then - Ox.SetGroupPermission(group, grade, ('mdt.%s'):format(permission), 'allow') - end - end -end - -CreateThread(function() - local dbUserIndexes = MySQL.rawExecute.await('SHOW INDEX FROM `characters`') or {} - local dbPlateIndexes = MySQL.rawExecute.await('SHOW INDEX FROM `vehicles`') or {} - local insertCharIndex = true - - for i = 1, #dbUserIndexes do - local index = dbUserIndexes[i] - - if index.Key_name == 'stateId_name' then - insertCharIndex = false - break - end - end - - if insertCharIndex then - MySQL.update('ALTER TABLE `characters` ADD FULLTEXT INDEX `stateId_name` (`stateId`, `firstName`, `lastName`)') - end - - for i = 1, #dbPlateIndexes do - local index = dbPlateIndexes[i] - - if index.Key_name == 'vehicle_plate' then - return - end - end - - MySQL.update('ALTER TABLE `vehicles` ADD FULLTEXT INDEX `vehicle_plate` (`plate`)') -end) - -local function addOfficer(playerId) - local player = Ox.GetPlayer(playerId) - local group = player and player.get('activeGroup') - local grade = group and player.getGroup(group) - - if not grade or not lib.array.includes(config.policeGroups, group) then return end - - officers.add(playerId, player.get('firstName'), player.get('lastName'), player.stateId, group, grade) -end - -CreateThread(function() - for _, playerId in pairs(GetPlayers()) do - addOfficer(tonumber(playerId)) - end -end) - -AddEventHandler('ox:playerLoaded', addOfficer) - -AddEventHandler('ox:setActiveGroup', function(playerId, name) - local officer = officers.get(playerId) - - if officer then - local grade = Ox.GetPlayer(playerId).getGroup(officer.group) - - if officer.group == name then - if not grade then - return officers.remove(playerId) - end - - officer.grade = grade - end - - return - end - - addOfficer(playerId) -end) - -AddEventHandler('ox:playerLogout', function(playerId) - local officer = officers.get(playerId) - - if officer then - local state = Player(playerId).state - units.removePlayerFromUnit(officer, state) - officers.remove(playerId) - end -end) - -local ox = {} - ----@param playerId number ----@param permission string ----@return boolean? -function ox.isAuthorised(playerId, permission) - if config.item and exports.ox_inventory:GetItemCount(playerId, config.item) == 0 then return false end - - local player = Ox.GetPlayer(playerId) - local group = player and player.get('activeGroup') - - if not group then return end - - permission = ('group.%s.%s'):format(group, permission) - - return player.hasPermission(permission) -end - ----@return { label: string, plate: string }[] -function ox.getVehicles(parameters) - local vehicles = MySQL.rawExecute.await('SELECT `plate`, `model` FROM `vehicles` WHERE `owner` = ?', parameters) or - {} - - for _, v in pairs(vehicles) do - v.label = Ox.GetVehicleData(v.model)?.name or v.model - v.model = nil - end - - return vehicles -end - ----@return table[] -function ox.getLicenses(parameters) - local licenses = MySQL.rawExecute.await( - 'SELECT ox_licenses.label, JSON_VALUE(character_licenses.data, "$.issued") AS `issued` FROM character_licenses LEFT JOIN ox_licenses ON ox_licenses.name = character_licenses.name WHERE `charid` = ?', - parameters) or {} - - return licenses -end - -local selectCharacters = [[ - SELECT - firstName, - lastName, - DATE_FORMAT(`dateofbirth`, "%Y-%m-%d") as dob, - stateId - FROM - characters -]] - -local selectCharactersFilter = selectCharacters .. - 'WHERE MATCH (`stateId`, `firstName`, `lastName`) AGAINST (? IN BOOLEAN MODE)' - ----@param parameters string[] ----@param filter? boolean ----@return PartialProfileData[]? -function ox.getCharacters(parameters, filter) - local query = filter and selectCharactersFilter or selectCharacters - return MySQL.rawExecute.await(query, parameters) -end - -local selectOfficers = ([[ - SELECT - ox_mdt_profiles.id, - firstName, - lastName, - characters.stateId, - character_groups.name AS `group`, - character_groups.grade, - ox_mdt_profiles.image, - ox_mdt_profiles.callSign - FROM - character_groups - LEFT JOIN - characters - ON - character_groups.charId = characters.charId - LEFT JOIN - ox_mdt_profiles - ON - characters.stateId = ox_mdt_profiles.stateId - WHERE - character_groups.name IN ("%s") -]]):format(table.concat(config.policeGroups, '","')) - -local selectOfficersFilter = selectOfficers .. - ' AND MATCH (characters.stateId, `firstName`, `lastName`) AGAINST (? IN BOOLEAN MODE)' -local selectOfficersPaginate = selectOfficers .. 'LIMIT 9 OFFSET ?' -local selectOfficersFilterPaginate = selectOfficersFilter .. ' LIMIT 9 OFFSET ?' -local selectOfficersCount = selectOfficers:gsub('SELECT.-FROM', 'SELECT COUNT(*) FROM') - ----@param parameters? string[] ----@param filter? boolean ----@return Officer[]? -function ox.getOfficers(parameters, filter) - local query = filter and selectOfficersFilter or selectOfficers - return MySQL.rawExecute.await(query, parameters) -end - ----@param source number ----@param data {page: number, search: string} -registerCallback('ox_mdt:fetchRoster', function(source, data) - if data.search == '' then - return { - totalRecords = MySQL.prepare.await(selectOfficersCount), - officers = MySQL.rawExecute.await(selectOfficersPaginate, { data.page - 1 }) - } - end - - return dbSearch(function(parameters, filter) - local response = MySQL.rawExecute.await(filter and selectOfficersFilterPaginate or selectOfficersPaginate, - parameters) - - return { - totalRecords = #response, - officers = response, - } - end, data.search, data.page - 1) -end) - -local selectWarrants = [[ - SELECT - warrants.reportId, - characters.stateId, - characters.firstName, - characters.lastName, - DATE_FORMAT(warrants.expiresAt, "%Y-%m-%d %T") AS expiresAt - FROM - `ox_mdt_warrants` warrants - LEFT JOIN - `characters` - ON - warrants.stateid = characters.stateid -]] - -local selectWarrantsFilter = selectWarrants .. - ' WHERE MATCH (characters.stateId, `firstName`, `lastName`) AGAINST (? IN BOOLEAN MODE)' - ----@param parameters table ----@param filter? boolean -function ox.getWarrants(parameters, filter) - local query = filter and selectWarrantsFilter or selectWarrants - return MySQL.rawExecute.await(query, parameters) -end - -local selectProfiles = [[ - SELECT - characters.stateId, - characters.firstName, - characters.lastName, - DATE_FORMAT(characters.dateofbirth, "%Y-%m-%d") AS dob, - profile.image - FROM - characters - LEFT JOIN - ox_mdt_profiles profile - ON - profile.stateid = characters.stateid - LIMIT 10 OFFSET ? -]] - -local selectProfilesFilter = selectProfiles:gsub('LIMIT', [[ - LEFT JOIN - vehicles - ON - vehicles.owner = characters.charId - WHERE MATCH - (characters.stateId, `firstName`, `lastName`) - AGAINST - (? IN BOOLEAN MODE) - OR MATCH - (vehicles.plate) - AGAINST - (? IN BOOLEAN MODE) - GROUP BY - characters.charId - LIMIT -]]) - ----@param parameters table ----@param filter? boolean -function ox.getProfiles(parameters, filter) - local query = filter and selectProfilesFilter or selectProfiles - local params = filter and { parameters[1], parameters[1], parameters[2] } or parameters - - return MySQL.rawExecute.await(query, params) -end - ----@param parameters { [1]: number } ----@return FetchOfficers? -function ox.getOfficersInvolved(parameters) - return MySQL.rawExecute.await([[ - SELECT - characters.firstName, - characters.lastName, - characters.stateId, - profile.callSign - FROM - ox_mdt_reports_officers officer - LEFT JOIN - characters - ON - characters.stateId = officer.stateId - LEFT JOIN - ox_mdt_profiles profile - ON - characters.stateId = profile.stateId - WHERE - reportid = ? - ]], parameters) -end - ----@param parameters { [1]: number } ----@return FetchCriminals? -function ox.getCriminalsInvolved(parameters) - return MySQL.rawExecute.await([[ - SELECT DISTINCT - criminal.stateId, - characters.firstName, - characters.lastName, - criminal.reduction, - DATE_FORMAT(criminal.warrantExpiry, "%Y-%m-%d") AS warrantExpiry, - criminal.processed, - criminal.pleadedGuilty - FROM - ox_mdt_reports_criminals criminal - LEFT JOIN - characters - ON - characters.stateId = criminal.stateId - WHERE - reportid = ? - ]], parameters) -end - ----@param parameters { [1]: number } ----@return FetchCharges? -function ox.getCriminalCharges(parameters) - return MySQL.rawExecute.await([[ - SELECT - stateId, - charge as label, - time, - fine, - count - FROM - ox_mdt_reports_charges - WHERE - reportid = ? - GROUP BY - charge, stateId - ]], parameters) -end - ----@param parameters { [1]: string } ----@return Profile? -function ox.getCharacterProfile(parameters) - ---@type Profile - local profile = MySQL.rawExecute.await([[ - SELECT - a.firstName, - a.lastName, - a.stateId, - a.charid, - DATE_FORMAT(a.dateofbirth, "%Y-%m-%d") AS dob, - a.phoneNumber, - b.image, - b.notes - FROM - `characters` a - LEFT JOIN - `ox_mdt_profiles` b - ON - b.stateid = a.stateid - WHERE - a.stateId = ? - ]], parameters)?[1] - - return profile -end - ----@param parameters { [1]: number } ----@return Announcement[]? -function ox.getAnnouncements(parameters) - return MySQL.rawExecute.await([[ - SELECT - a.id, - a.contents, - a.creator AS stateId, - b.firstName, - b.lastName, - c.image, - c.callSign, - DATE_FORMAT(a.createdAt, "%Y-%m-%d %T") AS createdAt - FROM - `ox_mdt_announcements` a - LEFT JOIN - `characters` b - ON - b.stateId = a.creator - LEFT JOIN - `ox_mdt_profiles` c - ON - c.stateId = a.creator - ORDER BY `id` DESC LIMIT 5 OFFSET ? - ]], parameters) -end - -function ox.getBOLOs(parameters) - return MySQL.rawExecute.await([[ - SELECT - a.id, - a.creator AS stateId, - a.contents, - b.callSign, - b.image, - c.firstName, - c.lastName, - JSON_ARRAYAGG(d.image) AS images, - DATE_FORMAT(a.createdAt, "%Y-%m-%d %T") AS createdAt - FROM - `ox_mdt_bolos` a - LEFT JOIN - `ox_mdt_profiles` b - ON - b.stateId = a.creator - LEFT JOIN - `characters` c - ON - c.stateId = b.stateId - LEFT JOIN - `ox_mdt_bolos_images` d - ON - d.boloId = a.id - GROUP BY `id` ORDER BY `id` DESC LIMIT 5 OFFSET ? - ]], parameters) -end - ----@param playerId number ----@param data {stateId: string, group: string, grade: number} -registerCallback('ox_mdt:setOfficerRank', function(playerId, data) - local player = Ox.GetPlayer(playerId) - local grade = player and player.getGroup(data.group) - - if not grade or grade <= data.grade then return false end - - local target = Ox.GetPlayerFromFilter({ stateId = data.stateId, groups = data.group }) - - if target then - if playerId == target.source or not target.getGroup(data.group) then return false end - - return target.setGroup(data.group, data.grade + 1) - end - - MySQL.prepare.await( - 'UPDATE `character_groups` SET `grade` = ? WHERE `charId` = (SELECT `charId` FROM `characters` WHERE `stateId` = ?) AND `name` = ? ', - { data.grade + 1, data.stateId, data.group }) - - return true -end, 'set_officer_rank') - ----@param source number ----@param stateId number -registerCallback('ox_mdt:fireOfficer', function(source, stateId) - local player = Ox.GetPlayerFromFilter({ stateId = stateId }) - - if player then - for i = 1, #config.policeGroups do - local group = config.policeGroups[i] - player.setGroup(group, -1) - end - - return true - end - - local charId = MySQL.prepare.await('SELECT `charid` FROM `characters` WHERE `stateId` = ?', { stateId }) - - MySQL.prepare.await('DELETE FROM `character_groups` WHERE `charId` = ? AND `name` IN (?) ', - { charId, config.policeGroups }) - - return true -end, 'fire_officer') - ----@param source number ----@param stateId string -registerCallback('ox_mdt:hireOfficer', function(source, stateId) - local player = Ox.GetPlayerFromFilter({ stateId = stateId }) - - if player then - if player.getGroup(config.policeGroups) then return false end - - player.setGroup('police', 1) - return true - end - - local charId = MySQL.prepare.await('SELECT `charid` FROM `characters` WHERE `stateId` = ?', { stateId }) - - local success = pcall(MySQL.prepare.await, - 'INSERT INTO `character_groups` (`charId`, `name`, `grade`) VALUES (?, ?, ?)', { charId, 'police', 1 }) - - return success -end, 'hire_officer') - -return ox diff --git a/server/main.lua b/server/main.lua deleted file mode 100644 index 8c13f70f..00000000 --- a/server/main.lua +++ /dev/null @@ -1,312 +0,0 @@ -local registerCallback = require 'server.utils.registerCallback' -local db = require 'server.db' -local officers = require 'server.officers' -local isAuthorised = require 'server.utils.isAuthorised' - -require 'server.units' -require 'server.charges' -require 'server.calls' - -registerCallback('ox_mdt:openMDT', function() - return officers.get(source) and true -end) - ----@param source number ----@param page number -registerCallback('ox_mdt:getAnnouncements', function(source, page) - local announcements = db.selectAnnouncements(page) - return { - hasMore = #announcements == 5 or false, - announcements = announcements - } -end) - ----@param source number ----@param contents string -registerCallback('ox_mdt:createAnnouncement', function(source, contents) - local officer = officers.get(source) - - return officer and db.createAnnouncement(officer.stateId, contents) -end, 'create_announcement') - ----@param source number ----@param data { announcement: Announcement, value: string, id: number } -registerCallback('ox_mdt:editAnnouncement', function(source, data) - local officer = officers.get(source) - local announcement = db.selectAnnouncement(data.id) - - if not officer then return end - - if announcement.creator ~= officer.stateId then return end - - return db.updateAnnouncementContents(data.announcement.id, data.value) -end) - ----@param source number ----@param id number -registerCallback('ox_mdt:deleteAnnouncement', function(source, id) - local officer = officers.get(source) - local announcement = db.selectAnnouncement(id) - - if not isAuthorised(source, 'delete_announcement') and announcement.creator ~= officer.stateId then return end - - return db.removeAnnouncement(id) -end, 'delete_announcement') - ----@param source number ----@param page number -registerCallback('ox_mdt:getBOLOs', function(source, page) - local bolos = db.selectBOLOs(page) - - for i = 1, #bolos do - bolos[i].images = json.decode(bolos[i].images) or nil - end - - return { - hasMore = #bolos == 5 or false, - bolos = bolos - } -end) - ----@param source number ----@param id number -registerCallback('ox_mdt:deleteBOLO', function(source, id) - return db.deleteBOLO(id) -end, 'delete_bolo') - ----@param source number ----@param data {id: number, contents: string, images: string[]} -registerCallback('ox_mdt:editBOLO', function(source, data) - local officer = officers.get(source) - - if not officer then return end - - local bolo = db.selectBOLO(data.id) - - if not bolo or bolo.creator ~= officer.stateId then return end - - return db.updateBOLO(data.id, data.contents, data.images) -end) - ----@param source number ----@param data {contents: string, images: string[]} -registerCallback('ox_mdt:createBOLO', function(source, data) - local officer = officers.get(source) - local boloId = db.createBOLO(officer.stateId, data.contents) - - db.createBOLOImages(boloId, data.images) - - return boloId -end, 'create_bolo') - ----@param source number ----@param search string ----@return CriminalProfile[]? -registerCallback('ox_mdt:getCriminalProfiles', function(source, search) - return db.searchCharacters(search) -end) - ----@param title string ----@return number? -registerCallback('ox_mdt:createReport', function(source, title) - local officer = officers.get(source) - - return officer and db.createReport(title, ('%s %s'):format(officer.firstName, officer.lastName)) -end, 'create_report') - ----@param source number ----@param data { page: number, search: string } ----@return PartialReportData[] -registerCallback('ox_mdt:getReports', function(source, data) - local reports = tonumber(data.search) and db.selectReportById(data.search --[[@as number]]) or - db.selectReports(data.page, data.search) - - return { - hasMore = #reports == 10 or false, - reports = reports - } -end) - ----@param source number ----@param reportId number ----@return Report? -registerCallback('ox_mdt:getReport', function(source, reportId) - local response = db.selectReportById(reportId) - - if response then - response.officersInvolved = db.selectOfficersInvolved(reportId) - response.evidence = db.selectEvidence(reportId) - response.criminals = db.selectCriminalsInvolved(reportId) - end - - return response -end) - ----@param source number ----@param reportId number ----@return number -registerCallback('ox_mdt:deleteReport', function(source, reportId) - return db.deleteReport(reportId) -end, 'delete_report') - ----@param source number ----@param data { id: number, title: string} ----@return number -registerCallback('ox_mdt:setReportTitle', function(source, data) - return db.updateReportTitle(data.title, data.id) -end, 'edit_report_title') - ----@param source number ----@param data { reportId: number, contents: string} ----@return number -registerCallback('ox_mdt:saveReportContents', function(source, data) - return db.updateReportContents(data.reportId, data.contents) -end, 'edit_report_contents') - ----@param source number ----@param data { id: number, criminalId: string } -registerCallback('ox_mdt:addCriminal', function(source, data) - return db.addCriminal(data.id, data.criminalId) -end, 'add_criminal') - ----@param source number ----@param data { id: number, criminalId: string } -registerCallback('ox_mdt:removeCriminal', function(source, data) - return db.removeCriminal(data.id, data.criminalId) -end, 'remove_criminal') - ----@param source number ----@param data { id: number, criminal: Criminal } -registerCallback('ox_mdt:saveCriminal', function(source, data) - if data.criminal.issueWarrant then - db.createWarrant(data.id, data.criminal.stateId, data.criminal.warrantExpiry) - else - -- This would still run the delete query even if the criminal was saved without - -- there previously being a warrant issued, but should be fine? - db.removeWarrant(data.id, data.criminal.stateId) - end - - return db.saveCriminal(data.id, data.criminal) -end, 'save_criminal') - ----@param source number ----@param data { id: number, evidence: Evidence } -registerCallback('ox_mdt:addEvidence', function(source, data) - return db.addEvidence(data.id, data.evidence.label, data.evidence.image) -end, 'add_evidence') - ----@param source number ----@param data { id: number, label: string, image: string } -registerCallback('ox_mdt:removeEvidence', function(source, data) - return db.removeEvidence(data.id, data.label, data.image) -end, 'remove_evidence') - ----@param source number ----@param data {page: number, search: string} -registerCallback('ox_mdt:getProfiles', function(source, data) - local profiles = db.selectProfiles(data.page, data.search) - - return { - hasMore = #profiles == 10 or false, - profiles = profiles - } -end) - ----@param source number ----@param data string -registerCallback('ox_mdt:getProfile', function(source, data) - return db.selectCharacterProfile(data) -end) - ----@param source number ----@param data {stateId: string, image: string} -registerCallback('ox_mdt:saveProfileImage', function(source, data) - return db.updateProfileImage(data.stateId, data.image) -end, 'change_profile_picture') - ----@param source number ----@param data {stateId: string, notes: string} -registerCallback('ox_mdt:saveProfileNotes', function(source, data) - return db.updateProfileNotes(data.stateId, data.notes) -end, 'edit_profile_notes') - ----@param source number ----@param data string -registerCallback('ox_mdt:getSearchOfficers', function(source, data) - return db.searchOfficers(data) -end) - ----@param source number ----@param data {id: number, stateId: number} -registerCallback('ox_mdt:addOfficer', function(source, data) - return db.addOfficer(data.id, data.stateId) -end, 'add_officer_involved') - ----@param source number ----@param data {id: number, stateId: number} -registerCallback('ox_mdt:removeOfficer', function(source, data) - return db.removeOfficer(data.id, data.stateId) -end, 'remove_officer_involved') - ----@param source number ----@param charges Charge[] -registerCallback('ox_mdt:getRecommendedWarrantExpiry', function(source, charges) - ---@diagnostic disable-next-line: param-type-mismatch - local currentTime = os.time(os.date("!*t")) - local baseWarrantDuration = 259200000 -- 72 hours - local addonTime = 0 - - for i = 1, #charges do - local charge = charges[i] - if charge.time ~= 0 then - addonTime = addonTime + - (charge.time * 60 * 60000 * charge.count) -- 1 month of penalty time = 1 hour of warrant time - end - end - - return currentTime * 1000 + addonTime + baseWarrantDuration -end) - ----@param search string -registerCallback('ox_mdt:getWarrants', function(source, search) - return db.selectWarrants(search) -end) - -registerCallback('ox_mdt:getActiveOfficers', function() - return officers.getAll() -end) - ----@param source number ----@param data { stateId: string, callSign: string } -registerCallback('ox_mdt:setOfficerCallSign', function(source, data) - if db.selectOfficerCallSign(data.callSign) then return false end - - db.updateOfficerCallSign(data.stateId, data.callSign) - - return true -end, 'set_call_sign') - -AddEventHandler('onResourceStop', function(resource) - if resource ~= cache.resource then return end - - for playerId, officer in pairs(officers.getAll()) do - if officer.unitId then - Player(playerId).state.mdtUnitId = nil - end - end -end) - -lib.cron.new('0 */1 * * *', function() - db.removeOldWarrants() -end) - --- for testing -RegisterCommand('toggleduty', function(playerId) - local player = Ox.GetPlayer(playerId) - - if not player.getGroup('police') then return end - - player.setActiveGroup(not player.get('activeGroup') and 'police' or nil) - - print(player.charId, player.get('activeGroup')) -end, true) \ No newline at end of file diff --git a/server/officers.lua b/server/officers.lua deleted file mode 100644 index 1279bc5a..00000000 --- a/server/officers.lua +++ /dev/null @@ -1,66 +0,0 @@ ----@type table -local activeOfficers = {} -local officersArray = {} - ----Triggers a client event for all active officers. ----@param eventName string ----@param eventData any -local function triggerOfficerEvent(eventName, eventData) - for playerId in pairs(activeOfficers) do - TriggerClientEvent(eventName, playerId, eventData) - end -end - -SetInterval(function() - local n = 0 - - for _, officer in pairs(activeOfficers) do - local coords = GetEntityCoords(officer.ped) - officer.position[1] = coords.x - officer.position[2] = coords.y - officer.position[3] = coords.z - n += 1 - officersArray[n] = officer - end - - triggerOfficerEvent('ox_mdt:updateOfficerPositions', officersArray) - table.wipe(officersArray) -end, math.max(500, GetConvarInt('mdt:positionRefreshInterval', 5000))) - ----@param playerId number ----@param firstName string ----@param lastName string ----@param stateId string ----@param group string ----@param grade number -local function addOfficer(playerId, firstName, lastName, stateId, group, grade) - activeOfficers[playerId] = { - firstName = firstName, - lastName = lastName, - stateId = stateId, - callSign = MySQL.prepare.await('SELECT `callSign` FROM `ox_mdt_profiles` WHERE stateId = ?', { stateId }) --[[@as string?]], - playerId = playerId, - ped = GetPlayerPed(playerId), - position = {}, - group = group, - grade = grade, - } -end - -local function removeOfficer(playerId) - activeOfficers[playerId] = nil -end - -local function getOfficer(playerId) - return activeOfficers[playerId] -end - -local function getAll() return activeOfficers end - -return { - add = addOfficer, - remove = removeOfficer, - get = getOfficer, - getAll = getAll, - triggerEvent = triggerOfficerEvent -} diff --git a/server/permissions.lua b/server/permissions.lua deleted file mode 100644 index bada8979..00000000 --- a/server/permissions.lua +++ /dev/null @@ -1,3 +0,0 @@ -local permissions = json.decode(LoadResourceFile(cache.resource, 'permissions.json')) - -return permissions diff --git a/server/profileCards.lua b/server/profileCards.lua deleted file mode 100644 index 01d91f65..00000000 --- a/server/profileCards.lua +++ /dev/null @@ -1,108 +0,0 @@ -local registerCallback = require 'server.utils.registerCallback' -local config = require 'config' -local framework = require(('server.framework.%s'):format(config.framework)) - ----@class CustomProfileCard ----@field id string ----@field title string ----@field icon string ----@field getData fun(parameters: {search: string}): string[] - ----@type CustomProfileCard[] -local customProfileCards = {} - ----@param newCard CustomProfileCard -local function checkCardExists(newCard) - for i = 1, #customProfileCards do - local card = customProfileCards[i] - - if card.id == newCard.id then - assert(false, ("Custom card with id `%s` already exists!"):format(card.id)) - return true - end - end - - return false -end - ----@param data CustomProfileCard | CustomProfileCard[] -local function createProfileCard(data) - local arrLength = #data - if arrLength > 0 then - for i = 1, arrLength do - local newCard = data[i] - if not checkCardExists(newCard) then - customProfileCards[#customProfileCards+1] = newCard - end - end - return - end - - ---@diagnostic disable-next-line: param-type-mismatch - if not checkCardExists(data.id) then - customProfileCards[#customProfileCards+1] = data - end -end - -exports('createProfileCard', createProfileCard) - -local function getAll() - return customProfileCards -end - -createProfileCard({ - { - id = 'licenses', - title = locale('licenses'), - icon = 'certificate', - getData = function(profile) - local licenses = framework.getLicenses({profile.charid}) - local licenseLabels = {} - - for i = 1, #licenses do - licenseLabels[#licenseLabels+1] = licenses[i].label - end - - return licenseLabels - end - }, - { - id = 'vehicles', - title = locale('vehicles'), - icon = 'car', - getData = function(profile) - local vehicles = framework.getVehicles({profile.charid}) - local vehicleLabels = {} - - for i = 1, #vehicles do - vehicleLabels[#vehicleLabels+1] = vehicles[i].label .. ' (' ..vehicles[i].plate.. ')' - end - - return vehicleLabels - end, - }, - { - id = 'pastCharges', - title = locale("past_charges"), - icon = 'gavel', - getData = function(profile) - local charges = MySQL.rawExecute.await('SELECT `charge` AS label, SUM(`count`) AS count FROM `ox_mdt_reports_charges` WHERE `charge` IS NOT NULL AND `stateId` = ? GROUP BY `charge`', {profile.stateId}) or {} - local chargeLabels = {} - - for i = 1, #charges do - chargeLabels[#chargeLabels+1] = charges[i].count ..'x ' .. charges[i].label - end - - return chargeLabels - end, - }, -}) - -registerCallback('ox_mdt:getCustomProfileCards', function() - return customProfileCards -end) - -return { - getAll = getAll, - create = createProfileCard -} \ No newline at end of file diff --git a/server/units.lua b/server/units.lua deleted file mode 100644 index d5ea2c30..00000000 --- a/server/units.lua +++ /dev/null @@ -1,184 +0,0 @@ ----@type Units -local units = {} -local officers = require 'server.officers' -local registerCallback = require 'server.utils.registerCallback' - ----@param officer Officer ----@param state StateBag -local function removePlayerFromUnit(officer, state) - local unitId = state.mdtUnitId - - if not unitId then return end - - local unit = units[unitId] - - if not unit then return end - - -- If unit owner leaves, remove everyone from the unit and delete it - if unit.id == officer.callSign then - for i = 1, #unit.members do - local member = unit.members[i] - member.unitId = nil - Player(member.playerId).state.mdtUnitId = nil - end - - units[unitId] = nil - - officers.triggerEvent('ox_mdt:refreshUnits', units) - - return true - end - - for i = 1, #unit.members do - local member = unit.members[i] - - if officer.stateId == member.stateId then - state.mdtUnitId = nil - table.remove(unit.members, i) - - if #unit.members == 0 then - units[unitId] = nil - -- TODO: Remove unit from all calls it's attached to - end - - officers.triggerEvent('ox_mdt:refreshUnits', units) - - return true - end - end -end - ----@param playerId number ----@param unitId string -local function addPlayerToUnit(playerId, unitId) - local officer = officers.get(playerId) - local unit = units[unitId] - local state = Player(playerId).state - - if not officer or not unit then return end - - if state.mdtUnitId then - removePlayerFromUnit(officer, state) - end - - unit.members[#unit.members + 1] = officer - officer.unitId = unitId - state.mdtUnitId = unitId - - officers.triggerEvent('ox_mdt:refreshUnits', units) - - return true -end - ----@param source number ----@param unitType UnitType -registerCallback('ox_mdt:createUnit', function(source, unitType) - local officer = officers.get(source) - - if not officer or not officer.callSign then return end - - ---@type string - local unitId = officer.callSign - local unitName = ('Unit %s'):format(unitId) - - - units[unitId] = { - id = unitId, - members = {}, - name = unitName, - type = unitType - } - - return addPlayerToUnit(source, unitId) and { - id = unitId, - name = unitName - } -end, 'create_unit') - ----@param source number ----@param unitId string -registerCallback('ox_mdt:joinUnit', function(source, unitId) - return addPlayerToUnit(source, unitId) -end) - ----@param source number -registerCallback('ox_mdt:leaveUnit', function(source) - local officer = officers.get(source) - - if not officer then return end - - return removePlayerFromUnit(officer, Player(source).state) -end) - -registerCallback('ox_mdt:getUnits', function() - return units -end) - ----@param source number ----@param data {id: number, officers: string[]} -registerCallback('ox_mdt:setUnitOfficers', function(source, data) - local unit = units[data.id] - local includesCreator = false - local newOfficers = {} - local thisOfficer = officers.get(source) - - if thisOfficer.group ~= 'dispatch' then return end - - for i = 1, #data.officers do - newOfficers[#newOfficers +1] = officers.get(tonumber(data.officers[i])) - end - - for i = 1, #unit.members do - local officer = unit.members[i] - - if officer.callSign == data.id then - includesCreator = true - end - end - - if #data.officers == 0 or not includesCreator then - for i = 1, #units[data.id].members do - local officer = units[data.id].members[i] - Player(officer.playerId).state.mdtUnitId = nil - end - - units[data.id] = nil - officers.triggerEvent('ox_mdt:refreshUnits', units) - - return - end - - units[data.id].members = newOfficers - - for i = 1, #newOfficers do - newOfficers[i].unitId = data.id - Player(newOfficers[i].playerId).state.mdtUnitId = data.id - end - - officers.triggerEvent('ox_mdt:refreshUnits', units) - - return true -end) - ----@param source number ----@param data {id: number, value: string} -registerCallback('ox_mdt:setUnitType', function(source, data) - local officer = officers.get(source) - - if officer.group ~= 'dispatch' and officer.unitId ~= data.id then return end - - units[data.id].type = data.value - - officers.triggerEvent('ox_mdt:refreshUnits', units) - - return true -end) - -local function getUnit(unitId) - return units[unitId] -end - -return { - getUnit = getUnit, - removePlayerFromUnit = removePlayerFromUnit -} diff --git a/server/utils/dbSearch.lua b/server/utils/dbSearch.lua deleted file mode 100644 index 9bc4e88d..00000000 --- a/server/utils/dbSearch.lua +++ /dev/null @@ -1,26 +0,0 @@ ----@generic T ----@param fn async fun(parameters?: table, match?: boolean): T ----@param search string? ----@param offset number? Offset query results when using LIMIT. ----@return T? -local function dbSearch(fn, search, offset) - if not search or search == '' then - return fn({ offset }) - end - - local str = {} - - for word in search:gmatch('%S+') do - str[#str + 1] = '+' - str[#str + 1] = word:gsub('[%p%c]', '') - str[#str + 1] = '*' - end - - if #str > 3 then table.remove(str, 3) end - - search = table.concat(str) - - return fn({ search == '+*' and '' or search, offset }, true) -end - -return dbSearch diff --git a/server/utils/isAuthorised.lua b/server/utils/isAuthorised.lua deleted file mode 100644 index 9d63d500..00000000 --- a/server/utils/isAuthorised.lua +++ /dev/null @@ -1,15 +0,0 @@ -local config = require 'config' -local framework - -CreateThread(function() - framework = require(('server.framework.%s'):format(config.framework)) -end) - ----@param playerId number ----@param permission string ----@return boolean? -local function isAuthorised(playerId, permission) - return framework.isAuthorised(playerId, 'mdt.' .. permission) -end - -return isAuthorised diff --git a/server/utils/registerCallback.lua b/server/utils/registerCallback.lua deleted file mode 100644 index 55953bfd..00000000 --- a/server/utils/registerCallback.lua +++ /dev/null @@ -1,14 +0,0 @@ -local isAuthorised = require 'server.utils.isAuthorised' - ----@param event string ----@param cb fun(playerId: number, ...: any): any ----@param permission string | false | nil -local function registerCallback(event, cb, permission) - lib.callback.register(event, function(source, ...) - if permission ~= false and not isAuthorised(source, permission or 'access') then return false end - - return cb(source, ...) - end) -end - -return registerCallback diff --git a/src/client/animManager.ts b/src/client/animManager.ts new file mode 100644 index 00000000..a7256161 --- /dev/null +++ b/src/client/animManager.ts @@ -0,0 +1,64 @@ +import { cache, sleep } from '@communityox/ox_lib'; +import { requestAnimDict, requestModel } from '@communityox/ox_lib/client'; + +export class AnimManager { + private static tabletAnimDict = 'amb@world_human_seat_wall_tablet@female@base'; + private static tabletEntity: number | null = null; + + static isPlayingAnim() { + return IsEntityPlayingAnim(cache.ped, this.tabletAnimDict, 'base', 3); + } + + static clearAnim() { + if (!this.isPlayingAnim()) return; + + ClearPedSecondaryTask(cache.ped); + } + + static async playAnim() { + if (this.isPlayingAnim()) return; + + await requestAnimDict(this.tabletAnimDict); + TaskPlayAnim(cache.ped, this.tabletAnimDict, 'base', 6.0, 3.0, -1, 49, 1.0, false, false, false); + RemoveAnimDict(this.tabletAnimDict); + } + + static async createTablet() { + const model = await requestModel('prop_cs_tablet'); + + if (!model) return; + + const [x, y, z] = GetEntityCoords(cache.ped, true); + this.tabletEntity = CreateObject(model, x, y, z, true, true, true); + AttachEntityToEntity( + this.tabletEntity, + cache.ped, + GetPedBoneIndex(cache.ped, 28422), + 0.0, + 0.0, + 0.03, + 0.0, + 0.0, + 0.0, + true, + true, + false, + true, + 0, + true + ); + SetModelAsNoLongerNeeded(model); + } + + static async deleteTablet() { + if (!this.tabletEntity) return; + + if (DoesEntityExist(this.tabletEntity)) { + await sleep(300); + + DeleteEntity(this.tabletEntity); + } + + this.tabletEntity = null; + } +} diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 00000000..bf9b22c7 --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1,136 @@ +import { addKeybind, cache } from '@communityox/ox_lib/client'; +import { MdtUiState } from './nui'; +import { PlayerManager } from './playerManager'; +import './nui'; + +addKeybind({ + defaultKey: 'm', + description: 'Open police MDT', + name: 'openMDT', + onPressed: MdtUiState.openMdt, +}); + +let callsAreFocused: false | CitizenTimer = false; +addKeybind({ + defaultKey: 'GRAVE', + description: 'Toggle MDT call focus', + name: 'focusCalls', + onPressed: () => { + if (callsAreFocused !== false) { + clearInterval(callsAreFocused); + callsAreFocused = false; + SetNuiFocus(false, false); + SetNuiFocusKeepInput(false); + return; + } + + if (IsNuiFocused() || IsPauseMenuActive()) return; + + SetNuiFocus(true, true); + SetNuiFocusKeepInput(true); + SetCursorLocation(0.5, 0.5); + + callsAreFocused = setInterval(() => { + DisablePlayerFiring(cache.playerId, true); + DisableControlAction(0, 1, true); + DisableControlAction(0, 2, true); + DisableControlAction(2, 199, true); + DisableControlAction(2, 200, true); + }, 1); + }, +}); + +RegisterNuiCallback('setWaypoint', (data: [number, number], cb: (_: number) => void) => { + SetNewWaypoint(data[0], data[1]); + cb(1); +}); + +const blips: Record = {}; + +onNet('ox_mdt:createCall', (data: { id: number; call: any }) => { + data.call.id = data.id; + + const [x, y, z] = data.call.coords; + const [streetHash] = GetStreetNameAtCoord(x, y, z); + data.call.location = GetStreetNameFromHashKey(streetHash); + + PlaySoundFrontend(-1, 'Near_Miss_Counter_Reset', 'GTAO_FM_Events_Soundset', false); + + SendNuiMessage( + JSON.stringify({ + action: 'addCall', + data: data.call, + }) + ); +}); + +onNet('ox_mdt:editCallUnits', (data: { id: number; call: any }) => { + SendNuiMessage( + JSON.stringify({ + action: 'editCallUnits', + data: data, + }) + ); +}); + +onNet('ox_mdt:updateCallCoords', (data: { id: number; coords: [number, number, number] }) => { + SendNuiMessage( + JSON.stringify({ + action: 'updateCallCoords', + data: data, + }) + ); +}); + +onNet('ox_mdt:setCallUnits', (data: { id: number; units: any }) => { + SendNuiMessage( + JSON.stringify({ + action: 'setCallUnits', + data: data, + }) + ); +}); + +onNet('ox_mdt:refreshUnits', (data: any) => { + SendNuiMessage( + JSON.stringify({ + action: 'refreshUnits', + data: data, + }) + ); +}); + +onNet('ox_mdt:updateOfficerPositions', (data: any[]) => { + if (!MdtUiState.isUiLoaded()) return; + + data.forEach((officer) => { + if (officer.stateId !== PlayerManager.getPlayer().stateid) { + let blip = blips[officer.stateId]; + + if (!blip) { + const blipName = `police:${officer.stateId}`; + blip = AddBlipForCoord(officer.position[0], officer.position[1], officer.position[2]); + blips[officer.stateId] = blip; + + SetBlipSprite(blip, 1); + SetBlipDisplay(blip, 3); + SetBlipColour(blip, 42); + ShowFriendIndicatorOnBlip(blip, true); + + AddTextEntry(blipName, `${officer.firstName} ${officer.lastName} (${officer.callSign})`); + BeginTextCommandSetBlipName(blipName); + EndTextCommandSetBlipName(blip); + SetBlipCategory(blip, 7); + } else { + SetBlipCoords(blip, officer.position[0], officer.position[1], officer.position[2]); + } + } + }); + + SendNuiMessage( + JSON.stringify({ + action: 'updateOfficerPositions', + data: data, + }) + ); +}); diff --git a/src/client/nui/index.ts b/src/client/nui/index.ts new file mode 100644 index 00000000..ee5aaad2 --- /dev/null +++ b/src/client/nui/index.ts @@ -0,0 +1,2 @@ +import './serverCallbacks'; +export * from './mdtState'; diff --git a/src/client/nui/mdtState.ts b/src/client/nui/mdtState.ts new file mode 100644 index 00000000..79cf04ee --- /dev/null +++ b/src/client/nui/mdtState.ts @@ -0,0 +1,95 @@ +import { cache, getLocales, triggerServerCallback } from '@communityox/ox_lib/client'; +import { DbCharge, Officer, ProfileCardData } from '@common/typings'; +import { AnimManager } from '../animManager'; +import { SendTypedNUIMessage } from '../utils'; +import { PlayerManager } from '../playerManager'; + +export class MdtUiState { + private static isLoaded = false; + private static isOpen: boolean = false; + + static isUiLoaded() { + return this.isLoaded; + } + + static closeMDT(hideUi: boolean) { + if (!MdtUiState.isOpen) return; + + MdtUiState.isOpen = false; + + if (hideUi) { + SendTypedNUIMessage('setVisible', false); + SetNuiFocus(false, false); + } + + AnimManager.clearAnim(); + AnimManager.deleteTablet(); + } + + static async openMdt() { + const officerData = await triggerServerCallback>('ox_mdt:openMDT', 500); + + if (!officerData) return; + + MdtUiState.isOpen = true; + + AnimManager.playAnim(); + AnimManager.createTablet(); + + if (!MdtUiState.isLoaded) { + const profileCards = + ((await triggerServerCallback('ox_mdt:getCustomProfileCards', null)) as ProfileCardData[]) ?? + []; + const charges = + ((await triggerServerCallback>('ox_mdt:getCustomProfileCards', null)) as Record< + string, + DbCharge[] + >) ?? {}; + + SendTypedNUIMessage<{ + profileCards: ProfileCardData[]; + locale: string; + locales: {}; + charges: Record; + }>('setInitData', { + profileCards, + charges, + locale: GetConvar('ox:locale', 'en'), + locales: getLocales(), + }); + + MdtUiState.isLoaded = true; + } + + SendTypedNUIMessage('setVisible', { + ...PlayerManager.getOfficerData(), + unit: LocalPlayer.state.mdtUnitId, + group: PlayerManager.getGroupInfo(), + permissions: PlayerManager.getPermissions(), + }); + + SetNuiFocus(true, true); + } + + static setLoadedState(state: boolean) { + MdtUiState.isLoaded = state; + } +} + +on('ox:playerLogout', () => { + MdtUiState.closeMDT(true); + MdtUiState.setLoadedState(false); +}); + +RegisterNuiCallback('hideMDT', (_: unknown, cb: ({}) => void) => { + SetNuiFocus(false, false); + MdtUiState.closeMDT(false); +}); + +on('onResourceStop', (resource: string) => { + if (resource !== cache.resource) return; + + MdtUiState.closeMDT(true); +}); + +exports('openMDT', () => MdtUiState.openMdt()); diff --git a/src/client/nui/serverCallbacks.ts b/src/client/nui/serverCallbacks.ts new file mode 100644 index 00000000..7658795a --- /dev/null +++ b/src/client/nui/serverCallbacks.ts @@ -0,0 +1,85 @@ +import { triggerServerCallback } from '@communityox/ox_lib/client'; +import { Calls, Officer } from '@common/typings'; +import { PlayerManager } from '../playerManager'; + +const serverNuiCallback = (event: string, clientCb?: (data: T, cb: (data: T) => void) => void) => { + RegisterNuiCallback(event, async function (data: T, cb: (data: T) => void) { + const response = (await triggerServerCallback>(`ox_mdt:${event}`, null, data)) as T; + + if (clientCb) { + clientCb(response, cb); + } else { + cb(response); + } + }); +}; + +// Dashboard +serverNuiCallback('getAnnouncements'); +serverNuiCallback('getWarrants'); +serverNuiCallback('createAnnouncement'); +serverNuiCallback('editAnnouncement'); +serverNuiCallback('deleteAnnouncement'); +serverNuiCallback('getBOLOs'); +serverNuiCallback('deleteBOLO'); +serverNuiCallback('createBOLO'); +serverNuiCallback('editBOLO'); + +// Reports +serverNuiCallback('getCriminalProfiles'); +serverNuiCallback('createReport'); +serverNuiCallback('getReports'); +serverNuiCallback('getReport'); +serverNuiCallback('deleteReport'); +serverNuiCallback('setReportTitle'); +serverNuiCallback('getSearchOfficers'); +serverNuiCallback('addCriminal'); +serverNuiCallback('removeCriminal'); +serverNuiCallback('saveCriminal'); +serverNuiCallback('addOfficer'); +serverNuiCallback('removeOfficer'); +serverNuiCallback('addEvidence'); +serverNuiCallback('removeEvidence'); +serverNuiCallback('saveReportContents'); +serverNuiCallback('getRecommendedWarrantExpiry'); + +// Profiles +serverNuiCallback('getProfiles'); +serverNuiCallback('getProfile'); +serverNuiCallback('saveProfileImage'); +serverNuiCallback('saveProfileNotes'); + +// Dispatch +serverNuiCallback('attachToCall'); +serverNuiCallback('completeCall'); +serverNuiCallback('detachFromCall'); +serverNuiCallback('getCalls', (data, cb) => { + // Assign street names to data from the sever to be sent to UI + for (let i = 0; i < data.length; i++) { + const call = data[i]; + const [x, y, z = 0] = call.coords; + const [h, _] = GetStreetNameAtCoord(x, y, z); + data[i].location = GetStreetNameFromHashKey(h); + } + + cb(data); +}); +serverNuiCallback('getUnits'); +serverNuiCallback('createUnit'); +serverNuiCallback('joinUnit'); +serverNuiCallback('leaveUnit'); +serverNuiCallback('setCallUnits'); +serverNuiCallback('getActiveOfficers'); +serverNuiCallback('setUnitOfficers'); +serverNuiCallback('setUnitType'); +serverNuiCallback('setOfficerCallSign'); +serverNuiCallback('setOfficerRank'); +serverNuiCallback('fireOfficer'); +serverNuiCallback('hireOfficer'); +serverNuiCallback('fetchRoster', ({ officers, totalRecords }: { officers: Officer[]; totalRecords: number }, cb) => { + officers.forEach((officer) => { + officer.title = PlayerManager.getGradeLabel(officer.group, officer.grade); + }); + + cb({ officers, totalRecords }); +}); diff --git a/src/client/playerManager.ts b/src/client/playerManager.ts new file mode 100644 index 00000000..8cdacf61 --- /dev/null +++ b/src/client/playerManager.ts @@ -0,0 +1,115 @@ +import { GetGroup, OxGroupPermissions } from '@communityox/ox_core'; +import { GetPlayer } from '@communityox/ox_core/client'; +import { Config } from '@common/index'; +import { Officer } from '@common/typings'; +import { MdtUiState } from './nui'; + +export class PlayerManager { + private static player = GetPlayer(); + private static officer: Partial = {}; + private static group: string; + private static grade: number; + private static label: string; + + static getPlayer() { + return PlayerManager.player; + } + + static getGradeLabel(groupName: string, gradeId: number): string { + const group = GetGroup(groupName); + const grade = group.grades[gradeId - 1]; + return `${group.label} ${grade}`; + } + + static getGroupInfo() { + if (!PlayerManager.player) PlayerManager.player = GetPlayer(); + + PlayerManager.group = PlayerManager.player.get('activeGroup'); + + if (!PlayerManager.group || !Config.policeGroups.includes(PlayerManager.group)) return; + + PlayerManager.grade = PlayerManager.player.getGroup(PlayerManager.group); + PlayerManager.label = PlayerManager.getGradeLabel(PlayerManager.group, PlayerManager.grade); + + return { + group: PlayerManager.group, + grade: PlayerManager.grade, + label: PlayerManager.label, + }; + } + + static getGroupTitle(officer: Officer) { + return PlayerManager.getGradeLabel(officer.group, officer.grade); + } + + static getOfficerData() { + if (!PlayerManager.player) PlayerManager.player = GetPlayer(); + + this.getGroupInfo(); + + PlayerManager.officer.stateId = PlayerManager.player.get('stateId'); + PlayerManager.officer.firstName = PlayerManager.player.get('firstName'); + PlayerManager.officer.lastName = PlayerManager.player.get('lastName'); + PlayerManager.officer.group = PlayerManager.group; + PlayerManager.officer.title = PlayerManager.label; + PlayerManager.officer.grade = PlayerManager.grade; + + return PlayerManager.officer; + } + + static getPermissions() { + const groupPermissions: OxGroupPermissions = GlobalState[`group.${PlayerManager.group}:permissions`]; + const permissions: Record = {}; + + if (!groupPermissions) return permissions; + + for (const [gradeKey, perms] of Object.entries(groupPermissions)) { + const grade = parseInt(gradeKey); + + if (grade <= PlayerManager.grade) { + for (const [permNode, access] of Object.entries(perms)) { + if (permNode.startsWith('mdt.')) { + const permission = permNode.replace('mdt.', ''); + permissions[permission] = access; + } + } + } + } + + return permissions; + } +} + +on('ox:playerLoaded', () => { + PlayerManager.getOfficerData(); +}); + +onNet('ox:setGroup', () => { + const { group: lastGroup } = PlayerManager.getPlayer(); + + const updatedPlayer = PlayerManager.getOfficerData(); + + if ((!updatedPlayer.group && lastGroup) || lastGroup !== updatedPlayer.group) { + MdtUiState.closeMDT(true); + } +}); + +RegisterNuiCallback('getDepartmentsData', (_: null, cb: (data: object) => void) => { + let groups: { + [key: string]: { + label: string; + ranks: string[]; + }; + } = {}; + + Config.policeGroups.forEach((group) => { + const groupData = GetGroup(group); + + groups[group] = { + label: groupData.label, + ranks: groupData.grades, + }; + }); + + cb(groups); +}); diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json new file mode 100644 index 00000000..f0213774 --- /dev/null +++ b/src/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../common/tsconfig.json", + "compilerOptions": { + "types": ["@citizenfx/client"] + }, + "include": ["./"] +} diff --git a/src/client/utils.ts b/src/client/utils.ts new file mode 100644 index 00000000..b9190481 --- /dev/null +++ b/src/client/utils.ts @@ -0,0 +1,6 @@ +export function SendTypedNUIMessage(action: string, data: T) { + SendNUIMessage({ + action, + data, + }); +} diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 00000000..6f50ee1b --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,16 @@ +import { cache } from '@communityox/ox_lib'; + +export * from './locales'; + +export function LoadFile(path: string) { + return LoadResourceFile(cache.resource, path); +} + +export function LoadJsonFile(path: string): T { + return JSON.parse(LoadFile(path)) as T; +} + +export const Config = LoadJsonFile('data/config.json'); +export const Permissions = LoadJsonFile('data/permissions.json'); + +export type PermissionKeys = keyof typeof Permissions; diff --git a/src/common/locales/index.ts b/src/common/locales/index.ts new file mode 100644 index 00000000..66757960 --- /dev/null +++ b/src/common/locales/index.ts @@ -0,0 +1,5 @@ +import { locale, FlattenObjectKeys } from '@communityox/ox_lib'; + +type Locales = FlattenObjectKeys; + +export const Locale = (str: T, ...args: any[]) => locale(str, ...args); diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json new file mode 100644 index 00000000..6fc2327b --- /dev/null +++ b/src/common/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "~/*": ["../../*"], + "@common/*": ["../common/*"] + }, + "types": ["@types/node", "@citizenfx/server", "@citizenfx/client"] + }, + "include": ["./"] +} diff --git a/src/common/typings/charges.ts b/src/common/typings/charges.ts new file mode 100644 index 00000000..fc0849eb --- /dev/null +++ b/src/common/typings/charges.ts @@ -0,0 +1,23 @@ +export interface DbCharge { + label: string; + type: 'misdemeanor' | 'felony' | 'infraction'; + category: ChargeCategoryKey; + description: string; + time: number; + fine: number; +} + +export const CHARGE_CATEGORIES: Record = { + 'OFFENSES AGAINST PERSONS': 'Offenses Against Persons', + 'OFFENSES INVOLVING THEFT': 'Offenses Involving Theft', + 'OFFENSES INVOLVING FRAUD': 'Offenses Involving Fraud', + 'OFFENSES INVOLVING DAMAGE TO PROPERTY': 'Offenses Involving Damage To Property', + 'OFFENSES AGAINST PUBLIC ADMINISTRATION': 'Offenses Against Public Administration', + 'OFFENSES AGAINST PUBLIC ORDER': 'Offenses Against Public Order', + 'OFFENSES AGAINST HEALTH AND MORALS': 'Offenses Agaisnt Health And Morals', + 'OFFENSES AGAINST PUBLIC SAFETY': 'Offenses Against Public Safety', + 'OFFENSES INVOLVING THE OPERATION OF A VEHICLE': 'Offenses Involving The Operation Of A Vehicle', + 'OFFENSES INVOLVING THE WELL-BEING OF WILDLIFE': 'Offenses Involving The Well-Being Of Wildlife', +}; + +export type ChargeCategoryKey = keyof typeof CHARGE_CATEGORIES; diff --git a/src/common/typings/game.ts b/src/common/typings/game.ts new file mode 100644 index 00000000..aa25dcf3 --- /dev/null +++ b/src/common/typings/game.ts @@ -0,0 +1,5 @@ +export type Vector3 = { + x: number; + y: number; + z: number; +}; diff --git a/src/common/typings/index.ts b/src/common/typings/index.ts new file mode 100644 index 00000000..9c23488d --- /dev/null +++ b/src/common/typings/index.ts @@ -0,0 +1,4 @@ +export * from './game'; +export * from './mdt'; +export * from './profileCards'; +export * from './charges'; diff --git a/src/common/typings/mdt.ts b/src/common/typings/mdt.ts new file mode 100644 index 00000000..e456d9a4 --- /dev/null +++ b/src/common/typings/mdt.ts @@ -0,0 +1,194 @@ +export interface PartialProfileData { + firstName: string; + lastName: string; + dob: number; + stateId: string; + image?: string; +} + +export interface Profile extends PartialProfileData { + charid: number | string; + notes?: string; + licenses?: Record[]; + vehicles?: { label: string; plate: string }[]; + pastCharges?: { label: string; count: number }[]; + relatedReports?: { title: string; author: string; date: string; id: number }[]; +} + +export interface Officer { + firstName: string; + lastName: string; + stateId: string; + callSign?: string; + unitId?: string; + position?: [number, number]; + title?: string; + ped: number; + playerId: number; + grade: number; + group: string; +} + +export interface CriminalProfile extends PartialProfileData {} + +export interface Criminal extends CriminalProfile { + charges: SelectedCharge[]; + issueWarrant: boolean; + pleadedGuilty?: boolean; + processed?: boolean; + warrantExpiry?: string; + penalty: { time: number; fine: number; reduction?: number }; +} + +export interface Charge { + label: string; + type: 'misdemeanour' | 'felony' | 'infraction'; + description: string; + time: number; + fine: number; + count: number; +} + +export interface SelectedCharge { + label: string; + count: number; + time: number; + fine: number; +} + +export interface Evidence { + label: string; + image: string; +} + +export interface Report { + title: string; + id: number; + description?: string; + officersInvolved: { name: string; callSign: string }; + evidence: Evidence[]; + criminals: Criminal[]; +} + +export interface PartialReportData { + title: string; + author: string; + date: string; + id: number; +} + +export interface Announcement { + id: number; + contents: string; + createdAt: string; + firstName: string; + lastName: string; + creator: string; +} + +export type UnitType = 'car' | 'motor' | 'heli' | 'boat'; +export const isUnitTypeValid = (value: string): boolean => { + return ['car', 'motor', 'heli', 'boat'].includes(value); +}; + +export interface Unit { + id: string; + name: string; + members: Officer[]; + type: UnitType; +} + +export type Units = Unit[]; + +export interface CallInfo { + plate?: string; + vehicle?: string; +} + +export interface Call { + id: number; + offense: string; + code: string; + completed: boolean | number; + coords: [number, number]; + blip: number; + units: Units; + time: number; + location: string; + isEmergency?: boolean; + info: CallInfo; +} + +export type Calls = Call[]; + +export interface CallDataInfo { + plate?: string; + vehicle?: string; +} + +export interface CallData { + offense: string; + code: string; + coords: [number, number]; + info: CallDataInfo; + blip: number; +} + +export type FetchOfficers = { + firstName: string; + lastName: string; + stateId: string; +}[]; + +export type FetchCriminals = { + stateId: string; + firstName: string; + lastName: string; + reduction: number; + dob: number; + warrantExpiry?: string; + processed?: number | boolean; + pleadedGuilty?: number | boolean; + issueWarrant?: boolean; + [key: string]: any; +}[]; + +export type FetchCharges = { + stateId: string; + label: string; + time?: number; + fine?: number; + count: number; +}[]; + +// for the getBolos db method joining data for display +export interface DbBoloRecap { + id: number; + stateId: string; + contents: string; + callSign: string | null; + image: string | null; + firstName: string; + lastName: string; + images: string; + createdAt: string; +} + +export type BoloRecap = Omit & { + images: string[]; +}; + +export interface DBBolo { + id: number; + creator: string; + contents: string; + createdAt: string; +} + +export interface Warrant { + reportId: number; + stateId: string; + firstName: string; + lastName: string; + expiresAt: string; // Format: YYYY-MM-DD HH:mm:ss +} diff --git a/src/common/typings/profileCards.ts b/src/common/typings/profileCards.ts new file mode 100644 index 00000000..fcbfff32 --- /dev/null +++ b/src/common/typings/profileCards.ts @@ -0,0 +1,8 @@ +import { Profile } from './mdt'; + +export interface ProfileCardData { + id: string; + title: string; + icon: string; + getData: (profile: Profile) => Promise; +} diff --git a/src/server/callbacks/announcements.ts b/src/server/callbacks/announcements.ts new file mode 100644 index 00000000..ad851a20 --- /dev/null +++ b/src/server/callbacks/announcements.ts @@ -0,0 +1,56 @@ +import { Announcement } from '@common/typings'; +import { DB } from '../framework'; +import { OfficerManager } from '../managers/officerManager'; +import { registerAuthorisedCallback } from '../utils/callback'; + +registerAuthorisedCallback('ox_mdt:getAnnouncements', async (source, page: number) => { + const announcements = await DB.getAnnouncements(page); + + return { + hasMore: announcements.length === 5, + announcements, + }; +}); + +registerAuthorisedCallback( + 'ox_mdt:createAnnouncement', + (source, contents: string) => { + const officer = OfficerManager.get(source); + + if (!officer) return true; + + return DB.createAnnouncement(officer.stateId, contents); + }, + 'create_announcement' +); + +type AnnouncementEditData = { + id: number; + announcement: Announcement; + value: string; +}; +registerAuthorisedCallback('ox_mdt:editAnnouncement', async (source, data: AnnouncementEditData) => { + const officer = OfficerManager.get(source); + + if (!officer) return false; + + const announcement = await DB.selectAnnouncement(data.id); + + if (announcement.creator !== officer.stateId) return; + + return await DB.updateAnnouncementContents(announcement.id, data.value); +}); + +registerAuthorisedCallback( + 'ox_mdt:deleteAnnouncement', + async (source, id) => { + const officer = OfficerManager.get(source); + + const announcement = await DB.selectAnnouncement(id); + + if (announcement.creator !== officer.stateId) return; + + return await DB.removeAnnouncement(id); + }, + 'delete_announcement' +); diff --git a/src/server/callbacks/bolos.ts b/src/server/callbacks/bolos.ts new file mode 100644 index 00000000..e243dfc6 --- /dev/null +++ b/src/server/callbacks/bolos.ts @@ -0,0 +1,51 @@ +import { DB } from '../framework'; +import { OfficerManager } from '../managers/officerManager'; +import { registerAuthorisedCallback } from '../utils/callback'; + +registerAuthorisedCallback('ox_mdt:getBOLOs', async (source, page: number) => { + const bolos = await DB.getBolos(page); + + return { + hasMore: bolos.length === 5, + bolos, + }; +}); + +registerAuthorisedCallback( + 'ox_mdt:deleteBOLO', + async (source, id: number) => { + return DB.deleteBOLO(id); + }, + 'delete_bolo' +); + +type BoloEditData = { + id: number; + contents: string; + images: string[]; +}; +registerAuthorisedCallback('ox_mdt:editBOLO', async (source, data: BoloEditData) => { + const officer = OfficerManager.get(source); + + if (!officer) return; + + const bolo = await DB.selectBolo(data.id); + + if (!bolo || bolo.creator !== officer.stateId) return; + + return await DB.updateBOLO(data.id, data.contents, data.images); +}); + +type BoloCreateData = { + contents: string; + images: string[]; +}; +registerAuthorisedCallback( + 'ox_mdt:createBOLO', + async (source, data: BoloCreateData) => { + const officer = OfficerManager.get(source); + + return await DB.createBolo(officer.stateId, data.contents, data.images); + }, + 'create_bolo' +); diff --git a/src/server/callbacks/calls.ts b/src/server/callbacks/calls.ts new file mode 100644 index 00000000..c95fb848 --- /dev/null +++ b/src/server/callbacks/calls.ts @@ -0,0 +1,43 @@ +import { CallManager } from '../managers/callManager'; +import { OfficerManager } from '../managers/officerManager'; +import { registerAuthorisedCallback } from '../utils/callback'; + +registerAuthorisedCallback('ox_mdt:getCalls', (source, type: 'active' | 'completed') => { + return CallManager.getCalls(type); +}); + +registerAuthorisedCallback('ox_mdt:attachToCall', (source, callId: number) => { + const unitId = Player(source).state.mdtUnitId; + + if (!unitId) return false; + + return CallManager.addUnitToCall(callId, unitId); +}); + +registerAuthorisedCallback('ox_mdt:detachFromCall', (source, callId: number) => { + const unitId = Player(source).state.mdtUnitId; + + if (!unitId) return false; + + return CallManager.removeUnitFromCall(callId, unitId); +}); + +registerAuthorisedCallback( + 'ox_mdt:completeCall', + (source, callId: number) => { + return CallManager.markCallComplete(callId); + }, + 'mark_call_completed' +); + +type SetCallUnitsData = { + id: number; // callId + units: string[]; +}; +registerAuthorisedCallback('ox_mdt:setCallUnits', (source, data: SetCallUnitsData) => { + const officer = OfficerManager.get(source); + + if (!officer || officer.group !== 'dispatch') return false; + + return CallManager.setUnitsOnCall(data.id, data.units); +}); diff --git a/src/server/callbacks/charges.ts b/src/server/callbacks/charges.ts new file mode 100644 index 00000000..a8c976e3 --- /dev/null +++ b/src/server/callbacks/charges.ts @@ -0,0 +1,21 @@ +import { oxmysql } from '@communityox/oxmysql'; +import { registerAuthorisedCallback } from '../utils/callback'; +import { CHARGE_CATEGORIES, ChargeCategoryKey, DbCharge } from '@common/typings'; + +const charges: Record = {}; + +for (let category of Object.keys(CHARGE_CATEGORIES)) { + charges[category] = []; +} + +oxmysql.ready(async () => { + const dbCharges = await oxmysql.rawExecute('SELECT * FROM `ox_mdt_offenses`'); + + dbCharges.forEach((charge) => { + const category = charge.category; + + charges[category].push(charge); + }); +}); + +registerAuthorisedCallback('ox_mdt:getAllCharges', () => charges); diff --git a/src/server/callbacks/index.ts b/src/server/callbacks/index.ts new file mode 100644 index 00000000..783e2ded --- /dev/null +++ b/src/server/callbacks/index.ts @@ -0,0 +1,9 @@ +import './announcements'; +import './bolos'; +import './calls'; +import './charges'; +import './officers'; +import './profileCards'; +import './reports'; +import './units'; +import './warrants'; diff --git a/src/server/callbacks/officers.ts b/src/server/callbacks/officers.ts new file mode 100644 index 00000000..201e824f --- /dev/null +++ b/src/server/callbacks/officers.ts @@ -0,0 +1,149 @@ +import { GetPlayer, GetPlayerFromFilter } from '@communityox/ox_core/server'; +import { DB } from '../framework'; +import { OfficerManager } from '../managers/officerManager'; +import { registerAuthorisedCallback } from '../utils/callback'; +import { oxmysql } from '@communityox/oxmysql'; +import { Config } from '@common/index'; + +registerAuthorisedCallback('ox_mdt:openMDT', (source) => { + return OfficerManager.get(source); +}); + +registerAuthorisedCallback('ox_mdt:getSearchOfficers', async (source, data: string) => { + return await DB.searchOfficers(data); +}); + +registerAuthorisedCallback('ox_mdt:getActiveOfficers', (source) => { + return OfficerManager.getAll(); +}); + +registerAuthorisedCallback( + 'ox_mdt:setOfficerCallSign', + async (source, data: { stateId: string; callSign: string }) => { + const exists = await DB.selectOfficerCallsign(data.callSign); + + if (exists) return false; + + await DB.updateOfficerCallsign(data.stateId, data.callSign); + + return true; + }, + 'set_call_sign' +); + +registerAuthorisedCallback( + 'ox_mdt:setOfficerRank', + async ( + source, + data: { + stateId: string; + group: string; + grade: number; + } + ) => { + const player = GetPlayer(source); + const grade = player.getGroup(data.group); + + if (player.stateId === data.stateId) return false; + if (!grade || grade <= data.grade) return false; + + const target = GetPlayerFromFilter({ + stateId: data.stateId, + groups: data.group, + }); + + if (target) { + if (!target.getGroup(data.group)) return false; + + return target.setGroup(data.group, data.grade + 1); + } else { + await oxmysql.prepare( + ` + UPDATE character_groups + SET grade = ? + + WHERE charId = ( + SELECT charId + FROM characters + WHERE stateId = ? + ) AND name = ? `, + [data.grade + 1, data.stateId, data.group] + ); + + return true; + } + }, + 'set_officer_rank' +); + +registerAuthorisedCallback( + 'ox_mdt:fireOfficer', + async (source, stateId: string) => { + const target = GetPlayerFromFilter({ stateId }); + + if (target) { + Config.policeGroups.forEach((group) => { + target.setGroup(group, 0); + }); + + return true; + } else { + const charId = await oxmysql.prepare('SELECT charid FROM characters WHERE stateId = ?', [stateId]); + + if (!charId) return false; + + const placeholders = Config.policeGroups.map(() => '?').join(','); + await oxmysql.update(`DELETE FROM character_groups WHERE charid = ? AND name IN (${placeholders})`, [ + charId, + ...Config.policeGroups, + ]); + + return true; + } + }, + 'fire_officer' +); + +registerAuthorisedCallback( + 'ox_mdt:hireOfficer', + async (source, stateId: string) => { + const target = GetPlayerFromFilter({ stateId }); + + if (target) { + if (target.getGroup(Config.policeGroups)) return false; + + target.setGroup('police', 1); + return true; + } else { + const charId = await oxmysql.prepare('SELECT charid FROM characters WHERE stateId = ?', [stateId]); + + if (!charId) return false; + + try { + await oxmysql.prepare('INSERT INTO `character_groups` (`charId`, `name`, `grade`) VALUES (?, ?, ?)', [ + charId, + 'police', + 1, + ]); + + return true; + } catch { + return false; + } + } + }, + 'hire_officer' +); + +registerAuthorisedCallback( + 'ox_mdt:fetchRoster', + async ( + source, + data: { + page: number; + search: string; + } + ) => { + return await DB.fetchRoster(data.page, data.search); + } +); diff --git a/src/server/callbacks/profileCards.ts b/src/server/callbacks/profileCards.ts new file mode 100644 index 00000000..038e738f --- /dev/null +++ b/src/server/callbacks/profileCards.ts @@ -0,0 +1,6 @@ +import { registerAuthorisedCallback } from '../utils/callback'; +import { ProfileCard } from '../utils/profileCards'; + +registerAuthorisedCallback('ox_mdt:getCustomProfileCards', async () => { + return ProfileCard.getAll(); +}); diff --git a/src/server/callbacks/reports.ts b/src/server/callbacks/reports.ts new file mode 100644 index 00000000..09ee10d6 --- /dev/null +++ b/src/server/callbacks/reports.ts @@ -0,0 +1,230 @@ +import { Evidence, PartialReportData } from '@common/typings'; +import { DB } from '../framework'; +import { OfficerManager } from '../managers/officerManager'; +import { registerAuthorisedCallback } from '../utils/callback'; + +registerAuthorisedCallback('ox_mdt:getCriminalProfiles', async (source, search) => { + return await DB.getCharacters(search); +}); + +registerAuthorisedCallback( + 'ox_mdt:createReport', + async (source, title: string) => { + const officer = OfficerManager.get(source); + + if (!officer) return; + + return await DB.createReport(title, `${officer.firstName} ${officer.lastName}`); + }, + 'create_report' +); + +registerAuthorisedCallback( + 'ox_mdt:getReports', + async ( + source, + data: { + search: string | number; + page: number; + } + ) => { + let reports: PartialReportData[]; + + if (typeof data.search === 'number') { + reports = await DB.getReportById(data.search); + } else { + reports = await DB.selectReports(data.page, data.search); + } + + return { + hasMore: reports.length === 10, + reports, + }; + } +); + +registerAuthorisedCallback('ox_mdt:getReport', async (source, reportId: number) => { + let report = await DB.getReportById(reportId); + + if (!report) return; + + const officersInvolved = await DB.getOfficersInvolved(reportId); + const evidence = await DB.selectEvidence(reportId); + const criminals = await DB.selectCriminalsInvolved(reportId); + + return { + ...report, + officersInvolved, + evidence, + criminals, + }; +}); + +registerAuthorisedCallback( + 'ox_mdt:deleteReport', + async (source, reportId) => { + return await DB.deleteReport(reportId); + }, + 'delete_report' +); + +registerAuthorisedCallback( + 'ox_mdt:setReportTitle', + async ( + source, + data: { + id: number; + title: string; + } + ) => { + return await DB.updateReportTitle(data.id, data.title); + }, + 'edit_report_title' +); + +registerAuthorisedCallback( + 'ox_mdt:saveReportContents', + async (source, reportId) => { + return DB.deleteReport(reportId); + }, + 'edit_report_contents' +); + +registerAuthorisedCallback( + 'ox_mdt:addCriminal', + async ( + source, + data: { + id: number; + criminalId: string; + } + ) => { + return await DB.addCriminal(data.id, data.criminalId); + }, + 'add_criminal' +); + +registerAuthorisedCallback( + 'ox_mdt:removeCriminal', + async ( + source, + data: { + id: number; + criminalId: string; + } + ) => { + return await DB.removeCriminal(data.id, data.criminalId); + }, + 'remove_criminal' +); + +registerAuthorisedCallback( + 'ox_mdt:addEvidence', + async ( + source, + data: { + id: number; + evidence: Evidence; + } + ) => { + return await DB.addEvidence(data.id, data.evidence.label, data.evidence.image); + }, + 'add_evidence' +); + +registerAuthorisedCallback( + 'ox_mdt:removeEvidence', + async ( + source, + data: { + id: number; + evidence: Evidence; + } + ) => { + return await DB.removeEvidence(data.id, data.evidence.label, data.evidence.image); + }, + 'add_evidence' +); + +registerAuthorisedCallback( + 'ox_mdt:getProfiles', + async ( + source, + data: { + page: number; + search: string; + } + ) => { + try { + const profiles = await DB.selectProfiles(data.page, data.search); + + return { + hasMore: profiles.length === 10, + profiles, + }; + } catch (err) { + console.error(err); + throw err; + } + } +); + +registerAuthorisedCallback('ox_mdt:getProfile', async (source, data: string) => { + return await DB.selectCharacterProfile(data); +}); + +registerAuthorisedCallback( + 'ox_mdt:saveProfileImage', + async ( + source, + data: { + stateId: string; + image: string; + } + ) => { + return await DB.updateProfilePicture(data.stateId, data.image); + }, + 'change_profile_picture' +); + +registerAuthorisedCallback( + 'ox_mdt:saveProfileNotes', + async ( + source, + data: { + stateId: string; + notes: string; + } + ) => { + return await DB.updateProfileNotes(data.stateId, data.notes); + }, + 'edit_profile_notes' +); + +registerAuthorisedCallback( + 'ox_mdt:addOfficer', + async ( + source, + data: { + id: number; + stateId: string; + } + ) => { + return await DB.addOfficer(data.id, data.stateId); + }, + 'add_officer_involved' +); + +registerAuthorisedCallback( + 'ox_mdt:removeOfficer', + async ( + source, + data: { + id: number; + stateId: string; + } + ) => { + return await DB.removeOfficer(data.id, data.stateId); + }, + 'remove_officer_involved' +); diff --git a/src/server/callbacks/units.ts b/src/server/callbacks/units.ts new file mode 100644 index 00000000..058c40da --- /dev/null +++ b/src/server/callbacks/units.ts @@ -0,0 +1,57 @@ +import { isUnitTypeValid, UnitType } from '@common/typings'; +import { registerAuthorisedCallback } from '../utils/callback'; +import { OfficerManager } from '../managers/officerManager'; +import { UnitManager } from '../managers/unitManager'; + +registerAuthorisedCallback( + 'ox_mdt:createUnit', + (source, unitType: UnitType) => { + const officer = OfficerManager.get(source); + + if (!officer || !officer.callSign) return; + + const unitId = officer.callSign; + const unitName = `Unit ${unitId}`; + + UnitManager.createUnit(unitId, unitName, unitType); + const result = UnitManager.addPlayerToUnit(source, unitId); + + return result ? { id: unitId, name: unitName } : false; + }, + 'create_unit' +); + +registerAuthorisedCallback('ox_mdt:joinUnit', (source, unitId) => { + return UnitManager.addPlayerToUnit(source, unitId); +}); + +registerAuthorisedCallback('ox_mdt:leaveUnit', (source) => { + const officer = OfficerManager.get(source); + + if (!officer) return false; + + const state = Player(source).state; + + return UnitManager.removePlayerFromUnit(officer, state); +}); + +registerAuthorisedCallback('ox_mdt:getUnits', () => { + return UnitManager.getUnits(); +}); + +registerAuthorisedCallback('ox_mdt:setUnitOfficers', (source, data: { id: string; officers: string[] }) => { + const officer = OfficerManager.get(source); + + if (!officer || officer.group !== 'dispatch') return false; + + return UnitManager.addPlayerToUnit(data.officers.map(parseInt), data.id); +}); + +registerAuthorisedCallback('ox_mdt:setUnitType', (source, data: { id: string; value: UnitType }) => { + const officer = OfficerManager.get(source); + + if (!officer || officer.group !== 'dispatch' || officer.callSign !== data.id) return false; + if (!isUnitTypeValid(data.value)) return false; + + return UnitManager.updateUnitType(data.id, data.value); +}); diff --git a/src/server/callbacks/warrants.ts b/src/server/callbacks/warrants.ts new file mode 100644 index 00000000..32b905e5 --- /dev/null +++ b/src/server/callbacks/warrants.ts @@ -0,0 +1,41 @@ +import { Charge, Criminal } from '@common/typings'; +import { registerAuthorisedCallback } from '../utils/callback'; +import { DB } from '../framework'; + +registerAuthorisedCallback( + 'ox_mdt:saveCriminal', + async ( + source, + data: { + id: number; + criminal: Criminal; + } + ) => { + if (data.criminal.issueWarrant) { + await DB.createWarrant(data.id, data.criminal.stateId, data.criminal.warrantExpiry); + } else { + await DB.removeWarrant(data.id, data.criminal.stateId); + } + + return await DB.saveCriminal(data.id, data.criminal); + }, + 'save_criminal' +); + +registerAuthorisedCallback('ox_mdt:getRecommendedWarrantExpiry', (source, charges: Charge[]) => { + const currentTime = Date.now(); + const baseWarrantDuration = 259200000; + let addonTime = 0; + + for (const charge of charges) { + if (charge.time !== 0) { + addonTime += charge.time * 60 * 60000 * charge.count; + } + } + + return currentTime + addonTime + baseWarrantDuration; +}); + +registerAuthorisedCallback('ox_mdt:getWarrants', async (source, search: string) => { + return await DB.selectWarrants(search); +}); diff --git a/src/server/framework/authorizedCheck.ts b/src/server/framework/authorizedCheck.ts new file mode 100644 index 00000000..24092e9a --- /dev/null +++ b/src/server/framework/authorizedCheck.ts @@ -0,0 +1,10 @@ +import { GetPlayer } from '@communityox/ox_core/server'; + +export const isAuthorised = (source: number, permission: string): boolean => { + const player = GetPlayer(source); + const group = player.get('activeGroup'); + + if (!group) return false; + + return player.hasPermission(`group.${group}.mdt.${permission}`); +}; diff --git a/src/server/framework/db.ts b/src/server/framework/db.ts new file mode 100644 index 00000000..ceca11f9 --- /dev/null +++ b/src/server/framework/db.ts @@ -0,0 +1,688 @@ +import { oxmysql } from '@communityox/oxmysql'; +import { + PartialProfileData, + Officer, + Profile, + FetchOfficers, + FetchCriminals, + Announcement, + DBBolo, + BoloRecap, + PartialReportData, + FetchCharges, + Criminal, + Warrant, + DbBoloRecap, +} from '@common/typings'; +import { Ox } from '@communityox/ox_core'; +import { ProfileCard } from '../utils/profileCards'; +import { Config } from '@common/index'; + +export class DB { + private static async query(query: string, params?: any[]): Promise { + const resp = await oxmysql.rawExecute(query, params); + return (resp || []) as T[]; + } + + // DONT COMMIT + private static formatSearchValue(search: string): string | null { + const words = search.trim().split(/\s+/); + + const cleanedWords = words + .map((word) => word.replace(/[^\w\s]/gi, '')) + .filter((word) => word.length > 0) + .map((word) => `+${word}*`); + + return cleanedWords.length > 0 ? cleanedWords.join(' ') : null; + } + + static async getVehicles(parameters: [any]): Promise<{ label: string; plate: string }[]> { + const vehicles = await this.query<{ plate: string; model: string }>( + 'SELECT `plate`, `model` FROM `vehicles` WHERE `owner` = ?', + parameters + ); + + return vehicles.map((v) => ({ + plate: v.plate, + label: Ox.GetVehicleData(v.model)?.name || v.model, + })); + } + + static async getLicenses(parameters: [any]): Promise<{ label: string; issued: number }[]> { + return this.query( + ` + SELECT + ox_licenses.label, + JSON_VALUE(character_licenses.data, "$.issued") AS \`issued\` + + FROM character_licenses + LEFT JOIN ox_licenses ON ox_licenses.name = character_licenses.name + + WHERE character_licenses.charId = ?`, + parameters + ); + } + + static async getCharacters(search: string): Promise { + const formattedSearch = search ? this.formatSearchValue(search) : null; + + // If no inputted search value return + if (!formattedSearch) return []; + + return this.query( + ` + SELECT + firstName, + lastName, + DATE_FORMAT(dateofbirth, "%Y-%m-%d") as dob, + stateId + FROM characters + WHERE MATCH (stateId, firstName, lastName) AGAINST (? IN BOOLEAN MODE)`, + [formattedSearch] + ); + } + + static async getOfficersInvolved(reportId: number): Promise { + return this.query( + ` + SELECT + characters.firstName, + characters.lastName, + characters.stateId, + profile.callSign + FROM ox_mdt_reports_officers officer + LEFT JOIN characters ON characters.stateId = officer.stateId + LEFT JOIN ox_mdt_profiles profile ON characters.stateId = profile.stateId + + WHERE reportid = ?`, + [reportId] + ); + } + + static async getAnnouncements(page: number): Promise { + const pageSize = 5; + return this.query( + ` + SELECT + a.id, + a.contents, + a.creator AS stateId, + b.firstName, + b.lastName, + c.image, + c.callSign, + DATE_FORMAT(a.createdAt, "%Y-%m-%d %T") AS createdAt + FROM \`ox_mdt_announcements\` a + LEFT JOIN \`characters\` b ON b.stateId = a.creator + LEFT JOIN \`ox_mdt_profiles\` c ON c.stateId = a.creator + + ORDER BY id DESC LIMIT ? OFFSET ?`, + [pageSize, (page - 1) * pageSize] + ); + } + + static async createAnnouncement(stateId: string, contents: string): Promise { + return await oxmysql.prepare('INSERT INTO `ox_mdt_announcements` (`creator`, `contents`) VALUES (?, ?)', [ + stateId, + contents, + ]); + } + + static async selectAnnouncement(id: number): Promise { + return await oxmysql.prepare('SELECT * FROM `ox_mdt_announcements` WHERE `id` = ?', [id]); + } + + static async updateAnnouncementContents(id: number, contents: string) { + return await oxmysql.prepare('UPDATE `ox_mdt_announcements` SET `contents` = ? WHERE `id` = ?', [contents, id]); + } + + static async removeAnnouncement(id: number) { + return await oxmysql.prepare('DELETE FROM `ox_mdt_announcements` WHERE `id` = ?', [id]); + } + + static async getBolos(page: number): Promise { + const pageSize = 5; + const bolos = await this.query( + `SELECT + a.id, + a.creator AS stateId, + a.contents, + b.callSign, + b.image, + c.firstName, + c.lastName, + JSON_ARRAYAGG(d.image) AS images, + DATE_FORMAT(a.createdAt, "%Y-%m-%d %T") AS createdAt + FROM + \`ox_mdt_bolos\` a + LEFT JOIN + \`ox_mdt_profiles\` b + ON + b.stateId = a.creator + LEFT JOIN + \`characters\` c + ON + c.stateId = b.stateId + LEFT JOIN + \`ox_mdt_bolos_images\` d + ON + d.boloId = a.id + GROUP BY \`id\` ORDER BY \`id\` DESC LIMIT ? OFFSET ?`, + [pageSize, (page - 1) * pageSize] + ); + + return bolos.map((bolo) => { + let parsedImages: (string | null)[] = []; + try { + parsedImages = typeof bolo.images === 'string' ? JSON.parse(bolo.images) : bolo.images; + } catch (e) { + parsedImages = []; + } + + return { + ...bolo, + images: (parsedImages || []).filter((img): img is string => img !== null), + }; + }); + } + + static async deleteBOLO(id: number) { + return await oxmysql.prepare('DELETE FROM `ox_mdt_bolos` WHERE id = ?', [id]); + } + + static async selectBolo(id: number): Promise { + return await oxmysql.prepare('SELECT * FROM `ox_mdt_bolos` WHERE `id` = ?', [id]); + } + + static async updateBOLO(id: number, contents: string, images: string[]) { + const queries: [string, (string | number)[]][] = [['DELETE FROM `ox_mdt_bolos_images` where `boloId` = ? ', [id]]]; + + images.forEach((img) => { + queries.push(['INSERT INTO `ox_mdt_bolos_images` (`boloId`, `image`) VALUES (?, ?)', [id, img]]); + }); + + queries.push(['UPDATE `ox_mdt_bolos` SET `contents` = ? WHERE `id` = ?', [contents, id]]); + + return oxmysql.transaction(queries); + } + + static async createBolo(creator: string, contents: string, images: string[]) { + const boloId: number = await oxmysql.prepare('INSERT INTO `ox_mdt_bolos` (`creator`, `contents`) VALUES (?, ?)', [ + creator, + contents, + ]); + + const queries: [string, (string | number)[]][] = images.map((img) => [ + 'INSERT INTO `ox_mdt_bolos_images` (`boloId`, `image`) VALUES (?, ?)', + [boloId, img], + ]); + + await oxmysql.transaction(queries); + + return boloId; + } + + static async createReport(title: string, author: string) { + return await oxmysql.prepare('INSERT INTO `ox_mdt_reports` (`title`, `author`) VALUES (?, ?)', [title, author]); + } + + static async getReportById(id: number): Promise { + return await oxmysql.prepare( + 'SELECT `id`, `title`, `description`, DATE_FORMAT(`date`, "%Y-%m-%d %T") as date FROM `ox_mdt_reports` WHERE `id` = ?', + [id] + ); + } + + static async selectReports(page: number, search: string): Promise { + const offset = (page - 1) * 10; + const formattedSearch = search ? this.formatSearchValue(search) : null; + + const selectReportQuery = + 'SELECT `id`, `title`, `author`, DATE_FORMAT(`date`, "%Y-%m-%d %T") as date FROM `ox_mdt_reports`'; + + if (!formattedSearch) { + return this.query(selectReportQuery + ' ORDER BY `id` DESC LIMIT 10 OFFSET ?', [offset]); + } else { + return this.query( + selectReportQuery + + ' WHERE MATCH (`title`, `author`, `description`) AGAINST (? IN BOOLEAN MODE) ORDER BY `id` DESC LIMIT 10 OFFSET ?', + [formattedSearch, offset] + ); + } + } + + static async selectEvidence(id: number) { + return await this.query('SELECT `label`, `image` FROM `ox_mdt_reports_evidence` WHERE reportId = ?', [id]); + } + + static async selectCriminalsInvolved(id: number) { + const dbCriminals = await oxmysql.rawExecute( + ` + SELECT DISTINCT + criminal.stateId, + characters.firstName, + characters.lastName, + characters.dateOfBirth AS dob, + criminal.reduction, + DATE_FORMAT(criminal.warrantExpiry, "%Y-%m-%d") AS warrantExpiry, + criminal.processed, + criminal.pleadedGuilty + FROM + ox_mdt_reports_criminals criminal + LEFT JOIN + characters ON characters.stateId = criminal.stateId + WHERE + reportid = ?`, + [id] + ); + + const dbCharges = await oxmysql.rawExecute( + ` + SELECT + stateId, + charge as label, + time, + fine, + count + FROM + ox_mdt_reports_charges + WHERE + reportid = ? + GROUP BY + charge, stateId`, + [id] + ); + + const criminals: Criminal[] = []; + const charges = dbCharges || []; + + for (const dbCrim of dbCriminals) { + const criminal: Criminal = { + stateId: dbCrim.stateId, + firstName: dbCrim.firstName, + lastName: dbCrim.lastName, + dob: dbCrim.dob, + charges: [], + issueWarrant: typeof dbCrim.warrantExpiry === 'string', + processed: !!dbCrim.processed, + pleadedGuilty: !!dbCrim.pleadedGuilty, + penalty: { + time: 0, + fine: 0, + reduction: dbCrim.reduction, + }, + }; + + for (const charge of charges) { + if (charge.label && charge.stateId === criminal.stateId) { + criminal.penalty.time += charge.time || 0; + criminal.penalty.fine += charge.fine || 0; + + criminal.charges.push({ + label: charge.label, + count: charge.count, + time: charge.time || 0, + fine: charge.fine || 0, + }); + } + } + } + + return criminals; + } + + static async deleteReport(reportId: number) { + return await oxmysql.prepare('DELETE FROM `ox_mdt_reports` WHERE `id` = ?', [reportId]); + } + + static async updateReportTitle(reportId: number, data: string) { + return await oxmysql.prepare('UPDATE `ox_mdt_reports` SET `title` = ? WHERE `id` = ?', [data, reportId]); + } + + static async updateReportContents(reportId: number, data: string) { + return await oxmysql.prepare('UPDATE `ox_mdt_reports` SET `description` = ? WHERE `id` = ?', [data, reportId]); + } + + static async addCriminal(reportId: number, criminalId: string) { + return await oxmysql.prepare('INSERT INTO `ox_mdt_reports_criminals` (`reportid`, `stateId`) VALUES (?, ?)', [ + reportId, + criminalId, + ]); + } + + static async removeCriminal(reportId: number, criminalId: string) { + return await oxmysql.prepare('DELETE FROM `ox_mdt_reports_criminals` WHERE `reportid` = ? AND `stateId` = ?', [ + reportId, + criminalId, + ]); + } + + static async saveCriminal(reportId: number, criminal: Criminal) { + const queries: [string, (string | number | boolean)[]][] = [ + ['DELETE FROM `ox_mdt_reports_charges` WHERE `reportid` = ? AND `stateId` = ?', [reportId, criminal.stateId]], + [ + 'UPDATE IGNORE `ox_mdt_reports_criminals` SET `warrantExpiry` = ?, `processed` = ?, `pleadedGuilty` = ? WHERE `reportid` = ? AND `stateId` = ?', + [ + criminal.issueWarrant ? criminal.warrantExpiry : null, + criminal.processed, + criminal.pleadedGuilty, + reportId, + criminal.stateId, + ], + ], + ]; + + criminal.charges.forEach((charge) => { + queries.push([ + 'INSERT INTO `ox_mdt_reports_charges` (`reportid`, `stateId`, `charge`, `count`, `time`, `fine`) VALUES (?, ?, ?, ?, ?, ?)', + [reportId, criminal.stateId, charge.label, charge.count, charge.time, charge.fine], + ]); + }); + + return await oxmysql.transaction(queries); + } + + static async createWarrant(reportId: number, stateId: string, expiry: string) { + const warrantExists = + (await oxmysql.prepare('SELECT COUNT(1) FROM `ox_mdt_warrants` WHERE `reportId` = ? AND `stateId` = ?', [ + reportId, + stateId, + ])) > 0; + + if (warrantExists) { + return await oxmysql.prepare( + 'UPDATE `ox_mdt_warrants` SET `expiresAt` = ? WHERE `reportId` = ? AND `stateId` = ?', + [expiry, reportId, stateId] + ); + } else { + return await oxmysql.prepare( + 'INSERT INTO `ox_mdt_warrants` (`reportid`, `stateid`, `expiresAt`) VALUES (?, ?, ?)', + [reportId, stateId, expiry] + ); + } + } + + static async removeWarrant(reportId: number, stateId: string) { + return await oxmysql.prepare('DELETE FROM `ox_mdt_warrants` WHERE `reportid` = ? AND `stateid` = ?', [ + reportId, + stateId, + ]); + } + + static async addEvidence(reportId: number, label: string, image: string) { + return await oxmysql.prepare( + 'INSERT INTO `ox_mdt_reports_evidence` (`reportid`, `label`, `image`) VALUES (?, ?, ?)', + [reportId, label, image] + ); + } + + static async removeEvidence(reportId: number, label: string, image: string) { + return await oxmysql.prepare( + 'DELETE FROM `ox_mdt_reports_evidence` WHERE `reportid` = ? AND `label` = ? AND `image` = ?', + [reportId, label, image] + ); + } + + static async selectProfiles(page: number, search: string) { + const offset = (page - 1) * 10; + const formattedSearch = search ? this.formatSearchValue(search) : null; + + const query = ` + SELECT + c.stateId, + c.firstName, + c.lastName, + DATE_FORMAT(c.dateofbirth, "%Y-%m-%d") AS dob, + p.image + FROM + characters c + LEFT JOIN ox_mdt_profiles p ON p.stateid = c.stateid + ${ + formattedSearch + ? ` + LEFT JOIN vehicles v ON v.owner = c.charId + WHERE + MATCH (c.stateId, c.firstName, c.lastName) AGAINST (? IN BOOLEAN MODE) + OR MATCH (v.plate) AGAINST (? IN BOOLEAN MODE) + ` + : '' + } + GROUP BY + c.charId + ORDER BY + c.stateId DESC + LIMIT 10 OFFSET ? + `; + + const params = formattedSearch ? [formattedSearch, formattedSearch, offset] : [offset]; + + const results = await oxmysql.rawExecute(query, params); + return results; + } + + static async selectCharacterProfile(stateId: string) { + const profile = ( + await this.query( + ` + SELECT + a.firstName, + a.lastName, + a.stateId, + a.charid, + DATE_FORMAT(a.dateofbirth, "%Y-%m-%d") AS dob, + a.phoneNumber, + b.image, + b.notes + FROM + \`characters\` a + LEFT JOIN + \`ox_mdt_profiles\` b + ON + b.stateid = a.stateid + WHERE + a.stateId = ?`, + [stateId] + ) + )[0]; + + if (!profile) return; + + const cards = ProfileCard.getAll(); + const profileCards: Record = {}; + + cards.forEach(async (cardData) => { + profileCards[cardData.id] = await cardData.getData(profile); + }); + + const relatedReports = await this.query( + ` + SELECT DISTINCT + id, + title, + author, + DATE_FORMAT(date, "%Y-%m-%d") as date + FROM ox_mdt_reports a + LEFT JOIN ox_mdt_reports_charges b ON b.reportid = a.id + + WHERE stateId = ?`, + [stateId] + ); + + return { + ...profile, + ...profileCards, + relatedReports, + }; + } + + static async updateProfilePicture(stateId: string, image: string) { + return await oxmysql.prepare( + ` + INSERT INTO ox_mdt_profiles (stateid, image, notes) + VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE image = ?`, + [stateId, image, null, image] + ); + } + + static async updateProfileNotes(stateId: string, notes: string) { + return await oxmysql.prepare( + ` + INSERT INTO ox_mdt_profiles (stateid, image, notes) + VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE notes = ?`, + [stateId, null, notes, notes] + ); + } + + static async addOfficer(reportId: number, stateId: string) { + return await oxmysql.prepare( + ` + INSERT INTO ox_mdt_reports_officers (reportid, stateId) + VALUES (?, ?)`, + [reportId, stateId] + ); + } + + static async removeOfficer(reportId: number, stateId: string) { + return await oxmysql.prepare( + ` + DELETE FROM ox_mdt_reports_officers + WHERE reportid = ? AND stateId = ?`, + [reportId, stateId] + ); + } + + static async getPastCharges(stateId: string): Promise<{ label: string; count: number }[]> { + return await this.query( + ` + SELECT + charge AS label, + SUM(count) AS count + FROM ox_mdt_reports_charges + WHERE charge IS NOT NULL AND stateId = ? + GROUP BY charge`, + [stateId] + ); + } + + static async searchOfficers(search: string) { + const formattedSearch = search ? this.formatSearchValue(search) : null; + const groupsFormatted = Config.policeGroups.join('","'); + + const query = ` + SELECT + p.id, + c.firstName, + c.lastName, + c.stateId, + cg.name AS \`group\`, + cg.grade, + p.image, + p.callSign + FROM + character_groups cg + LEFT JOIN + characters c ON cg.charId = c.charId + LEFT JOIN + ox_mdt_profiles p ON c.stateId = p.stateId + WHERE + cg.name IN ("${groupsFormatted}") + ${formattedSearch ? 'AND MATCH (c.stateId, c.firstName, c.lastName) AGAINST (? IN BOOLEAN MODE)' : ''} + ORDER BY + p.callSign ASC + ${formattedSearch ? '' : 'LIMIT 9'} + `; + + const params = formattedSearch ? [formattedSearch] : []; + + const results = await oxmysql.rawExecute(query, params); + return results || []; + } + + static async fetchRoster(page: number, search: string) { + const offset = (page - 1) * 9; + const isFilter = search && search.trim().length > 0; + const groupsFormatted = Config.policeGroups.join('","'); + + const baseQuery = ` + FROM + character_groups cg + LEFT JOIN + characters c ON cg.charId = c.charId + LEFT JOIN + ox_mdt_profiles p ON c.stateId = p.stateId + WHERE + cg.name IN ("${groupsFormatted}") + `; + + const selectColumns = ` + SELECT + p.id, c.firstName, c.lastName, c.stateId, + cg.name AS \`group\`, cg.grade, p.image, p.callSign + `; + + if (!isFilter) { + const [countResult, officers] = await Promise.all([ + oxmysql.scalar(`SELECT COUNT(*) ${baseQuery}`), + oxmysql.rawExecute(`${selectColumns} ${baseQuery} LIMIT 9 OFFSET ?`, [offset]), + ]); + + return { + totalRecords: countResult || 0, + officers: officers || [], + }; + } + + const filterClause = 'AND MATCH (c.stateId, c.firstName, c.lastName) AGAINST (? IN BOOLEAN MODE)'; + + const filteredOfficers = await oxmysql.rawExecute( + `${selectColumns} ${baseQuery} ${filterClause} LIMIT 9 OFFSET ?`, + [search, offset] + ); + + const totalFiltered = await oxmysql.scalar(`SELECT COUNT(*) ${baseQuery} ${filterClause}`, [search]); + + return { + totalRecords: totalFiltered || 0, + officers: filteredOfficers || [], + }; + } + + static async selectOfficerCallsign(callSign: string) { + return await oxmysql.prepare<{ callSign: string }>('SELECT `callSign` FROM `ox_mdt_profiles` WHERE callSign = ?', [ + callSign, + ]); + } + + static async updateOfficerCallsign(stateId: string, callSign: string) { + return await oxmysql.prepare( + ` + INSERT INTO ox_mdt_profiles (stateId, image, notes, callSign) + VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE callSign = ?`, + [stateId, null, null, callSign, callSign] + ); + } + + static async selectWarrants(search?: string): Promise { + const formattedSearch = search ? this.formatSearchValue(search) : null; + + const query = ` + SELECT + w.reportId, + c.stateId, + c.firstName, + c.lastName, + DATE_FORMAT(w.expiresAt, "%Y-%m-%d %T") AS expiresAt + FROM + \`ox_mdt_warrants\` w + LEFT JOIN + \`characters\` c ON w.stateid = c.stateid + ${formattedSearch ? 'WHERE MATCH (c.stateId, c.firstName, c.lastName) AGAINST (? IN BOOLEAN MODE)' : ''} + ORDER BY + w.expiresAt ASC + `; + + const params = formattedSearch ? [formattedSearch] : []; + + return await this.query(query, params); + } +} diff --git a/src/server/framework/events.ts b/src/server/framework/events.ts new file mode 100644 index 00000000..91518194 --- /dev/null +++ b/src/server/framework/events.ts @@ -0,0 +1,61 @@ +import { Config } from '@common/index'; +import { GetPlayer, GetPlayers } from '@communityox/ox_core/server'; +import { OfficerManager } from '../managers/officerManager'; + +const addOfficer = (playerId: number): void => { + const player = GetPlayer(playerId); + + if (!player) return; + + const group = player.get('activeGroup'); + if (!group) return; + + const grade = player.getGroup(group); + + if (!Config.policeGroups.includes(group)) return; + + OfficerManager.add(playerId, player.get('firstName'), player.get('lastName'), player.stateId, group, grade); +}; + +on('ox:playerLoaded', (playerId: number, userId: number, charId: number) => { + addOfficer(playerId); +}); + +on('ox:setActiveGroup', (playerId: number, groupName: string, previousGroupName: string | undefined) => { + const officer = OfficerManager.get(playerId); + + if (officer) { + const grade = GetPlayer(playerId).getGroup(officer.group); + + if (officer.group == groupName) { + if (!grade) { + OfficerManager.remove(playerId); + return; + } + + officer.grade = grade; + + return; + } + } + + addOfficer(playerId); +}); + +on('ox:playerLogout', (playerId: number, userId: number, charId: number) => { + const officer = OfficerManager.get(playerId); + + if (!officer) return; + + // ToDo: + // const state = Player(playerId).state; + // UnitManager.removePlayerFromUnit(officer, state); + + OfficerManager.remove(playerId); +}); + +setImmediate(() => { + GetPlayers().forEach((ply) => { + addOfficer(ply.source); + }); +}); diff --git a/src/server/framework/index.ts b/src/server/framework/index.ts new file mode 100644 index 00000000..0082865c --- /dev/null +++ b/src/server/framework/index.ts @@ -0,0 +1,24 @@ +import { Ox } from '@communityox/ox_core/server'; +import { Config, Permissions } from '@common/index'; + +export * from './authorizedCheck'; +export * from './db'; + +// initialize event listeners +import './events'; + +Config.policeGroups.forEach((group) => { + Ox.SetGroupPermission(group, 1, 'mdt.access', 'allow'); + + for (const [permission, required] of Object.entries(Permissions)) { + let grade: number | false = false; + + if (typeof required === 'number') { + grade = required; + } else { + grade = (required as Record)[group] ?? false; + } + + if (typeof grade === 'number') Ox.SetGroupPermission(group, grade, `mdt.${permission}`, 'allow'); + } +}); diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 00000000..e4b9bce1 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,23 @@ +import { versionCheck, checkDependency, cache } from '@communityox/ox_lib/server'; +import { OfficerManager } from './managers/officerManager'; +import './callbacks'; + +versionCheck('communityox/ox_mdt'); + +const coreDepCheck: true | [false, string] = checkDependency('ox_core', '1.1.0'); + +if (coreDepCheck !== true) { + setInterval(() => { + console.error(coreDepCheck[1]); + }, 1000); +} + +on('onResourceStop', (resource: string) => { + if (resource !== cache.resource) return; + + OfficerManager.getAll().forEach(({ unitId, playerId }) => { + if (unitId) { + Player(playerId).state.mdtUnitId = null; + } + }); +}); diff --git a/src/server/managers/callManager.ts b/src/server/managers/callManager.ts new file mode 100644 index 00000000..fcd05b05 --- /dev/null +++ b/src/server/managers/callManager.ts @@ -0,0 +1,121 @@ +import { Call, CallData, Calls } from '@common/typings'; +import { OfficerManager } from './officerManager'; +import { UnitManager } from './unitManager'; + +// ToDo: +// add interval to clear completed calls on an hourly basis + +export class CallManager { + private static callId: number = 0; + private static activeCalls: Calls = []; + private static completedCalls: Calls = []; + + public static createCall(data: CallData) { + const call: Call = { + id: this.callId, + code: data.code, + offense: data.offense, + completed: false, + units: [], + coords: data.coords, + blip: data.blip, + // ToDo ? - is it implemented + // isEmergency: data.isEmergency, + time: Date.now(), + location: '', + info: data.info, + }; + + this.activeCalls[this.callId] = call; + + OfficerManager.triggerEvent('ox_mdt:createCall', { + id: this.callId, + call, + }); + + this.callId++; + + return call.id; + } + + public static updateCallCoords(callId: number, coords: [number, number]) { + const call = this.activeCalls[callId]; + if (!call) return; + + call.coords = coords; + + OfficerManager.triggerEvent('ox_mdt:updateCallCoords', { + id: callId, + coords, + }); + } + + public static markCallComplete(callId: number) { + const call = this.activeCalls[callId]; + + if (!call) return false; + + call.completed = Date.now(); + this.completedCalls[callId] = call; + delete this.activeCalls[callId]; + + return true; + } + + public static setUnitsOnCall(callId: number, units: string[]) { + const call = this.activeCalls[callId]; + + if (!call) return; + + units.forEach((unitId) => { + const unit = UnitManager.getUnit(unitId); + if (unit) call.units.push(unit); + }); + + OfficerManager.triggerEvent('ox_mdt:setCallUnits', { + id: callId, + units: call.units, + }); + + return true; + } + + public static addUnitToCall(callId: number, unitId: string) { + const call = this.activeCalls[callId]; + + if (!call || call.units.some((u) => u.id === unitId)) return false; + + const unit = UnitManager.getUnit(unitId); + if (unit) call.units.push(unit); + + OfficerManager.triggerEvent('ox_mdt:editCallUnits', { + id: callId, + units: call.units, + }); + + return true; + } + + public static removeUnitFromCall(callId: number, unitId: string) { + const call = this.activeCalls[callId]; + + const unitIdx = call.units.findIndex((u) => u.id === unitId); + if (!call || unitIdx !== -1) return false; + + delete call.units[unitIdx]; + + OfficerManager.triggerEvent('ox_mdt:editCallUnits', { + id: callId, + units: call.units, + }); + + return true; + } + + public static getCalls(type: 'active' | 'completed') { + return type === 'completed' ? this.completedCalls : this.activeCalls; + } +} + +exports('createCall', (data: CallData) => CallManager.createCall(data)); +exports('updateCallCoords', (id: number, coords: [number, number]) => CallManager.updateCallCoords(id, coords)); diff --git a/src/server/managers/officerManager.ts b/src/server/managers/officerManager.ts new file mode 100644 index 00000000..abe6ad64 --- /dev/null +++ b/src/server/managers/officerManager.ts @@ -0,0 +1,76 @@ +import { oxmysql } from '@communityox/oxmysql'; +import { Officer } from '@common/typings'; + +export class OfficerManager { + private static activeUpdating = false; + private static activeOfficers: Map = new Map(); + private static refreshInterval: number = Math.max(500, GetConvarInt('mdt:positionRefreshInterval', 5000)); + + static startPositionUpdater() { + if (this.activeUpdating) return; + setInterval(() => { + if (this.activeOfficers.size === 0) return; + + const officersArray: Officer[] = []; + + this.activeOfficers.forEach((officer, playerId) => { + const ped = GetPlayerPed(playerId.toString()); + if (ped !== 0) { + const coords = GetEntityCoords(ped); + officer.position = [coords[0], coords[1]]; + officer.ped = ped; + } + officersArray.push(officer); + }); + + this.triggerEvent('ox_mdt:updateOfficerPositions', officersArray); + }, this.refreshInterval); + } + + public static async add( + playerId: number, + firstName: string, + lastName: string, + stateId: string, + group: string, + grade: number + ) { + const callSign = await oxmysql.prepare('SELECT `callSign` FROM `ox_mdt_profiles` WHERE stateId = ?', [ + stateId, + ]); + + const officer: Officer = { + firstName, + lastName, + stateId, + callSign, + playerId, + ped: GetPlayerPed(playerId.toString()), + position: [0, 0], + group, + grade, + }; + + this.activeOfficers.set(playerId, officer); + } + + public static remove(playerId: number) { + this.activeOfficers.delete(playerId); + } + + public static get(playerId: number): Officer | undefined { + return this.activeOfficers.get(playerId); + } + + public static getAll(): Map { + return this.activeOfficers; + } + + public static triggerEvent(eventName: string, eventData: any) { + for (const playerId of this.activeOfficers.keys()) { + TriggerClientEvent(eventName, playerId, eventData); + } + } +} + +setImmediate(() => OfficerManager.startPositionUpdater()); diff --git a/src/server/managers/unitManager.ts b/src/server/managers/unitManager.ts new file mode 100644 index 00000000..2fc7ac8a --- /dev/null +++ b/src/server/managers/unitManager.ts @@ -0,0 +1,116 @@ +import { Officer, Unit, Units, UnitType } from '@common/typings'; +import { OfficerManager } from './officerManager'; + +export class UnitManager { + private static units: Units = {}; + + public static createUnit(unitId: string, unitName: string, unitType: UnitType) { + const unitData: Unit = { + id: unitId, + name: unitName, + members: [], + type: unitType, + }; + + this.units[unitId] = unitData; + + return unitData; + } + + public static updateUnitType(unitId: string, unitType: UnitType): boolean { + const unit = this.units[unitId]; + + if (!unit) return false; + + unit.type = unitType; + + OfficerManager.triggerEvent('ox_mdt:refreshUnits', this.units); + + return true; + } + + public static getUnit(unitId: string) { + return this.units[unitId] ?? false; + } + + public static getUnits(): Units { + return this.units; + } + + public static removePlayerFromUnit(officer: Officer, state: StateBagInterface): boolean { + const unitId = state.mdtUnitId as number; + + if (!unitId) return; + + const unit = this.units[unitId]; + + // Unit gets deleted when the owner leaves it + if (unit.id === officer.callSign) { + unit.members.forEach((member) => { + delete member.unitId; + Player(member.playerId).state.mdtUnitId = null; + }); + + delete this.units[unitId]; + + OfficerManager.triggerEvent('ox_mdt:refreshUnits', this.units); + return true; + } + + const member = unit.members.findIndex((member) => member.stateId === officer.stateId); + + if (member === -1) return false; + + state.mdtUnitId = null; + + unit.members = unit.members.filter((member) => member.stateId !== officer.stateId); + + if (unit.members.length === 0) { + delete this.units[unitId]; + + // ToDo: detach unit from all calls + } + + OfficerManager.triggerEvent('ox_mdt:refreshUnits', this.units); + + return true; + } + + public static addPlayerToUnit(playerIds: number[], unitId: string): boolean; + public static addPlayerToUnit(playerId: number, unitId: string): boolean; + public static addPlayerToUnit(idInput: number | number[], unitId: string): boolean { + const unit = this.units[unitId]; + if (!unit) return false; + + const playerIds = Array.isArray(idInput) ? idInput : [idInput]; + let anySuccess = false; + + for (const playerId of playerIds) { + const officer = OfficerManager.get(playerId); + const state = Player(playerId.toString()).state; + + if (!officer) continue; + + if (state.mdtUnitId === unitId) { + anySuccess = true; + continue; + } + + if (state.mdtUnitId) { + this.removePlayerFromUnit(officer, state); + } + + unit.members.push(officer); + officer.unitId = unitId; + state.mdtUnitId = unitId; + + anySuccess = true; + } + + if (anySuccess) { + OfficerManager.triggerEvent('ox_mdt:refreshUnits', this.units); + } + + return anySuccess; + } +} diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json new file mode 100644 index 00000000..486b4298 --- /dev/null +++ b/src/server/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../common/tsconfig.json", + "compilerOptions": { + "types": ["@types/node", "@citizenfx/server"] + }, + "include": ["./"] +} diff --git a/src/server/utils/callback.ts b/src/server/utils/callback.ts new file mode 100644 index 00000000..c38a2aa2 --- /dev/null +++ b/src/server/utils/callback.ts @@ -0,0 +1,18 @@ +import { onClientCallback } from '@communityox/ox_lib/server'; +import { isAuthorised } from '../framework/authorizedCheck'; +import { PermissionKeys } from '@common/index'; + +export const registerAuthorisedCallback = ( + event: string, + cb: (source: number, ...args: any[]) => any, + permission: PermissionKeys | 'access' | false = 'access' +) => { + onClientCallback(event, (playerId, ...args) => { + if (typeof permission === 'string' && !isAuthorised(source, permission)) { + DEV: console.info(`User (${source}) tried accessing callback without required permission (${permission}).`); + return false; + } + + return cb(playerId, ...args); + }); +}; diff --git a/src/server/utils/profileCards.ts b/src/server/utils/profileCards.ts new file mode 100644 index 00000000..169cc70f --- /dev/null +++ b/src/server/utils/profileCards.ts @@ -0,0 +1,62 @@ +import { ProfileCardData } from '@common/typings'; +import { locale } from '@communityox/ox_lib'; +import { DB } from '../framework'; + +export class ProfileCard { + private static customProfileCards: ProfileCardData[] = []; + + static checkCardExists(card: ProfileCardData) { + const exists = this.customProfileCards.some((refCard) => refCard.id === card.id); + if (exists) { + // Print an error because you're not supposed to try and overwrite a card + console.error(`Custom Card with id "${card.id}" already exists!`); + } + return exists; + } + + static createCustomCard(data: ProfileCardData | ProfileCardData[]) { + if (!Array.isArray(data)) data = [data]; + + for (const card of data) { + if (!this.checkCardExists(card)) { + this.customProfileCards.push(card); + } + } + } + + static getAll() { + return this.customProfileCards; + } +} + +exports('createProfileCard', ProfileCard.createCustomCard); + +ProfileCard.createCustomCard([ + { + id: 'licenses', + title: locale('licenses'), + icon: 'certificate', + getData: async (profile) => { + const licenses = await DB.getLicenses([profile.charid]); + return licenses.map((lic) => (typeof lic === 'string' ? lic : lic.label)); + }, + }, + { + id: 'vehicles', + title: locale('vehicles'), + icon: 'car', + getData: async (profile) => { + const vehicles = await DB.getVehicles([profile.charid]); + return vehicles.map((v) => `${v.label} (${v.plate})`); + }, + }, + { + id: 'pastCharges', + title: locale('past_charges'), + icon: 'gavel', + getData: async (profile) => { + const charges = await DB.getPastCharges(profile.stateId); + return charges.map((c) => `${c.count}x ${c.label}`); + }, + }, +]); diff --git a/web/.gitignore b/src/web/.gitignore similarity index 83% rename from web/.gitignore rename to src/web/.gitignore index a547bf36..2934fe00 100644 --- a/web/.gitignore +++ b/src/web/.gitignore @@ -1,5 +1,6 @@ # Logs logs +!/src/layouts/bank/pages/accounts/logs/ *.log npm-debug.log* yarn-debug.log* @@ -8,8 +9,7 @@ pnpm-debug.log* lerna-debug.log* node_modules -dist -dist-ssr +build *.local # Editor directories and files diff --git a/src/web/index.html b/src/web/index.html new file mode 100644 index 00000000..591dfffd --- /dev/null +++ b/src/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + Ox MDT + + +
+ + + diff --git a/src/web/nui.d.ts b/src/web/nui.d.ts new file mode 100644 index 00000000..8227c835 --- /dev/null +++ b/src/web/nui.d.ts @@ -0,0 +1,4 @@ +interface Window { + invokeNative: (native: string, arg: string) => void; + GetParentResourceName: () => string; +} diff --git a/web/public/blips/111.png b/src/web/public/blips/111.png similarity index 100% rename from web/public/blips/111.png rename to src/web/public/blips/111.png diff --git a/web/public/blips/162.png b/src/web/public/blips/162.png similarity index 100% rename from web/public/blips/162.png rename to src/web/public/blips/162.png diff --git a/web/public/blips/310.png b/src/web/public/blips/310.png similarity index 100% rename from web/public/blips/310.png rename to src/web/public/blips/310.png diff --git a/web/public/blips/51.png b/src/web/public/blips/51.png similarity index 100% rename from web/public/blips/51.png rename to src/web/public/blips/51.png diff --git a/web/public/blips/60.png b/src/web/public/blips/60.png similarity index 100% rename from web/public/blips/60.png rename to src/web/public/blips/60.png diff --git a/web/public/blips/67.png b/src/web/public/blips/67.png similarity index 100% rename from web/public/blips/67.png rename to src/web/public/blips/67.png diff --git a/web/public/map.jpeg b/src/web/public/map.jpeg similarity index 100% rename from web/public/map.jpeg rename to src/web/public/map.jpeg diff --git a/web/src/App.tsx b/src/web/src/App.tsx similarity index 100% rename from web/src/App.tsx rename to src/web/src/App.tsx diff --git a/web/src/helpers/convert.ts b/src/web/src/helpers/convert.ts similarity index 100% rename from web/src/helpers/convert.ts rename to src/web/src/helpers/convert.ts diff --git a/web/src/helpers/formatNumber.ts b/src/web/src/helpers/formatNumber.ts similarity index 100% rename from web/src/helpers/formatNumber.ts rename to src/web/src/helpers/formatNumber.ts diff --git a/src/web/src/helpers/hasPermission.ts b/src/web/src/helpers/hasPermission.ts new file mode 100644 index 00000000..c128b505 --- /dev/null +++ b/src/web/src/helpers/hasPermission.ts @@ -0,0 +1,17 @@ +import type { Character } from '../typings'; + +type PermissionKey = keyof Character['permissions']; + +export const hasPermission = (character: Character, permission: PermissionKey | PermissionKey[]) => { + if (!Array.isArray(permission)) { + return character.permissions[permission]; + } + + let failedPerms = 0; + + for (let i = 0; i < permission.length; i++) { + if (!character.permissions[permission[i]]) failedPerms++; + } + + return failedPerms !== permission.length; +}; diff --git a/web/src/helpers/index.ts b/src/web/src/helpers/index.ts similarity index 100% rename from web/src/helpers/index.ts rename to src/web/src/helpers/index.ts diff --git a/web/src/helpers/removePages.ts b/src/web/src/helpers/removePages.ts similarity index 63% rename from web/src/helpers/removePages.ts rename to src/web/src/helpers/removePages.ts index 08bfc1f9..a4f43707 100644 --- a/web/src/helpers/removePages.ts +++ b/src/web/src/helpers/removePages.ts @@ -2,7 +2,9 @@ import { queryClient } from '../main'; export function removePages(queryKey: string[]) { queryClient.setQueriesData<{ pages: unknown[][]; pageParams: number[] }>(queryKey, (data) => { - if (!data) return; + // for some reason this is receiving player data data, + // not sure where from at the writing of this comment + if (!data || !data.pages || !data.pageParams) return; return { pages: data.pages.slice(0, 1), diff --git a/web/src/hooks/useInfiniteScroll.ts b/src/web/src/hooks/useInfiniteScroll.ts similarity index 100% rename from web/src/hooks/useInfiniteScroll.ts rename to src/web/src/hooks/useInfiniteScroll.ts diff --git a/web/src/hooks/useNuiEvent.ts b/src/web/src/hooks/useNuiEvent.ts similarity index 100% rename from web/src/hooks/useNuiEvent.ts rename to src/web/src/hooks/useNuiEvent.ts diff --git a/web/src/index.css b/src/web/src/index.css similarity index 100% rename from web/src/index.css rename to src/web/src/index.css diff --git a/web/src/layers/dev/Dev.tsx b/src/web/src/layers/dev/Dev.tsx similarity index 100% rename from web/src/layers/dev/Dev.tsx rename to src/web/src/layers/dev/Dev.tsx diff --git a/web/src/layers/mdt/MDT.tsx b/src/web/src/layers/mdt/MDT.tsx similarity index 100% rename from web/src/layers/mdt/MDT.tsx rename to src/web/src/layers/mdt/MDT.tsx diff --git a/web/src/layers/mdt/components/BadgeButton.tsx b/src/web/src/layers/mdt/components/BadgeButton.tsx similarity index 100% rename from web/src/layers/mdt/components/BadgeButton.tsx rename to src/web/src/layers/mdt/components/BadgeButton.tsx diff --git a/web/src/layers/mdt/components/CardTitle.tsx b/src/web/src/layers/mdt/components/CardTitle.tsx similarity index 100% rename from web/src/layers/mdt/components/CardTitle.tsx rename to src/web/src/layers/mdt/components/CardTitle.tsx diff --git a/web/src/layers/mdt/components/Editor.tsx b/src/web/src/layers/mdt/components/Editor.tsx similarity index 100% rename from web/src/layers/mdt/components/Editor.tsx rename to src/web/src/layers/mdt/components/Editor.tsx diff --git a/web/src/layers/mdt/components/ListContainer.tsx b/src/web/src/layers/mdt/components/ListContainer.tsx similarity index 100% rename from web/src/layers/mdt/components/ListContainer.tsx rename to src/web/src/layers/mdt/components/ListContainer.tsx diff --git a/web/src/layers/mdt/components/ListSearch.tsx b/src/web/src/layers/mdt/components/ListSearch.tsx similarity index 100% rename from web/src/layers/mdt/components/ListSearch.tsx rename to src/web/src/layers/mdt/components/ListSearch.tsx diff --git a/web/src/layers/mdt/components/LoaderModal.tsx b/src/web/src/layers/mdt/components/LoaderModal.tsx similarity index 100% rename from web/src/layers/mdt/components/LoaderModal.tsx rename to src/web/src/layers/mdt/components/LoaderModal.tsx diff --git a/web/src/layers/mdt/components/NavButton.tsx b/src/web/src/layers/mdt/components/NavButton.tsx similarity index 100% rename from web/src/layers/mdt/components/NavButton.tsx rename to src/web/src/layers/mdt/components/NavButton.tsx diff --git a/web/src/layers/mdt/components/NavCharacter.tsx b/src/web/src/layers/mdt/components/NavCharacter.tsx similarity index 100% rename from web/src/layers/mdt/components/NavCharacter.tsx rename to src/web/src/layers/mdt/components/NavCharacter.tsx diff --git a/web/src/layers/mdt/components/Navbar.tsx b/src/web/src/layers/mdt/components/Navbar.tsx similarity index 100% rename from web/src/layers/mdt/components/Navbar.tsx rename to src/web/src/layers/mdt/components/Navbar.tsx diff --git a/web/src/layers/mdt/components/NotFound.tsx b/src/web/src/layers/mdt/components/NotFound.tsx similarity index 100% rename from web/src/layers/mdt/components/NotFound.tsx rename to src/web/src/layers/mdt/components/NotFound.tsx diff --git a/web/src/layers/mdt/components/ReadOnlyEditor.tsx b/src/web/src/layers/mdt/components/ReadOnlyEditor.tsx similarity index 100% rename from web/src/layers/mdt/components/ReadOnlyEditor.tsx rename to src/web/src/layers/mdt/components/ReadOnlyEditor.tsx diff --git a/web/src/layers/mdt/components/SuspenseLoader.tsx b/src/web/src/layers/mdt/components/SuspenseLoader.tsx similarity index 100% rename from web/src/layers/mdt/components/SuspenseLoader.tsx rename to src/web/src/layers/mdt/components/SuspenseLoader.tsx diff --git a/web/src/layers/mdt/components/UnitBadge.tsx b/src/web/src/layers/mdt/components/UnitBadge.tsx similarity index 100% rename from web/src/layers/mdt/components/UnitBadge.tsx rename to src/web/src/layers/mdt/components/UnitBadge.tsx diff --git a/web/src/layers/mdt/pages/charges/Charges.tsx b/src/web/src/layers/mdt/pages/charges/Charges.tsx similarity index 100% rename from web/src/layers/mdt/pages/charges/Charges.tsx rename to src/web/src/layers/mdt/pages/charges/Charges.tsx diff --git a/web/src/layers/mdt/pages/dashboard/Dashboard.tsx b/src/web/src/layers/mdt/pages/dashboard/Dashboard.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/Dashboard.tsx rename to src/web/src/layers/mdt/pages/dashboard/Dashboard.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementCard.tsx b/src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementCard.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementCard.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementCard.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementList.tsx b/src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementList.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementList.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementList.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementModal.tsx b/src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementModal.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementModal.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementModal.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementsContainer.tsx b/src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementsContainer.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementsContainer.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/announcements/AnnouncementsContainer.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/BolosContainer.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/BolosContainer.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/BolosContainer.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/BolosContainer.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/components/AddImagePopover.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/components/AddImagePopover.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/components/AddImagePopover.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/components/AddImagePopover.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloCard.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloCard.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloCard.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloCard.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImage.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImage.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImage.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImage.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImages.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImages.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImages.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloImages.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloList.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloList.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloList.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/components/BoloList.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/components/ConfirmBoloButton.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/components/ConfirmBoloButton.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/components/ConfirmBoloButton.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/components/ConfirmBoloButton.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/bolos/modals/CreateBoloModal.tsx b/src/web/src/layers/mdt/pages/dashboard/components/bolos/modals/CreateBoloModal.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/bolos/modals/CreateBoloModal.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/bolos/modals/CreateBoloModal.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantCard.tsx b/src/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantCard.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/warrants/WarrantCard.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantCard.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantList.tsx b/src/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantList.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/warrants/WarrantList.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantList.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/warrants/Warrants.tsx b/src/web/src/layers/mdt/pages/dashboard/components/warrants/Warrants.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/warrants/Warrants.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/warrants/Warrants.tsx diff --git a/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantsContainer.tsx b/src/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantsContainer.tsx similarity index 100% rename from web/src/layers/mdt/pages/dashboard/components/warrants/WarrantsContainer.tsx rename to src/web/src/layers/mdt/pages/dashboard/components/warrants/WarrantsContainer.tsx diff --git a/web/src/layers/mdt/pages/dispatch/Dispatch.tsx b/src/web/src/layers/mdt/pages/dispatch/Dispatch.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/Dispatch.tsx rename to src/web/src/layers/mdt/pages/dispatch/Dispatch.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/CallMarkers.tsx b/src/web/src/layers/mdt/pages/dispatch/components/CallMarkers.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/CallMarkers.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/CallMarkers.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/Map.tsx b/src/web/src/layers/mdt/pages/dispatch/components/Map.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/Map.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/Map.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/MapWrapper.tsx b/src/web/src/layers/mdt/pages/dispatch/components/MapWrapper.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/MapWrapper.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/MapWrapper.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/MarkerPopup.tsx b/src/web/src/layers/mdt/pages/dispatch/components/MarkerPopup.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/MarkerPopup.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/MarkerPopup.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/OfficerMarkers.tsx b/src/web/src/layers/mdt/pages/dispatch/components/OfficerMarkers.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/OfficerMarkers.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/OfficerMarkers.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/calls/CallActionMenu.tsx b/src/web/src/layers/mdt/pages/dispatch/components/calls/CallActionMenu.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/calls/CallActionMenu.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/calls/CallActionMenu.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/calls/CallCard.tsx b/src/web/src/layers/mdt/pages/dispatch/components/calls/CallCard.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/calls/CallCard.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/calls/CallCard.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/calls/CallTypeSwitcher.tsx b/src/web/src/layers/mdt/pages/dispatch/components/calls/CallTypeSwitcher.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/calls/CallTypeSwitcher.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/calls/CallTypeSwitcher.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/calls/CallsContainer.tsx b/src/web/src/layers/mdt/pages/dispatch/components/calls/CallsContainer.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/calls/CallsContainer.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/calls/CallsContainer.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/calls/CallsList.tsx b/src/web/src/layers/mdt/pages/dispatch/components/calls/CallsList.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/calls/CallsList.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/calls/CallsList.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/modals/CreateUnitModal.tsx b/src/web/src/layers/mdt/pages/dispatch/components/modals/CreateUnitModal.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/modals/CreateUnitModal.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/modals/CreateUnitModal.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/modals/ManageOfficersModal.tsx b/src/web/src/layers/mdt/pages/dispatch/components/modals/ManageOfficersModal.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/modals/ManageOfficersModal.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/modals/ManageOfficersModal.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/modals/ManageUnitsModal.tsx b/src/web/src/layers/mdt/pages/dispatch/components/modals/ManageUnitsModal.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/modals/ManageUnitsModal.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/modals/ManageUnitsModal.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/units/ChangeUnitTypeModal.tsx b/src/web/src/layers/mdt/pages/dispatch/components/units/ChangeUnitTypeModal.tsx similarity index 100% rename from web/src/layers/mdt/pages/dispatch/components/units/ChangeUnitTypeModal.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/units/ChangeUnitTypeModal.tsx diff --git a/web/src/layers/mdt/pages/dispatch/components/units/CreateUnitButton.tsx b/src/web/src/layers/mdt/pages/dispatch/components/units/CreateUnitButton.tsx similarity index 89% rename from web/src/layers/mdt/pages/dispatch/components/units/CreateUnitButton.tsx rename to src/web/src/layers/mdt/pages/dispatch/components/units/CreateUnitButton.tsx index fd83bcd4..3e1d3966 100644 --- a/web/src/layers/mdt/pages/dispatch/components/units/CreateUnitButton.tsx +++ b/src/web/src/layers/mdt/pages/dispatch/components/units/CreateUnitButton.tsx @@ -14,7 +14,7 @@ const CreateUnitButton: React.FC = () => {