MIF_E31221222/sigap-website/src/utils/common.ts

1037 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { format } from "date-fns"
import { redirect } from "next/navigation"
import { toast } from "sonner"
import { v4 as uuidv4 } from "uuid"
import * as crypto from "crypto"
// import { districtsGeoJson } from "../../prisma/data/geojson/jember/districts-geojson"
import { CRegex } from "../constants/regex"
// import { getNavData } from "../../prisma/data/jsons/nav"
import {
DateFormatOptions,
DateFormatPattern,
} from "../types/raws/date-format.interface"
import { customAlphabet } from "nanoid"
import { IUserModel } from "@/types/models"
const prefixes: Record<string, unknown> = {}
interface IGenerateFilterIdOptions {
length?: number
separator?: string
}
// Used to track generated IDs
const usedIdRegistry = new Set<string>()
// Add type definitions for global counters
declare global {
var __idCounter: number
var __idCounterRegistry: Record<string, number>
}
/**
* Redirects to a specified path with an encoded message as a query parameter.
* @param {('error' | 'success')} type - The type of message, either 'error' or 'success'.
* @param {string} path - The path to redirect to.
* @param {string} message - The message to be encoded and added as a query parameter.
* @returns {never} This function doesn't return as it triggers a redirect.
*/
export function encodedRedirect(
type: "error" | "success",
path: string,
message: string
) {
return redirect(`${path}?${type}=${encodeURIComponent(message)}`)
}
/**
* Formats a URL by removing any trailing slashes.
* @param {string} url - The URL to format.
* @returns {string} The formatted URL.
*/
// Helper function to ensure URLs are properly formatted
export function formatUrl(url: string): string {
// If URL starts with a slash, it's already absolute
if (url.startsWith("/")) {
return url
}
// Otherwise, ensure it's properly formatted relative to root
// Remove any potential duplicated '/dashboard' prefixes
if (url.startsWith("dashboard/")) {
return "/" + url
}
return "/" + url
}
/**
* Creates a FormData object from the FormData object.
* @returns {FormData} The FormData object.
*/
export function createFormData(): FormData {
const data = new FormData()
Object.entries(FormData).forEach(([key, value]) => {
if (value) {
data.append(key, value)
}
})
return data
}
/**
* Generates a unique username based on the provided email address.
*
* The username is created by combining the local part of the email (before the '@' symbol)
* with a randomly generated alphanumeric suffix.
*
* @param email - The email address to generate the username from.
* @returns A string representing the generated username.
*
* @example
* ```typescript
* const username = generateUsername("example@gmail.com");
* console.log(username); // Output: "example.abc123" (random suffix will vary)
* ```
*/
export function generateUsername(email: string): string {
const [localPart] = email.split("@")
const randomSuffix = Math.random().toString(36).substring(2, 8) // Generate a random alphanumeric string
return `${localPart}.${randomSuffix}`
}
/**
* Formats a date string to a human-readable format with type safety.
* @param date - The date string to format.
* @param options - Formatting options or a format string.
* @returns The formatted date string.
* @example
* // Using default format
* formatDate("2025-03-23")
*
* // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd")
*
* // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/
export const formatDate = (
date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
): string => {
if (!date) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
const dateObj = date instanceof Date ? date : new Date(date)
// Handle invalid dates
if (isNaN(dateObj.getTime())) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
if (typeof options === "string") {
return format(dateObj, options)
}
const { format: formatPattern = "PPpp", locale } = options
return locale
? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern)
}
export const copyItem = (
item: string,
options?: {
label?: string
onSuccess?: () => void
onError?: (error: unknown) => void
}
) => {
if (!navigator.clipboard) {
const error = new Error("Clipboard not supported")
toast.error("Clipboard not supported")
options?.onError?.(error)
return
}
if (!item) {
const error = new Error("Nothing to copy")
toast.error("Nothing to copy")
options?.onError?.(error)
return
}
navigator.clipboard
.writeText(item)
.then(() => {
const label = options?.label || item
toast.success(`${label} copied to clipboard`)
options?.onSuccess?.()
})
.catch((error) => {
toast.error("Failed to copy to clipboard")
options?.onError?.(error)
})
}
/**
* Formats a date string to a human-readable format with type safety.
* @param date - The date string to format.
* @param options - Formatting options or a format string.
* @returns The formatted date string.
* @example
* // Using default format
* formatDate("2025-03-23")
*
* // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd")
*
* // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/
export const formatDateWithFallback = (
date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
): string => {
if (!date) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
const dateObj = date instanceof Date ? date : new Date(date)
// Handle invalid dates
if (isNaN(dateObj.getTime())) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
if (typeof options === "string") {
return format(dateObj, options)
}
const { format: formatPattern = "PPpp", locale } = options
return locale
? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern)
}
export const formatDateWithLocale = (
date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
): string => {
if (!date) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
const dateObj = date instanceof Date ? date : new Date(date)
// Handle invalid dates
if (isNaN(dateObj.getTime())) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
if (typeof options === "string") {
return format(dateObj, options)
}
const { format: formatPattern = "PPpp", locale } = options
return locale
? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern)
}
/**
* Formats a date string to a human-readable format with type safety.
* @param date - The date string to format.
* @param options - Formatting options or a format string.
* @returns The formatted date string.
* @example
* // Using default format
* formatDate("2025-03-23")
*
* // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd")
*
* // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/
export const formatDateWithLocaleAndFallback = (
date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
): string => {
if (!date) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
const dateObj = date instanceof Date ? date : new Date(date)
// Handle invalid dates
if (isNaN(dateObj.getTime())) {
return typeof options === "string" ? "-" : options.fallback || "-"
}
if (typeof options === "string") {
return format(dateObj, options)
}
const { format: formatPattern = "PPpp", locale } = options
return locale
? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern)
}
/**
* Generates a full name from first and last names.
* @param firstName - The first name.
* @param lastName - The last name.
* @returns The full name or "User" if both names are empty.
*/
export const getFullName = (
firstName: string | null | undefined,
lastName: string | null | undefined
): string => {
return `${firstName || ""} ${lastName || ""}`.trim() || "User"
}
/**
* Generates initials for a user based on their first and last names.
* @param firstName - The first name.
* @param lastName - The last name.
* @param email - The email address.
* @returns The initials or "U" if both names are empty.
*/
export const getInitials = (
firstName: string,
lastName: string,
email: string
): string => {
if (firstName && lastName) {
return `${firstName[0]}${lastName[0]}`.toUpperCase()
}
if (firstName) {
return firstName[0].toUpperCase()
}
if (email) {
return email[0].toUpperCase()
}
return "U"
}
export function calculateUserStats(users: IUserModel[] | undefined) {
if (!users || !Array.isArray(users)) {
return {
totalUsers: 0,
activeUsers: 0,
inactiveUsers: 0,
activePercentage: "0.0",
inactivePercentage: "0.0",
}
}
const totalUsers = users.length
const activeUsers = users.filter(
(user) => !user.banned_until && user.email_confirmed_at
).length
const inactiveUsers = totalUsers - activeUsers
return {
totalUsers,
activeUsers,
inactiveUsers,
activePercentage:
totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0",
inactivePercentage:
totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0",
}
}
/**
* Generate route with query parameters
* @param baseRoute - The base route path
* @param params - Object containing query parameters
* @returns Formatted route with query parameters
*/
export const createRoute = (
baseRoute: string,
params?: Record<string, string>
): string => {
if (!params || Object.keys(params).length === 0) {
return baseRoute
}
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&")
return `${baseRoute}?${queryString}`
}
// Format date helper function
function formatDateV2(date: Date, formatStr: string): string {
const pad = (num: number) => String(num).padStart(2, "0")
return formatStr
.replace("yyyy", String(date.getFullYear()))
.replace("MM", pad(date.getMonth() + 1))
.replace("dd", pad(date.getDate()))
.replace("HH", pad(date.getHours()))
.replace("mm", pad(date.getMinutes()))
.replace("ss", pad(date.getSeconds()))
}
// /**
// * Universal Custom ID Generator
// * Creates structured, readable IDs for any system or entity
// *
// * @param {Object} options - Configuration options
// * @param {string} options.prefix - Primary identifier prefix (e.g., "CRIME", "USER", "INVOICE")
// * @param {Object} options.segments - Collection of ID segments to include
// * @param {string[]} options.segments.codes - Array of short codes (e.g., region codes, department codes)
// * @param {number} options.segments.year - Year to include in the ID
// * @param {number} options.segments.sequentialDigits - Number of digits for sequential number
// * @param {boolean} options.segments.includeDate - Whether to include current date
// * @param {string} options.segments.dateFormat - Format for date (e.g., "yyyy-MM-dd", "dd/MM/yyyy")
// * @param {boolean} options.segments.includeTime - Whether to include timestamp
// * @param {string} options.format - Custom format string for ID structure
// * @param {string} options.separator - Character to separate ID components
// * @param {boolean} options.upperCase - Convert result to uppercase
// * @returns {string} - Generated custom ID
// */
// export function generateId(
// options: {
// prefix?: string;
// segments?: {
// codes?: string[];
// year?: number | boolean; // Year diubah menjadi number | boolean
// sequentialDigits?: number;
// includeDate?: boolean;
// dateFormat?: string;
// includeTime?: boolean;
// includeMilliseconds?: boolean;
// };
// format?: string | null;
// separator?: string;
// upperCase?: boolean;
// randomSequence?: boolean;
// uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash';
// retryOnCollision?: boolean;
// maxRetries?: number;
// } = {}
// ): string {
// // Jika uniquenessStrategy tidak diatur dan randomSequence = false,
// // gunakan counter sebagai strategi default
// if (!options.uniquenessStrategy && options.randomSequence === false) {
// options.uniquenessStrategy = 'counter';
// }
// const config = {
// prefix: options.prefix || 'ID',
// segments: {
// codes: options.segments?.codes || [],
// year: options.segments?.year, // Akan diproses secara kondisional nanti
// sequentialDigits: options.segments?.sequentialDigits || 6,
// includeDate: options.segments?.includeDate ?? false,
// dateFormat: options.segments?.dateFormat || 'yyyyMMdd',
// includeTime: options.segments?.includeTime ?? false,
// includeMilliseconds: options.segments?.includeMilliseconds ?? false,
// },
// format: options.format || null,
// separator: options.separator || '-',
// upperCase: options.upperCase ?? false,
// randomSequence: options.randomSequence ?? true,
// uniquenessStrategy: options.uniquenessStrategy || 'timestamp',
// retryOnCollision: options.retryOnCollision ?? true,
// maxRetries: options.maxRetries || 10,
// };
// // Initialize global counter if not exists
// if (typeof globalThis.__idCounter === 'undefined') {
// globalThis.__idCounter = 0;
// }
// const now = new Date();
// // Generate date string if needed
// let dateString = '';
// if (config.segments.includeDate) {
// dateString = format(now, config.segments.dateFormat);
// }
// // Generate time string if needed
// let timeString = '';
// if (config.segments.includeTime) {
// timeString = format(now, 'HHmmss');
// if (config.segments.includeMilliseconds) {
// timeString += now.getMilliseconds().toString().padStart(3, '0');
// }
// }
// // Generate sequential number based on uniqueness strategy
// let sequentialNum: string;
// try {
// switch (config.uniquenessStrategy) {
// case 'uuid':
// sequentialNum = uuidv4().split('-')[0];
// break;
// case 'timestamp':
// sequentialNum = `${now.getTime()}${Math.floor(Math.random() * 1000)}`;
// sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
// break;
// case 'counter':
// sequentialNum = (++globalThis.__idCounter)
// .toString()
// .padStart(config.segments.sequentialDigits, '0');
// break;
// case 'hash':
// const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
// const hash = crypto
// .createHash('sha256')
// .update(hashSource)
// .digest('hex');
// sequentialNum = hash.substring(0, config.segments.sequentialDigits);
// break;
// default:
// if (config.randomSequence) {
// const randomBytes = crypto.randomBytes(4);
// const randomNum = parseInt(randomBytes.toString('hex'), 16);
// sequentialNum = (
// randomNum % Math.pow(10, config.segments.sequentialDigits)
// )
// .toString()
// .padStart(config.segments.sequentialDigits, '0');
// } else {
// sequentialNum = (++globalThis.__idCounter)
// .toString()
// .padStart(config.segments.sequentialDigits, '0');
// }
// }
// } catch (error) {
// console.error('Error generating sequential number:', error);
// // Fallback to timestamp strategy if other methods fail
// sequentialNum = `${now.getTime()}`.slice(-config.segments.sequentialDigits);
// }
// // Determine if year should be included and what value to use
// let yearValue = null;
// if (config.segments.year !== undefined || config.segments.year != false) {
// if (typeof config.segments.year === 'number') {
// yearValue = String(config.segments.year);
// } else if (config.segments.year === true) {
// yearValue = format(now, 'yyyy');
// }
// // if year is false, yearValue remains null and won't be included
// } else {
// // Default behavior (backward compatibility)
// yearValue = format(now, 'yyyy');
// }
// // Prepare components for ID assembly
// const components = {
// prefix: config.prefix,
// codes:
// config.segments.codes.length > 0
// ? config.segments.codes.join(config.separator)
// : '',
// year: yearValue, // Added the year value to components
// sequence: sequentialNum,
// date: dateString,
// time: timeString,
// };
// let result: string;
// // Use custom format if provided
// if (config.format) {
// let customID = config.format;
// for (const [key, value] of Object.entries(components)) {
// if (value) {
// const placeholder = `{${key}}`;
// customID = customID.replace(
// new RegExp(placeholder, 'g'),
// String(value)
// );
// }
// }
// // Remove unused placeholders
// customID = customID.replace(/{[^}]+}/g, '');
// // Clean up separators
// const escapedSeparator = config.separator.replace(
// /[-\/\\^$*+?.()|[\]{}]/g,
// '\\$&'
// );
// const separatorRegex = new RegExp(`${escapedSeparator}+`, 'g');
// customID = customID.replace(separatorRegex, config.separator);
// customID = customID.replace(
// new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'),
// ''
// );
// result = config.upperCase ? customID.toUpperCase() : customID;
// } else {
// // Assemble ID from parts
// const parts = [];
// if (components.prefix) parts.push(components.prefix);
// if (components.codes) parts.push(components.codes);
// if (components.year) parts.push(components.year);
// if (components.date) parts.push(components.date);
// if (components.time) parts.push(components.time);
// if (components.sequence) parts.push(components.sequence);
// result = parts.join(config.separator);
// if (config.upperCase) result = result.toUpperCase();
// }
// // Handle collisions if required
// if (config.retryOnCollision) {
// let retryCount = 0;
// let originalResult = result;
// while (usedIdRegistry.has(result) && retryCount < config.maxRetries) {
// retryCount++;
// try {
// const suffix = crypto.randomBytes(2).toString('hex');
// result = `${originalResult}${config.separator}${suffix}`;
// } catch (error) {
// console.error('Error generating collision suffix:', error);
// // Simple fallback if crypto fails
// result = `${originalResult}${config.separator}${Date.now().toString(36)}`;
// }
// }
// if (retryCount >= config.maxRetries) {
// console.warn(
// `Warning: Max ID generation retries (${config.maxRetries}) reached for prefix ${config.prefix}`
// );
// }
// }
// // Register the ID and maintain registry size
// usedIdRegistry.add(result);
// if (usedIdRegistry.size > 10000) {
// const entriesToKeep = Array.from(usedIdRegistry).slice(-5000);
// usedIdRegistry.clear();
// entriesToKeep.forEach((id) => usedIdRegistry.add(id));
// }
// return result.trim();
// }
/**
* Generates a unique code based on the provided name.
* @param name - The name to generate the code from.
* @returns The generated code.
*/
export function generateCode(name: string): string {
const words = name.toUpperCase().split(" ")
let code = ""
if (name.length <= 3) {
code = name.toUpperCase()
} else if (words.length > 1) {
code = words
.map((w) => w[0])
.join("")
.padEnd(3, "X")
.slice(0, 3)
} else {
const nameClean = name.replace(/[aeiou]/gi, "")
code = (nameClean.slice(0, 3) || name.slice(0, 3)).toUpperCase()
}
return code
}
/**
* Generates a unique sequential ID based on a base string and existing codes.
* @param base - The base string to generate the ID from.
* @param existingCodes - A set of existing codes to check against.
* @returns The generated unique sequential ID.
*/
export function getLatestSequentialId(
base: string,
existingCodes: Set<string>
): string {
let attempt = 1
let newCode = base
while (existingCodes.has(newCode)) {
newCode = base.slice(0, 2) + attempt
attempt++
}
return newCode
}
/**
* Get color and text for a crime rate level
*/
export function getCrimeRateInfo(
rate?: "low" | "medium" | "high" | "critical"
) {
switch (rate) {
case "low":
return { color: "bg-green-100 text-green-800", text: "Low" }
case "medium":
return { color: "bg-yellow-100 text-yellow-800", text: "Medium" }
case "high":
return { color: "bg-orange-100 text-orange-800", text: "High" }
case "critical":
return { color: "bg-red-100 text-red-800", text: "Critical" }
default:
return { color: "bg-gray-100 text-gray-800", text: "Unknown" }
}
}
/**
* Get month name from month number (1-12)
*/
export function getMonthName(month: string | number): string {
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
const monthNum = parseInt(month.toString())
if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) {
return "Invalid Month"
}
return months[monthNum - 1]
}
/**
* Format a date into a readable string
*/
export function formatDateString(date: Date | string): string {
if (!date) return "Unknown Date"
const d = typeof date === "string" ? new Date(date) : date
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}
// Helper function to get district name from district ID
// export const getDistrictName = (districtId: string): string => {
// const feature = districtsGeoJson.features.find(
// (f) => f.properties?.kode_kec === districtId
// )
// return (
// feature?.properties?.nama ||
// feature?.properties?.kecamatan ||
// "Unknown District"
// )
// }
/**
* Format number with commas or abbreviate large numbers
*/
export function formatNumber(num?: number): string {
if (num === undefined || num === null) return "N/A"
// If number is in the thousands, abbreviate
if (num >= 1_000_000) {
return (num / 1_000_000).toFixed(1) + "M"
}
if (num >= 1_000) {
return (num / 1_000).toFixed(1) + "K"
}
// Otherwise, format with commas
return num.toLocaleString()
}
const CRIME_SEVERITY_MAP: Record<
string,
"Critical" | "High" | "Medium" | "Low"
> = {
// Critical - Highest risk of death
Pembunuhan: "Critical",
"Kelalaian Akibatkan Orang Mati": "Critical",
"Lahgun Senpi/Handak/Sajam": "Critical", // Firearms/explosives/weapons misuse
Penculikan: "Critical",
Separatisme: "Critical",
"Selundup Senpi": "Critical", // Firearms smuggling
"Kebakaran / Meletus": "Critical",
Pembakaran: "Critical",
// High - Significant risk of death or serious harm
Perkosaan: "High",
PTPPO: "High", // Human trafficking
"Trafficking In Person": "High",
"Penganiayaan Berat": "High",
Curas: "High", // Violent theft
"Pemerasan Dan Pengancaman": "High",
Premanisme: "High",
"Membahayakan Kam Umum": "High", // Endangering public safety
Pengeroyokan: "High", // Group assault
"Konflik Etnis": "High",
// Medium - Moderate risk, potential for physical harm
PKDRT: "Medium", // Domestic violence
"Penganiayaan Ringan": "Medium",
"Kelalaian Akibatkan Orang Luka": "Medium",
Curanmor: "Medium", // Vehicle theft
Curat: "Medium", // Theft with aggravating circumstances
"Pencurian Biasa": "Medium",
"Perlindungan Anak": "Medium",
"Kenakalan Remaja": "Medium",
Pengrusakan: "Medium",
"Perbuatan Tidak Menyenangkan": "Medium",
"BBM Illegal": "Medium",
"Illegal Mining": "Medium",
"Illegal Logging": "Medium",
"Illegal Fishing": "Medium",
// Low - Little to no direct risk of physical harm/death
"Pemalsuan Materai": "Low",
"Pemalsuan Surat": "Low",
Perzinahan: "Low",
"Member Suap": "Low",
Penipuan: "Low",
Agraria: "Low",
"Peradilan Anak": "Low",
Upal: "Low",
"Terhadap Ketertiban Umum": "Low",
Penghinaan: "Low",
"Sumpah Palsu": "Low",
Perjudian: "Low",
"Menerima Suap": "Low",
"Pekerjakan Anak": "Low",
Penggelapan: "Low",
"Perlindungan Saksi Korban": "Low",
"Perlindungan TKI": "Low",
Pornografi: "Low",
Keimigrasian: "Low",
Satwa: "Low",
"Money Loudering": "Low",
"Trans Ekonomi Crime": "Low",
ITE: "Low",
"Pemerintah Daerah": "Low",
"Sistem Peradilan Anak": "Low",
"Pidum Lainnya": "Low",
"Penyelenggaraan Pemilu": "Low",
"Niaga Pupuk": "Low",
Ekstradisi: "Low",
Fidusia: "Low",
"Perlindungan Konsumen": "Low",
Korupsi: "Low",
"Pidter Lainnya": "Low",
}
export function getIncidentSeverity(
incident: any
): "Low" | "Medium" | "High" | "Critical" {
if (!incident) return "Low"
const category = incident.category || "Unknown"
// console.log(incident.category);
const criticalSeverityCategories = [
"Pembunuhan",
"Kelalaian Akibatkan Orang Mati",
"Lahgun Senpi/Handak/Sajam",
"Penculikan",
"Separatisme",
"Selundup Senpi",
"Kebakaran / Meletus",
"Pembakaran",
"Perkosaan",
]
const highSeverityCategories = [
"PTPPO",
"Trafficking In Person",
"Penganiayaan Berat",
"Curas",
"Pemerasan Dan Pengancaman",
"Premanisme",
"Membahayakan Kam Umum",
"Pengeroyokan",
"Konflik Etnis",
]
const mediumSeverityCategories = [
"PKDRT",
"Penganiayaan Ringan",
"Kelalaian Akibatkan Orang Luka",
"Curanmor",
"Curat",
"Pencurian Biasa",
"Perlindungan Anak",
"Kenakalan Remaja",
"Pengrusakan",
"Perbuatan Tidak Menyenangkan",
"BBM Illegal",
"Illegal Mining",
"Illegal Logging",
"Illegal Fishing",
"Penadahan",
"Curingan",
]
const lowSeverityCategories = [
"Pemalsuan Materai",
"Pemalsuan Surat",
"Perzinahan",
"Member Suap",
"Penipuan",
"Agraria",
"Peradilan Anak",
"Upal",
"Terhadap Ketertiban Umum",
"Penghinaan",
"Sumpah Palsu",
"Perjudian",
"Menerima Suap",
"Pekerjakan Anak",
"Penggelapan",
"Perlindungan Saksi Korban",
"Perlindungan TKI",
"Pornografi",
"Keimigrasian",
"Satwa",
"Money Loudering",
"Trans Ekonomi Crime",
"ITE",
"Pemerintah Daerah",
"Sistem Peradilan Anak",
"Pidum Lainnya",
"Penyelenggaraan Pemilu",
"Niaga Pupuk",
"Ekstradisi",
"Fidusia",
"Perlindungan Konsumen",
"Korupsi",
"Pidter Lainnya",
]
if (criticalSeverityCategories.includes(category)) return "Critical"
if (highSeverityCategories.includes(category)) return "High"
if (mediumSeverityCategories.includes(category)) return "Medium"
if (lowSeverityCategories.includes(category)) return "Low"
return "Low"
}
export function formatMonthKey(monthKey: string): string {
const [year, month] = monthKey.split("-").map(Number)
return `${getMonthName(month)} ${year}`
}
export function getTimeAgo(timestamp: string | Date) {
const now = new Date()
const eventTime = new Date(timestamp)
const diffMs = now.getTime() - eventTime.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMins / 60)
const diffDays = Math.floor(diffHours / 24)
if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`
if (diffHours > 0) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`
if (diffMins > 0) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`
return "just now"
}
// export const getBreadcrumbItems = (
// pathname: string
// ): { label: string; href: string; isLast: boolean }[] => {
// // Split the path and filter empty
// const segments = pathname.split("/").filter(Boolean)
// // Build up the path for each segment
// let path = ""
// return segments.map((seg: string, idx: number) => {
// path += "/" + seg
// // Try to find a matching nav item
// let label = seg
// // Search in navPreMain, navMain, and subItems recursively
// const findLabel = (items: any[]): string | null => {
// for (const item of items) {
// if (item.url === path) return item.title
// if (item.subItems) {
// const found = findLabel(item.subItems)
// if (found) return found
// }
// if (item.subSubItems) {
// const found = findLabel(item.subSubItems)
// if (found) return found
// }
// }
// return null
// }
// label =
// findLabel(
// getNavData({
// id: "0",
// name: "John Doe",
// email: "john.doe@example.com",
// avatar: "https://avatars.githubusercontent.com/u/1486366",
// }).NavPreMain
// ) ||
// findLabel(
// getNavData({
// id: "0",
// name: "John Doe",
// email: "john.doe@example.com",
// avatar: "https://avatars.githubusercontent.com/u/1486366",
// }).reports
// ) ||
// seg.charAt(0).toUpperCase() + seg.slice(1)
// return {
// label,
// href: path,
// isLast: idx === segments.length - 1,
// }
// })
// }
export function generateFilterId(
prefixOrOptions?: keyof typeof prefixes | IGenerateFilterIdOptions,
inputOptions: IGenerateFilterIdOptions = {}
) {
const finalOptions =
typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions
const prefix =
typeof prefixOrOptions === "object" ? undefined : prefixOrOptions
const { length = 12, separator = "_" } = finalOptions
const id = customAlphabet(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
length
)()
return prefix ? `${prefixes[prefix]}${separator}${id}` : id
}
export function extractTimeRangeFilter(
filters: any[]
): [number, number] | null {
if (!Array.isArray(filters)) return null
const timeRangeFilter = filters.find(
(f) =>
f.id === "timeRange" && Array.isArray(f.value) && f.value.length === 2
)
return timeRangeFilter ? timeRangeFilter.value : null
}