chore: import codes

This commit is contained in:
2026-04-12 12:50:14 -04:00
parent 4bcb4898e8
commit 65ae3c6788
241 changed files with 48834 additions and 1 deletions
+77
View File
@@ -0,0 +1,77 @@
/**
* Application Configuration
* Centralized configuration management for the entire package
*/
import { getAvailableModules } from '../../modules/modules.registry.js';
/**
* Get application name from environment variables
* @returns {string} Application name
*/
export function getAppName() {
return process.env.ZEN_NAME || 'ZEN';
}
/**
* Get session cookie name from environment variables
* @returns {string} Session cookie name
*/
export function getSessionCookieName() {
return process.env.ZEN_AUTH_SESSION_COOKIE_NAME || 'zen_session';
}
/**
* Public site origin for server-side absolute URLs (emails, redirects, PDF assets).
* When NODE_ENV is development, NEXT_PUBLIC_URL_DEV is preferred, then NEXT_PUBLIC_URL.
* Otherwise NEXT_PUBLIC_URL is used. Trailing slashes are stripped.
* @returns {string}
*/
export function getPublicBaseUrl() {
const fallback = 'http://localhost:3000';
const isDev = process.env.NODE_ENV === 'development';
const raw = isDev
? (process.env.NEXT_PUBLIC_URL_DEV || process.env.NEXT_PUBLIC_URL || fallback)
: (process.env.NEXT_PUBLIC_URL || fallback);
return String(raw).replace(/\/$/, '');
}
/**
* Get enabled modules configuration (server-side only)
* This function dynamically reads from modules.registry.js and checks environment variables
* Use this on the server and pass the result to client components as props
*
* To enable a module, set the environment variable: ZEN_MODULE_{NAME}=true
* Example: ZEN_MODULE_INVOICE=true
*
* @returns {Object} Object with module names as keys and boolean values
*/
export function getModulesConfig() {
const modules = {};
const availableModules = getAvailableModules();
for (const moduleName of availableModules) {
const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`;
modules[moduleName] = process.env[envVar] === 'true';
}
return modules;
}
/**
* Get application configuration
* @returns {Object} Application configuration object
*/
export function getAppConfig() {
return {
name: getAppName(),
sessionCookieName: getSessionCookieName(),
timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
dateFormat: process.env.ZEN_DATE_FORMAT || 'YYYY-MM-DD',
// Currency configuration (for currency module)
defaultCurrency: process.env.ZEN_CURRENCY || 'CAD',
currencySymbol: process.env.ZEN_CURRENCY_SYMBOL || '$',
// Enabled modules
modules: getModulesConfig(),
};
}
+240
View File
@@ -0,0 +1,240 @@
/**
* Date Utilities
* All date functions work with UTC dates to avoid timezone issues
*/
/**
* Parse a date string as UTC date (ignoring timezone)
* This ensures dates like "2025-11-15" are always treated as November 15, 2025
* regardless of the user's timezone
*
* @param {string|Date} dateInput - Date string (YYYY-MM-DD) or Date object
* @returns {Date} Date object in UTC
*/
export function parseUTCDate(dateInput) {
if (!dateInput) return null;
// If already a Date object, return it
if (dateInput instanceof Date) {
return dateInput;
}
// Parse YYYY-MM-DD string as UTC
const [year, month, day] = dateInput.split('T')[0].split('-').map(Number);
return new Date(Date.UTC(year, month - 1, day));
}
/**
* Format a date for display
*
* @param {string|Date} dateInput - Date string or Date object
* @param {string} locale - Locale string (default: 'en-US')
* @param {Object} options - Intl.DateTimeFormat options
* @returns {string} Formatted date string
*/
export function formatDateForDisplay(dateInput, locale = 'en-US', options = {}) {
if (!dateInput) return '';
const date = parseUTCDate(dateInput);
if (!date) return '';
const defaultOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
...options
};
return date.toLocaleDateString(locale, defaultOptions);
}
/**
* Format a date for HTML input type="date"
* Returns YYYY-MM-DD format
*
* @param {string|Date} dateInput - Date string or Date object
* @returns {string} Date in YYYY-MM-DD format
*/
export function formatDateForInput(dateInput) {
if (!dateInput) return '';
const date = parseUTCDate(dateInput);
if (!date) return '';
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Format a date for HTML input type="datetime-local"
* Returns YYYY-MM-DDTHH:MM format
*
* @param {string|Date} dateInput - Date string or Date object
* @returns {string} Date in YYYY-MM-DDTHH:MM format
*/
export function formatDateTimeForInput(dateInput) {
if (!dateInput) return '';
const date = new Date(dateInput);
if (isNaN(date.getTime())) return '';
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
/**
* Format a datetime string for display (preserves time component)
* Use this for datetime fields — formatDateForDisplay strips the time via parseUTCDate.
*
* @param {string|Date} dateInput - ISO datetime string or Date object
* @param {string} locale - Locale string (default: 'fr-FR')
* @param {Object} options - Intl.DateTimeFormat options
* @returns {string} Formatted datetime string
*/
export function formatDateTimeForDisplay(dateInput, locale = 'fr-FR', options = {}) {
if (!dateInput) return '';
const date = new Date(dateInput);
if (isNaN(date.getTime())) return '';
const defaultOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC',
...options
};
return date.toLocaleDateString(locale, defaultOptions);
}
/**
* Format a date for short display (e.g., "Nov 15, 2025")
*
* @param {string|Date} dateInput - Date string or Date object
* @param {string} locale - Locale string (default: 'en-US')
* @returns {string} Formatted date string
*/
export function formatDateShort(dateInput, locale = 'en-US') {
return formatDateForDisplay(dateInput, locale, {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
/**
* Calculate days between two dates
*
* @param {string|Date} date1 - First date
* @param {string|Date} date2 - Second date
* @returns {number} Number of days between dates (can be negative)
*/
export function getDaysBetween(date1, date2) {
const d1 = parseUTCDate(date1);
const d2 = parseUTCDate(date2);
if (!d1 || !d2) return 0;
// Reset both dates to midnight UTC to avoid any time component issues
d1.setUTCHours(0, 0, 0, 0);
d2.setUTCHours(0, 0, 0, 0);
const diffTime = d2.getTime() - d1.getTime();
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}
/**
* Add days to a date
*
* @param {string|Date} dateInput - Date to add days to
* @param {number} days - Number of days to add (can be negative)
* @returns {Date} New date with days added
*/
export function addDays(dateInput, days) {
const date = parseUTCDate(dateInput);
if (!date) return null;
const newDate = new Date(date);
newDate.setUTCDate(newDate.getUTCDate() + days);
return newDate;
}
/**
* Subtract days from a date
*
* @param {string|Date} dateInput - Date to subtract days from
* @param {number} days - Number of days to subtract
* @returns {Date} New date with days subtracted
*/
export function subtractDays(dateInput, days) {
return addDays(dateInput, -days);
}
/**
* Check if a date is overdue (before today)
*
* @param {string|Date} dueDate - Due date to check
* @param {string|Date} today - Today's date (default: current date)
* @returns {boolean} True if overdue
*/
export function isOverdue(dueDate, today = null) {
const due = parseUTCDate(dueDate);
if (!due) return false;
const todayDate = today ? parseUTCDate(today) : getTodayUTC();
return due < todayDate;
}
/**
* Get today's date in UTC (at midnight)
*
* @returns {Date} Today's date in UTC
*/
export function getTodayUTC() {
const now = new Date();
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
}
/**
* Get today's date as YYYY-MM-DD string
*
* @returns {string} Today's date in YYYY-MM-DD format
*/
export function getTodayString() {
return formatDateForInput(getTodayUTC());
}
/**
* Compare two dates (ignoring time)
*
* @param {string|Date} date1 - First date
* @param {string|Date} date2 - Second date
* @returns {number} -1 if date1 < date2, 0 if equal, 1 if date1 > date2
*/
export function compareDates(date1, date2) {
const d1 = parseUTCDate(date1);
const d2 = parseUTCDate(date2);
if (!d1 || !d2) return 0;
if (d1 < d2) return -1;
if (d1 > d2) return 1;
return 0;
}
+109
View File
@@ -0,0 +1,109 @@
/**
* ZEN Initialization
* Initialize all ZEN services and modules using dynamic module discovery
*/
import { discoverModules, startModuleCronJobs, stopModuleCronJobs } from '../../core/modules/index.js';
// Use globalThis to persist initialization flag across module reloads
const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
/**
* Initialize ZEN system
* Discovers modules dynamically and starts cron jobs
*
* Recommended: Use instrumentation.js for automatic initialization
* Alternative: Call this function manually in your root layout
*
* @example
* // instrumentation.js (Recommended)
* export async function register() {
* if (process.env.NEXT_RUNTIME === 'nodejs') {
* const { initializeZen } = await import('@hykocx/zen');
* await initializeZen();
* }
* }
*
* @example
* // app/layout.js (Alternative)
* import { initializeZen } from '@hykocx/zen';
* initializeZen();
*
* @param {Object} options - Initialization options
* @param {boolean} options.skipCron - Skip cron job initialization
* @param {boolean} options.skipDb - Skip database initialization
* @returns {Promise<Object>} Initialization result
*/
export async function initializeZen(options = {}) {
const { skipCron = false, skipDb = true } = options;
// Only run on server-side
if (typeof window !== 'undefined') {
return { skipped: true, reason: 'client-side' };
}
// Prevent multiple initializations using globalThis
if (globalThis[ZEN_INIT_KEY]) {
console.log('⚠ ZEN: Already initialized, skipping...');
return { skipped: true, reason: 'already-initialized' };
}
globalThis[ZEN_INIT_KEY] = true;
console.log('🚀 ZEN: Starting initialization...');
const result = {
discovery: null,
cron: { started: [], errors: [] }
};
try {
// Step 1: Discover and register all enabled modules
// This reads from modules.registry.js and loads each module's config files
result.discovery = await discoverModules();
const enabledCount = result.discovery.enabled?.length || 0;
const skippedCount = result.discovery.skipped?.length || 0;
if (enabledCount > 0) {
console.log(`✓ ZEN: Discovered ${enabledCount} enabled module(s): ${result.discovery.enabled.join(', ')}`);
}
if (skippedCount > 0) {
console.log(`⚠ ZEN: Skipped ${skippedCount} disabled module(s): ${result.discovery.skipped.join(', ')}`);
}
// Step 2: Start cron jobs for all enabled modules
if (!skipCron) {
result.cron = await startModuleCronJobs();
if (result.cron.started.length > 0) {
console.log(`✓ ZEN: Started ${result.cron.started.length} cron job(s): ${result.cron.started.join(', ')}`);
}
}
console.log('✓ ZEN: Initialization complete');
} catch (error) {
console.error('✗ ZEN: Initialization failed:', error);
result.error = error.message;
}
return result;
}
/**
* Reset initialization flag (useful for testing or manual reinitialization)
* @returns {void}
*/
export function resetZenInitialization() {
globalThis[ZEN_INIT_KEY] = false;
// Stop all cron jobs using the module system
try {
stopModuleCronJobs();
} catch (e) {
// Cron system not available
}
console.log('⚠ ZEN: Initialization flag reset');
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Metadata Utilities
* Functions to generate dynamic metadata for Next.js pages
*/
import { getAppName } from '../appConfig.js';
/**
* Generate page title with app name
* @param {string} title - Page title
* @param {Object} options - Options
* @param {boolean} options.includeAppName - Include app name in title (default: true)
* @param {string} options.separator - Separator between title and app name (default: ' - ')
* @param {string} options.appName - Custom app name (default: from config)
* @returns {string} Formatted title
*/
export function generateTitle(title, options = {}) {
const {
includeAppName = true,
separator = ' - ',
appName = null
} = options;
if (!includeAppName) {
return title;
}
const name = appName || getAppName();
return `${title}${separator}${name}`;
}
/**
* Generate metadata object for Next.js
* @param {Object} params - Metadata parameters
* @param {string} params.title - Page title
* @param {string} params.description - Page description
* @param {Object} params.openGraph - Open Graph metadata
* @param {Object} params.twitter - Twitter metadata
* @param {Object} params.other - Other metadata fields
* @param {Object} options - Generation options
* @param {boolean} options.includeAppName - Include app name in title (default: true)
* @param {string} options.appName - Custom app name
* @returns {Object} Next.js metadata object
*/
export function generateMetadata(params = {}, options = {}) {
const {
title,
description,
openGraph,
twitter,
...other
} = params;
const metadata = {};
// Title
if (title) {
metadata.title = generateTitle(title, options);
}
// Description
if (description) {
metadata.description = description;
}
// Open Graph
if (openGraph) {
metadata.openGraph = {
...openGraph,
title: openGraph.title ? generateTitle(openGraph.title, options) : metadata.title,
description: openGraph.description || description,
};
}
// Twitter
if (twitter) {
metadata.twitter = {
...twitter,
title: twitter.title ? generateTitle(twitter.title, options) : metadata.title,
description: twitter.description || description,
};
}
// Other metadata
Object.assign(metadata, other);
return metadata;
}
/**
* Generate robots metadata
* @param {Object} options - Robots options
* @param {boolean} options.index - Allow indexing (default: true)
* @param {boolean} options.follow - Allow following links (default: true)
* @param {boolean} options.noarchive - Prevent archiving
* @param {boolean} options.nocache - Prevent caching
* @returns {Object} Robots metadata
*/
export function generateRobots(options = {}) {
const {
index = true,
follow = true,
noarchive = false,
nocache = false,
} = options;
return {
index,
follow,
...(noarchive && { noarchive: true }),
...(nocache && { nocache: true }),
};
}