import { format } from 'date-fns'; import { redirect } from 'next/navigation'; import { DateFormatOptions, DateFormatPattern, } from './types/date-format.interface'; import { toast } from 'sonner'; import { IUserSchema } from '@/src/entities/models/users/users.model'; import db from '../../prisma/db'; import { v4 as uuidv4 } from 'uuid'; import * as crypto from 'crypto'; import { CRIME_RATE_COLORS } from './const/map'; import { districtsGeoJson } from '../../prisma/data/geojson/jember/districts'; // Used to track generated IDs const usedIdRegistry = new Set(); // Add type definition for global counter declare global { var __idCounter: 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: IUserSchema[] | 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 => { 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 */ /** * 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(); } /** * Gets the last ID from a specified table and column. * @param tableName - The name of the table to query. * @param columnName - The column containing the IDs. * @returns The last ID as a string, or null if no records exist. */ export async function getLastId( tableName: string, columnName: string ): Promise { try { const result = await db.$queryRawUnsafe( `SELECT ${columnName} FROM ${tableName} ORDER BY ${columnName} DESC LIMIT 1` ); if (Array.isArray(result) && result.length > 0) { return result[0][columnName]; } } catch (error) { console.error('Error fetching last ID:', error); } return null; } /** * 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 { 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' ); };