feat: add generateId, generateCityCode and getLatId
This commit is contained in:
parent
0cbcd3e636
commit
410535e1d9
|
@ -1,8 +1,22 @@
|
||||||
import { format } from "date-fns";
|
import { format } from 'date-fns';
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from 'next/navigation';
|
||||||
import { DateFormatOptions, DateFormatPattern } from "./types/date-format.interface";
|
import {
|
||||||
import { toast } from "sonner";
|
DateFormatOptions,
|
||||||
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
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<string>();
|
||||||
|
|
||||||
|
// Type definition for the global counter
|
||||||
|
declare global {
|
||||||
|
var __idCounter: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirects to a specified path with an encoded message as a query parameter.
|
* Redirects to a specified path with an encoded message as a query parameter.
|
||||||
|
@ -12,9 +26,9 @@ import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
* @returns {never} This function doesn't return as it triggers a redirect.
|
* @returns {never} This function doesn't return as it triggers a redirect.
|
||||||
*/
|
*/
|
||||||
export function encodedRedirect(
|
export function encodedRedirect(
|
||||||
type: "error" | "success",
|
type: 'error' | 'success',
|
||||||
path: string,
|
path: string,
|
||||||
message: string,
|
message: string
|
||||||
) {
|
) {
|
||||||
return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
|
return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
|
||||||
}
|
}
|
||||||
|
@ -27,17 +41,17 @@ export function encodedRedirect(
|
||||||
// Helper function to ensure URLs are properly formatted
|
// Helper function to ensure URLs are properly formatted
|
||||||
export function formatUrl(url: string): string {
|
export function formatUrl(url: string): string {
|
||||||
// If URL starts with a slash, it's already absolute
|
// If URL starts with a slash, it's already absolute
|
||||||
if (url.startsWith("/")) {
|
if (url.startsWith('/')) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, ensure it's properly formatted relative to root
|
// Otherwise, ensure it's properly formatted relative to root
|
||||||
// Remove any potential duplicated '/dashboard' prefixes
|
// Remove any potential duplicated '/dashboard' prefixes
|
||||||
if (url.startsWith("dashboard/")) {
|
if (url.startsWith('dashboard/')) {
|
||||||
return "/" + url;
|
return '/' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "/" + url;
|
return '/' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,8 +66,7 @@ export function createFormData(): FormData {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a unique username based on the provided email address.
|
* Generates a unique username based on the provided email address.
|
||||||
|
@ -71,7 +84,7 @@ export function createFormData(): FormData {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function generateUsername(email: string): string {
|
export function generateUsername(email: string): string {
|
||||||
const [localPart] = email.split("@");
|
const [localPart] = email.split('@');
|
||||||
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
|
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
|
||||||
return `${localPart}.${randomSuffix}`;
|
return `${localPart}.${randomSuffix}`;
|
||||||
}
|
}
|
||||||
|
@ -93,61 +106,61 @@ export function generateUsername(email: string): string {
|
||||||
*/
|
*/
|
||||||
export const formatDate = (
|
export const formatDate = (
|
||||||
date: string | Date | undefined | null,
|
date: string | Date | undefined | null,
|
||||||
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
|
options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
|
||||||
): string => {
|
): string => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
// Handle invalid dates
|
// Handle invalid dates
|
||||||
if (isNaN(dateObj.getTime())) {
|
if (isNaN(dateObj.getTime())) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options === "string") {
|
if (typeof options === 'string') {
|
||||||
return format(dateObj, options);
|
return format(dateObj, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { format: formatPattern = "PPpp", locale } = options;
|
const { format: formatPattern = 'PPpp', locale } = options;
|
||||||
|
|
||||||
return locale
|
return locale
|
||||||
? format(dateObj, formatPattern, { locale })
|
? format(dateObj, formatPattern, { locale })
|
||||||
: format(dateObj, formatPattern);
|
: format(dateObj, formatPattern);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyItem = (item: string, options?: {
|
export const copyItem = (
|
||||||
label?: string,
|
item: string,
|
||||||
onSuccess?: () => void,
|
options?: {
|
||||||
onError?: (error: unknown) => void
|
label?: string;
|
||||||
}) => {
|
onSuccess?: () => void;
|
||||||
|
onError?: (error: unknown) => void;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
if (!navigator.clipboard) {
|
if (!navigator.clipboard) {
|
||||||
const error = new Error("Clipboard not supported");
|
const error = new Error('Clipboard not supported');
|
||||||
toast.error("Clipboard not supported");
|
toast.error('Clipboard not supported');
|
||||||
options?.onError?.(error);
|
options?.onError?.(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
const error = new Error("Nothing to copy");
|
const error = new Error('Nothing to copy');
|
||||||
toast.error("Nothing to copy");
|
toast.error('Nothing to copy');
|
||||||
options?.onError?.(error);
|
options?.onError?.(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.clipboard.writeText(item)
|
navigator.clipboard
|
||||||
|
.writeText(item)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const label = options?.label || item;
|
const label = options?.label || item;
|
||||||
toast.success(`${label} copied to clipboard`);
|
toast.success(`${label} copied to clipboard`);
|
||||||
options?.onSuccess?.();
|
options?.onSuccess?.();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error("Failed to copy to clipboard");
|
toast.error('Failed to copy to clipboard');
|
||||||
options?.onError?.(error);
|
options?.onError?.(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -169,58 +182,50 @@ export const copyItem = (item: string, options?: {
|
||||||
*/
|
*/
|
||||||
export const formatDateWithFallback = (
|
export const formatDateWithFallback = (
|
||||||
date: string | Date | undefined | null,
|
date: string | Date | undefined | null,
|
||||||
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
|
options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
|
||||||
): string => {
|
): string => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
// Handle invalid dates
|
// Handle invalid dates
|
||||||
if (isNaN(dateObj.getTime())) {
|
if (isNaN(dateObj.getTime())) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options === "string") {
|
if (typeof options === 'string') {
|
||||||
return format(dateObj, options);
|
return format(dateObj, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { format: formatPattern = "PPpp", locale } = options;
|
const { format: formatPattern = 'PPpp', locale } = options;
|
||||||
|
|
||||||
return locale
|
return locale
|
||||||
? format(dateObj, formatPattern, { locale })
|
? format(dateObj, formatPattern, { locale })
|
||||||
: format(dateObj, formatPattern);
|
: format(dateObj, formatPattern);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatDateWithLocale = (
|
export const formatDateWithLocale = (
|
||||||
date: string | Date | undefined | null,
|
date: string | Date | undefined | null,
|
||||||
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
|
options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
|
||||||
): string => {
|
): string => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
// Handle invalid dates
|
// Handle invalid dates
|
||||||
if (isNaN(dateObj.getTime())) {
|
if (isNaN(dateObj.getTime())) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options === "string") {
|
if (typeof options === 'string') {
|
||||||
return format(dateObj, options);
|
return format(dateObj, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { format: formatPattern = "PPpp", locale } = options;
|
const { format: formatPattern = 'PPpp', locale } = options;
|
||||||
|
|
||||||
return locale
|
return locale
|
||||||
? format(dateObj, formatPattern, { locale })
|
? format(dateObj, formatPattern, { locale })
|
||||||
|
@ -244,33 +249,29 @@ export const formatDateWithLocale = (
|
||||||
*/
|
*/
|
||||||
export const formatDateWithLocaleAndFallback = (
|
export const formatDateWithLocaleAndFallback = (
|
||||||
date: string | Date | undefined | null,
|
date: string | Date | undefined | null,
|
||||||
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
|
options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
|
||||||
): string => {
|
): string => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
// Handle invalid dates
|
// Handle invalid dates
|
||||||
if (isNaN(dateObj.getTime())) {
|
if (isNaN(dateObj.getTime())) {
|
||||||
return typeof options === "string"
|
return typeof options === 'string' ? '-' : options.fallback || '-';
|
||||||
? "-"
|
|
||||||
: (options.fallback || "-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options === "string") {
|
if (typeof options === 'string') {
|
||||||
return format(dateObj, options);
|
return format(dateObj, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { format: formatPattern = "PPpp", locale } = options;
|
const { format: formatPattern = 'PPpp', locale } = options;
|
||||||
|
|
||||||
return locale
|
return locale
|
||||||
? format(dateObj, formatPattern, { locale })
|
? format(dateObj, formatPattern, { locale })
|
||||||
: format(dateObj, formatPattern);
|
: format(dateObj, formatPattern);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a full name from first and last names.
|
* Generates a full name from first and last names.
|
||||||
|
@ -278,9 +279,12 @@ export const formatDateWithLocaleAndFallback = (
|
||||||
* @param lastName - The last name.
|
* @param lastName - The last name.
|
||||||
* @returns The full name or "User" if both names are empty.
|
* @returns The full name or "User" if both names are empty.
|
||||||
*/
|
*/
|
||||||
export const getFullName = (firstName: string | null | undefined, lastName: string | null | undefined): string => {
|
export const getFullName = (
|
||||||
return `${firstName || ""} ${lastName || ""}`.trim() || "User";
|
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.
|
* Generates initials for a user based on their first and last names.
|
||||||
|
@ -289,7 +293,11 @@ export const getFullName = (firstName: string | null | undefined, lastName: stri
|
||||||
* @param email - The email address.
|
* @param email - The email address.
|
||||||
* @returns The initials or "U" if both names are empty.
|
* @returns The initials or "U" if both names are empty.
|
||||||
*/
|
*/
|
||||||
export const getInitials = (firstName: string, lastName: string, email: string): string => {
|
export const getInitials = (
|
||||||
|
firstName: string,
|
||||||
|
lastName: string,
|
||||||
|
email: string
|
||||||
|
): string => {
|
||||||
if (firstName && lastName) {
|
if (firstName && lastName) {
|
||||||
return `${firstName[0]}${lastName[0]}`.toUpperCase();
|
return `${firstName[0]}${lastName[0]}`.toUpperCase();
|
||||||
}
|
}
|
||||||
|
@ -299,9 +307,8 @@ export const getInitials = (firstName: string, lastName: string, email: string):
|
||||||
if (email) {
|
if (email) {
|
||||||
return email[0].toUpperCase();
|
return email[0].toUpperCase();
|
||||||
}
|
}
|
||||||
return "U";
|
return 'U';
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export function calculateUserStats(users: IUserSchema[] | undefined) {
|
export function calculateUserStats(users: IUserSchema[] | undefined) {
|
||||||
if (!users || !Array.isArray(users)) {
|
if (!users || !Array.isArray(users)) {
|
||||||
|
@ -337,7 +344,10 @@ export function calculateUserStats(users: IUserSchema[] | undefined) {
|
||||||
* @param params - Object containing query parameters
|
* @param params - Object containing query parameters
|
||||||
* @returns Formatted route with query parameters
|
* @returns Formatted route with query parameters
|
||||||
*/
|
*/
|
||||||
export const createRoute = (baseRoute: string, params?: Record<string, string>): string => {
|
export const createRoute = (
|
||||||
|
baseRoute: string,
|
||||||
|
params?: Record<string, string>
|
||||||
|
): string => {
|
||||||
if (!params || Object.keys(params).length === 0) {
|
if (!params || Object.keys(params).length === 0) {
|
||||||
return baseRoute;
|
return baseRoute;
|
||||||
}
|
}
|
||||||
|
@ -348,3 +358,293 @@ export const createRoute = (baseRoute: string, params?: Record<string, string>):
|
||||||
|
|
||||||
return `${baseRoute}?${queryString}`;
|
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<string | null> {
|
||||||
|
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>
|
||||||
|
): string {
|
||||||
|
let attempt = 1;
|
||||||
|
let newCode = base;
|
||||||
|
|
||||||
|
while (existingCodes.has(newCode)) {
|
||||||
|
newCode = base.slice(0, 2) + attempt;
|
||||||
|
attempt++;
|
||||||
|
}
|
||||||
|
return newCode;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue