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'; // Maintain a registry of used IDs to prevent duplicates const usedIdRegistry = new Set(); // Type definition for the 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}`; }; /** * 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 */ /** * Generate a unique ID with multiple options to reduce collision risk */ export function generateId( options: { prefix?: string; segments?: { codes?: string[]; year?: number; sequentialDigits?: number; includeDate?: boolean; dateFormat?: string; includeTime?: boolean; includeMilliseconds?: boolean; }; format?: string; separator?: string; upperCase?: boolean; randomSequence?: boolean; uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash'; retryOnCollision?: boolean; maxRetries?: number; } = {} ): string { // Default options const config = { prefix: options.prefix || 'ID', segments: { codes: options.segments?.codes || [], year: options.segments?.year, sequentialDigits: options.segments?.sequentialDigits || 6, // Increased to 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, }; // Static counter for sequential IDs (module-level) if (typeof globalThis.__idCounter === 'undefined') { globalThis.__idCounter = 0; } // Get current date and time with high precision const now = new Date(); // Format date based on selected format let dateString = ''; if (config.segments.includeDate) { dateString = format(now, config.segments.dateFormat); } // Format time if included (with higher precision) let timeString = ''; if (config.segments.includeTime) { timeString = format(now, 'HHmmss'); // Add milliseconds for even more uniqueness if (config.segments.includeMilliseconds) { timeString += now.getMilliseconds().toString().padStart(3, '0'); } } // Generate sequence based on strategy let sequentialNum: string; switch (config.uniquenessStrategy) { case 'uuid': // Use first part of UUID for high uniqueness sequentialNum = uuidv4().split('-')[0]; break; case 'timestamp': // Use high-precision timestamp sequentialNum = `${now.getTime()}${Math.floor(Math.random() * 1000)}`; sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits); break; case 'counter': // Use an incrementing counter sequentialNum = (++globalThis.__idCounter) .toString() .padStart(config.segments.sequentialDigits, '0'); break; case 'hash': // Create a hash from the current time and options 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: // Standard random sequence with improved randomness 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'); } } // Prepare all components const components = { prefix: config.prefix, codes: config.segments.codes.join(config.separator), year: config.segments.year || format(now, 'yyyy'), sequence: sequentialNum, date: dateString, time: timeString, }; // Build the ID based on custom format if provided let result: string; if (config.format) { let customID = config.format; for (const [key, value] of Object.entries(components)) { if (value) { const placeholder = `{${key}}`; customID = customID.replace(placeholder, String(value)); } } // Clean up any unused placeholders customID = customID.replace(/{[^}]+}/g, ''); // Clean up any consecutive separators const escapedSeparator = config.separator.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); const separatorRegex = new RegExp(`${escapedSeparator}+`, 'g'); customID = customID.replace(separatorRegex, config.separator); // Remove leading/trailing separators customID = customID.replace( new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'), '' ); result = config.upperCase ? customID.toUpperCase() : customID; } else { // Default structured build if no format specified 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.sequence) parts.push(components.sequence); if (components.date) parts.push(components.date); if (components.time) parts.push(components.time); result = parts.join(config.separator); if (config.upperCase) result = result.toUpperCase(); } // Check for collisions and retry if necessary if (config.retryOnCollision) { let retryCount = 0; let originalResult = result; while (usedIdRegistry.has(result) && retryCount < config.maxRetries) { retryCount++; // Try adding a unique suffix const suffix = crypto.randomBytes(2).toString('hex'); result = `${originalResult}${config.separator}${suffix}`; } if (retryCount >= config.maxRetries) { console.warn( `Warning: Max ID generation retries (${config.maxRetries}) reached for prefix ${config.prefix}` ); } } // Register this ID to prevent future duplicates usedIdRegistry.add(result); // Periodically clean up the registry to prevent memory leaks (optional) if (usedIdRegistry.size > 10000) { // This is a simple implementation - in production you might want a more sophisticated strategy const entriesToKeep = Array.from(usedIdRegistry).slice(-5000); usedIdRegistry.clear(); entriesToKeep.forEach((id) => usedIdRegistry.add(id)); } return result; } /** * 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; }