chore: refactor project structure and clean up unused components
- Updated `TODO.md` to reflect new testing tasks and final structure expectations. - Simplified TypeScript path mappings in `tsconfig.json` for better clarity. - Revised business logic separation rules in `.cursor/rules` to align with new directory structure. - Deleted unused client components and services to streamline the codebase. - Adjusted import paths in scripts to match the new structure.
This commit is contained in:
211
src/lib/backup-utils.ts
Normal file
211
src/lib/backup-utils.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Utilitaires pour les opérations de backup
|
||||
*/
|
||||
export class BackupUtils {
|
||||
/**
|
||||
* Calcule un hash SHA-256 d'un fichier
|
||||
*/
|
||||
static async calculateFileHash(filePath: string): Promise<string> {
|
||||
try {
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
return createHash('sha256').update(fileBuffer).digest('hex');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to calculate hash for ${filePath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout le chemin de la base de données selon la configuration
|
||||
*/
|
||||
static resolveDatabasePath(): string {
|
||||
if (process.env.BACKUP_DATABASE_PATH) {
|
||||
return path.resolve(process.cwd(), process.env.BACKUP_DATABASE_PATH);
|
||||
} else if (process.env.DATABASE_URL) {
|
||||
return path.resolve(process.env.DATABASE_URL.replace('file:', ''));
|
||||
} else {
|
||||
return path.resolve(process.cwd(), 'prisma', 'dev.db');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout le chemin de stockage des backups
|
||||
*/
|
||||
static resolveBackupStoragePath(): string {
|
||||
if (process.env.BACKUP_STORAGE_PATH) {
|
||||
return path.resolve(process.cwd(), process.env.BACKUP_STORAGE_PATH);
|
||||
}
|
||||
|
||||
return process.env.NODE_ENV === 'production'
|
||||
? path.join(process.cwd(), 'data', 'backups')
|
||||
: path.join(process.cwd(), 'backups');
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une sauvegarde SQLite en utilisant la commande .backup
|
||||
*/
|
||||
static async createSQLiteBackup(sourcePath: string, backupPath: string): Promise<void> {
|
||||
// Vérifier que le fichier source existe
|
||||
try {
|
||||
await fs.stat(sourcePath);
|
||||
} catch {
|
||||
throw new Error(`Source database not found: ${sourcePath}`);
|
||||
}
|
||||
|
||||
// Méthode 1: Utiliser sqlite3 CLI (plus fiable)
|
||||
try {
|
||||
const command = `sqlite3 "${sourcePath}" ".backup '${backupPath}'"`;
|
||||
await execAsync(command);
|
||||
console.log(`✅ SQLite backup created using CLI: ${backupPath}`);
|
||||
return;
|
||||
} catch (cliError) {
|
||||
console.warn(`⚠️ SQLite CLI backup failed, trying copy method:`, cliError);
|
||||
}
|
||||
|
||||
// Méthode 2: Copie simple du fichier (fallback)
|
||||
try {
|
||||
await fs.copyFile(sourcePath, backupPath);
|
||||
console.log(`✅ SQLite backup created using file copy: ${backupPath}`);
|
||||
} catch (copyError) {
|
||||
throw new Error(`Failed to create SQLite backup: ${copyError}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresse un fichier avec gzip
|
||||
*/
|
||||
static async compressFile(filePath: string): Promise<string> {
|
||||
const compressedPath = `${filePath}.gz`;
|
||||
|
||||
try {
|
||||
const command = `gzip -c "${filePath}" > "${compressedPath}"`;
|
||||
await execAsync(command);
|
||||
console.log(`✅ File compressed: ${compressedPath}`);
|
||||
return compressedPath;
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Compression failed, keeping uncompressed file:`, error);
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Décompresse un fichier gzip temporairement
|
||||
*/
|
||||
static async decompressFileTemp(compressedPath: string, tempPath: string): Promise<void> {
|
||||
try {
|
||||
await execAsync(`gunzip -c "${compressedPath}" > "${tempPath}"`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to decompress ${compressedPath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate la taille de fichier en unités lisibles
|
||||
*/
|
||||
static formatFileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* S'assure qu'un dossier existe
|
||||
*/
|
||||
static async ensureDirectory(dirPath: string): Promise<void> {
|
||||
try {
|
||||
await fs.access(dirPath);
|
||||
} catch {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
console.log(`📁 Created directory: ${dirPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse le nom de fichier de backup pour extraire les métadonnées
|
||||
*/
|
||||
static parseBackupFilename(filename: string): { type: 'manual' | 'automatic'; date: Date | null } {
|
||||
// Nouveau format: towercontrol_manual_2025-09-18T14-12-05-737Z.db
|
||||
// Ancien format: towercontrol_2025-09-18T14-12-05-737Z.db (considéré comme automatic)
|
||||
let type: 'manual' | 'automatic' = 'automatic';
|
||||
let dateMatch = filename.match(/towercontrol_(manual|automatic)_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/);
|
||||
|
||||
if (!dateMatch) {
|
||||
// Format ancien sans type - considérer comme automatic
|
||||
dateMatch = filename.match(/towercontrol_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/);
|
||||
if (dateMatch) {
|
||||
dateMatch = [dateMatch[0], 'automatic', dateMatch[1]]; // Restructurer pour compatibilité
|
||||
}
|
||||
} else {
|
||||
type = dateMatch[1] as 'manual' | 'automatic';
|
||||
}
|
||||
|
||||
let date: Date | null = null;
|
||||
if (dateMatch && dateMatch[2]) {
|
||||
// Convertir le format de fichier vers ISO string valide
|
||||
// Format: 2025-09-18T14-12-05-737Z -> 2025-09-18T14:12:05.737Z
|
||||
const isoString = dateMatch[2]
|
||||
.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/, 'T$1:$2:$3.$4Z');
|
||||
date = new Date(isoString);
|
||||
}
|
||||
|
||||
return { type, date };
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un nom de fichier de backup
|
||||
*/
|
||||
static generateBackupFilename(type: 'manual' | 'automatic'): string {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
return `towercontrol_${type}_${timestamp}.db`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit une entrée dans le fichier de log
|
||||
*/
|
||||
static async writeLogEntry(
|
||||
logPath: string,
|
||||
type: 'manual' | 'automatic',
|
||||
action: 'created' | 'skipped' | 'failed',
|
||||
details: string,
|
||||
extra?: { hash?: string; size?: number; previousHash?: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
const date = new Date().toLocaleString('fr-FR');
|
||||
|
||||
let logEntry = `[${date}] ${type.toUpperCase()} BACKUP ${action.toUpperCase()}: ${details}`;
|
||||
|
||||
if (extra) {
|
||||
if (extra.hash) {
|
||||
logEntry += ` | Hash: ${extra.hash.substring(0, 12)}...`;
|
||||
}
|
||||
if (extra.size) {
|
||||
logEntry += ` | Size: ${BackupUtils.formatFileSize(extra.size)}`;
|
||||
}
|
||||
if (extra.previousHash) {
|
||||
logEntry += ` | Previous: ${extra.previousHash.substring(0, 12)}...`;
|
||||
}
|
||||
}
|
||||
|
||||
logEntry += '\n';
|
||||
|
||||
await fs.appendFile(logPath, logEntry);
|
||||
} catch (error) {
|
||||
console.error('Error writing to backup log:', error);
|
||||
// Ne pas faire échouer l'opération si on ne peut pas logger
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/lib/config.ts
Normal file
68
src/lib/config.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Configuration de l'application TowerControl (version standalone)
|
||||
*/
|
||||
|
||||
export interface AppConfig {
|
||||
app: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
ui: {
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
itemsPerPage: number;
|
||||
};
|
||||
features: {
|
||||
enableDragAndDrop: boolean;
|
||||
enableNotifications: boolean;
|
||||
autoSave: boolean;
|
||||
};
|
||||
integrations: {
|
||||
jira: {
|
||||
enabled: boolean;
|
||||
baseUrl?: string;
|
||||
email?: string;
|
||||
apiToken?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Configuration par défaut
|
||||
const defaultConfig: AppConfig = {
|
||||
app: {
|
||||
name: 'TowerControl',
|
||||
version: '2.0.0'
|
||||
},
|
||||
ui: {
|
||||
theme: (process.env.NEXT_PUBLIC_THEME as 'light' | 'dark' | 'system') || 'system',
|
||||
itemsPerPage: parseInt(process.env.NEXT_PUBLIC_ITEMS_PER_PAGE || '50')
|
||||
},
|
||||
features: {
|
||||
enableDragAndDrop: process.env.NEXT_PUBLIC_ENABLE_DRAG_DROP !== 'false',
|
||||
enableNotifications: process.env.NEXT_PUBLIC_ENABLE_NOTIFICATIONS === 'true',
|
||||
autoSave: process.env.NEXT_PUBLIC_AUTO_SAVE !== 'false'
|
||||
},
|
||||
integrations: {
|
||||
jira: {
|
||||
enabled: Boolean(process.env.JIRA_BASE_URL && process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN),
|
||||
baseUrl: process.env.JIRA_BASE_URL,
|
||||
email: process.env.JIRA_EMAIL,
|
||||
apiToken: process.env.JIRA_API_TOKEN
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Récupère la configuration de l'application
|
||||
*/
|
||||
export function getConfig(): AppConfig {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration pour le développement/debug
|
||||
*/
|
||||
export const DEBUG_CONFIG = {
|
||||
isDevelopment: process.env.NODE_ENV === 'development',
|
||||
verboseLogging: process.env.VERBOSE_LOGGING === 'true',
|
||||
enableDevTools: process.env.NODE_ENV === 'development'
|
||||
};
|
||||
143
src/lib/jira-period-filter.ts
Normal file
143
src/lib/jira-period-filter.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { JiraAnalytics } from './types';
|
||||
|
||||
export type PeriodFilter = '7d' | '30d' | '3m' | 'current';
|
||||
|
||||
/**
|
||||
* Filtre les analytics Jira selon la période sélectionnée
|
||||
*/
|
||||
export function filterAnalyticsByPeriod(
|
||||
analytics: JiraAnalytics,
|
||||
period: PeriodFilter
|
||||
): JiraAnalytics {
|
||||
const now = new Date();
|
||||
let cutoffDate: Date;
|
||||
|
||||
switch (period) {
|
||||
case '7d':
|
||||
cutoffDate = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '30d':
|
||||
cutoffDate = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '3m':
|
||||
cutoffDate = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case 'current':
|
||||
default:
|
||||
// Pour "Sprint actuel", on garde toutes les données mais on filtre les sprints
|
||||
return filterCurrentSprintAnalytics(analytics);
|
||||
}
|
||||
|
||||
// Filtrer les données par date
|
||||
return filterAnalyticsByDate(analytics, cutoffDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les analytics pour ne garder que le sprint actuel
|
||||
*/
|
||||
function filterCurrentSprintAnalytics(analytics: JiraAnalytics): JiraAnalytics {
|
||||
// Garder seulement le dernier sprint (le plus récent)
|
||||
const currentSprint = analytics.velocityMetrics.sprintHistory.slice(-1);
|
||||
|
||||
return {
|
||||
...analytics,
|
||||
velocityMetrics: {
|
||||
...analytics.velocityMetrics,
|
||||
sprintHistory: currentSprint,
|
||||
// Recalculer la vélocité moyenne avec seulement le sprint actuel
|
||||
averageVelocity: currentSprint.length > 0 ? currentSprint[0].completedPoints : 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les analytics par date de cutoff
|
||||
*/
|
||||
function filterAnalyticsByDate(analytics: JiraAnalytics, cutoffDate: Date): JiraAnalytics {
|
||||
// Filtrer l'historique des sprints
|
||||
const filteredSprintHistory = analytics.velocityMetrics.sprintHistory.filter(sprint => {
|
||||
const sprintEndDate = new Date(sprint.endDate);
|
||||
return sprintEndDate >= cutoffDate;
|
||||
});
|
||||
|
||||
// Si aucun sprint dans la période, garder au moins le plus récent
|
||||
const sprintHistory = filteredSprintHistory.length > 0
|
||||
? filteredSprintHistory
|
||||
: analytics.velocityMetrics.sprintHistory.slice(-1);
|
||||
|
||||
// Recalculer la vélocité moyenne
|
||||
const averageVelocity = sprintHistory.length > 0
|
||||
? Math.round(sprintHistory.reduce((sum, sprint) => sum + sprint.completedPoints, 0) / sprintHistory.length)
|
||||
: 0;
|
||||
|
||||
// Pour simplifier, on garde les autres métriques inchangées
|
||||
// Dans une vraie implémentation, on devrait re-filtrer toutes les données
|
||||
return {
|
||||
...analytics,
|
||||
velocityMetrics: {
|
||||
...analytics.velocityMetrics,
|
||||
sprintHistory,
|
||||
averageVelocity
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne un label descriptif pour la période sélectionnée
|
||||
*/
|
||||
export function getPeriodLabel(period: PeriodFilter): string {
|
||||
switch (period) {
|
||||
case '7d':
|
||||
return 'Derniers 7 jours';
|
||||
case '30d':
|
||||
return 'Derniers 30 jours';
|
||||
case '3m':
|
||||
return 'Derniers 3 mois';
|
||||
case 'current':
|
||||
return 'Sprint actuel';
|
||||
default:
|
||||
return 'Période inconnue';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne des informations sur la période pour l'affichage
|
||||
*/
|
||||
export function getPeriodInfo(period: PeriodFilter): {
|
||||
label: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
} {
|
||||
switch (period) {
|
||||
case '7d':
|
||||
return {
|
||||
label: 'Derniers 7 jours',
|
||||
description: 'Vue hebdomadaire des métriques',
|
||||
icon: '📅'
|
||||
};
|
||||
case '30d':
|
||||
return {
|
||||
label: 'Derniers 30 jours',
|
||||
description: 'Vue mensuelle des métriques',
|
||||
icon: '📊'
|
||||
};
|
||||
case '3m':
|
||||
return {
|
||||
label: 'Derniers 3 mois',
|
||||
description: 'Vue trimestrielle des métriques',
|
||||
icon: '📈'
|
||||
};
|
||||
case 'current':
|
||||
return {
|
||||
label: 'Sprint actuel',
|
||||
description: 'Focus sur le sprint en cours',
|
||||
icon: '🎯'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: 'Période inconnue',
|
||||
description: '',
|
||||
icon: '❓'
|
||||
};
|
||||
}
|
||||
}
|
||||
205
src/lib/sort-config.ts
Normal file
205
src/lib/sort-config.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { Task, TaskPriority } from './types';
|
||||
import { getPriorityConfig } from './status-config';
|
||||
|
||||
export type SortField = 'priority' | 'tags' | 'createdAt' | 'updatedAt' | 'dueDate' | 'title';
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export interface SortConfig {
|
||||
field: SortField;
|
||||
direction: SortDirection;
|
||||
}
|
||||
|
||||
export interface SortOption {
|
||||
key: string;
|
||||
label: string;
|
||||
field: SortField;
|
||||
direction: SortDirection;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// Configuration des options de tri disponibles
|
||||
export const SORT_OPTIONS: SortOption[] = [
|
||||
{
|
||||
key: 'priority-desc',
|
||||
label: 'Priorité (Urgente → Faible)',
|
||||
field: 'priority',
|
||||
direction: 'desc',
|
||||
icon: '🔥'
|
||||
},
|
||||
{
|
||||
key: 'priority-asc',
|
||||
label: 'Priorité (Faible → Urgente)',
|
||||
field: 'priority',
|
||||
direction: 'asc',
|
||||
icon: '🔵'
|
||||
},
|
||||
{
|
||||
key: 'tags-asc',
|
||||
label: 'Tags (A → Z)',
|
||||
field: 'tags',
|
||||
direction: 'asc',
|
||||
icon: '🏷️'
|
||||
},
|
||||
{
|
||||
key: 'title-asc',
|
||||
label: 'Titre (A → Z)',
|
||||
field: 'title',
|
||||
direction: 'asc',
|
||||
icon: '📝'
|
||||
},
|
||||
{
|
||||
key: 'title-desc',
|
||||
label: 'Titre (Z → A)',
|
||||
field: 'title',
|
||||
direction: 'desc',
|
||||
icon: '📝'
|
||||
},
|
||||
{
|
||||
key: 'createdAt-desc',
|
||||
label: 'Date création (Récent → Ancien)',
|
||||
field: 'createdAt',
|
||||
direction: 'desc',
|
||||
icon: '📅'
|
||||
},
|
||||
{
|
||||
key: 'createdAt-asc',
|
||||
label: 'Date création (Ancien → Récent)',
|
||||
field: 'createdAt',
|
||||
direction: 'asc',
|
||||
icon: '📅'
|
||||
},
|
||||
{
|
||||
key: 'dueDate-asc',
|
||||
label: 'Échéance (Proche → Lointaine)',
|
||||
field: 'dueDate',
|
||||
direction: 'asc',
|
||||
icon: '⏰'
|
||||
},
|
||||
{
|
||||
key: 'dueDate-desc',
|
||||
label: 'Échéance (Lointaine → Proche)',
|
||||
field: 'dueDate',
|
||||
direction: 'desc',
|
||||
icon: '⏰'
|
||||
}
|
||||
];
|
||||
|
||||
// Tri par défaut : Priorité (desc) puis Tags (asc)
|
||||
export const DEFAULT_SORT: SortConfig[] = [
|
||||
{ field: 'priority', direction: 'desc' },
|
||||
{ field: 'tags', direction: 'asc' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Compare deux valeurs selon la direction de tri
|
||||
*/
|
||||
function compareValues<T>(a: T, b: T, direction: SortDirection): number {
|
||||
if (a === b) return 0;
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
|
||||
const result = a < b ? -1 : 1;
|
||||
return direction === 'asc' ? result : -result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la valeur de priorité numérique pour le tri
|
||||
*/
|
||||
function getPriorityValue(priority: TaskPriority): number {
|
||||
const config = getPriorityConfig(priority);
|
||||
if (!config) {
|
||||
console.warn(`⚠️ Priorité inconnue: ${priority}, utilisation de 'medium' par défaut`);
|
||||
return getPriorityConfig('medium').order;
|
||||
}
|
||||
return config.order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le premier tag pour le tri (ou chaîne vide si pas de tags)
|
||||
*/
|
||||
function getFirstTag(task: Task): string {
|
||||
return task.tags?.[0]?.toLowerCase() || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare deux tâches selon un critère de tri
|
||||
*/
|
||||
function compareTasksByField(a: Task, b: Task, sortConfig: SortConfig): number {
|
||||
const { field, direction } = sortConfig;
|
||||
|
||||
switch (field) {
|
||||
case 'priority':
|
||||
return compareValues(
|
||||
getPriorityValue(a.priority),
|
||||
getPriorityValue(b.priority),
|
||||
direction
|
||||
);
|
||||
|
||||
case 'tags':
|
||||
return compareValues(
|
||||
getFirstTag(a),
|
||||
getFirstTag(b),
|
||||
direction
|
||||
);
|
||||
|
||||
case 'title':
|
||||
return compareValues(
|
||||
a.title.toLowerCase(),
|
||||
b.title.toLowerCase(),
|
||||
direction
|
||||
);
|
||||
|
||||
case 'createdAt':
|
||||
return compareValues(
|
||||
new Date(a.createdAt).getTime(),
|
||||
new Date(b.createdAt).getTime(),
|
||||
direction
|
||||
);
|
||||
|
||||
case 'updatedAt':
|
||||
return compareValues(
|
||||
new Date(a.updatedAt).getTime(),
|
||||
new Date(b.updatedAt).getTime(),
|
||||
direction
|
||||
);
|
||||
|
||||
case 'dueDate':
|
||||
return compareValues(
|
||||
a.dueDate ? new Date(a.dueDate).getTime() : null,
|
||||
b.dueDate ? new Date(b.dueDate).getTime() : null,
|
||||
direction
|
||||
);
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trie un tableau de tâches selon une configuration de tri multiple
|
||||
*/
|
||||
export function sortTasks(tasks: Task[], sortConfigs: SortConfig[] = DEFAULT_SORT): Task[] {
|
||||
return [...tasks].sort((a, b) => {
|
||||
for (const sortConfig of sortConfigs) {
|
||||
const result = compareTasksByField(a, b, sortConfig);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilitaire pour obtenir une option de tri par sa clé
|
||||
*/
|
||||
export function getSortOption(key: string): SortOption | undefined {
|
||||
return SORT_OPTIONS.find(option => option.key === key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilitaire pour créer une clé de tri
|
||||
*/
|
||||
export function createSortKey(field: SortField, direction: SortDirection): string {
|
||||
return `${field}-${direction}`;
|
||||
}
|
||||
266
src/lib/status-config.ts
Normal file
266
src/lib/status-config.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { TaskStatus, TaskPriority } from './types';
|
||||
|
||||
export interface StatusConfig {
|
||||
key: TaskStatus;
|
||||
label: string;
|
||||
icon: string;
|
||||
color: 'gray' | 'blue' | 'green' | 'red' | 'purple';
|
||||
order: number;
|
||||
}
|
||||
|
||||
export const STATUS_CONFIG: Record<TaskStatus, StatusConfig> = {
|
||||
backlog: {
|
||||
key: 'backlog',
|
||||
label: 'Backlog',
|
||||
icon: '📋',
|
||||
color: 'gray',
|
||||
order: 0
|
||||
},
|
||||
todo: {
|
||||
key: 'todo',
|
||||
label: 'À faire',
|
||||
icon: '⚡',
|
||||
color: 'gray',
|
||||
order: 1
|
||||
},
|
||||
in_progress: {
|
||||
key: 'in_progress',
|
||||
label: 'En cours',
|
||||
icon: '⚙️',
|
||||
color: 'blue',
|
||||
order: 2
|
||||
},
|
||||
freeze: {
|
||||
key: 'freeze',
|
||||
label: 'Gelé',
|
||||
icon: '🧊',
|
||||
color: 'purple',
|
||||
order: 3
|
||||
},
|
||||
done: {
|
||||
key: 'done',
|
||||
label: 'Terminé',
|
||||
icon: '✓',
|
||||
color: 'green',
|
||||
order: 4
|
||||
},
|
||||
cancelled: {
|
||||
key: 'cancelled',
|
||||
label: 'Annulé',
|
||||
icon: '✕',
|
||||
color: 'red',
|
||||
order: 5
|
||||
},
|
||||
archived: {
|
||||
key: 'archived',
|
||||
label: 'Archivé',
|
||||
icon: '📦',
|
||||
color: 'gray',
|
||||
order: 6
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Utilitaires pour récupérer facilement les infos
|
||||
export const getStatusConfig = (status: TaskStatus): StatusConfig => {
|
||||
return STATUS_CONFIG[status];
|
||||
};
|
||||
|
||||
export const getAllStatuses = (): StatusConfig[] => {
|
||||
return Object.values(STATUS_CONFIG).sort((a, b) => a.order - b.order);
|
||||
};
|
||||
|
||||
export const getStatusLabel = (status: TaskStatus): string => {
|
||||
return STATUS_CONFIG[status].label;
|
||||
};
|
||||
|
||||
export const getStatusIcon = (status: TaskStatus): string => {
|
||||
return STATUS_CONFIG[status].icon;
|
||||
};
|
||||
|
||||
export const getStatusColor = (status: TaskStatus): StatusConfig['color'] => {
|
||||
return STATUS_CONFIG[status].color;
|
||||
};
|
||||
|
||||
// Configuration des couleurs tech/cyberpunk
|
||||
export const TECH_STYLES = {
|
||||
gray: {
|
||||
border: 'border-slate-700',
|
||||
glow: 'shadow-slate-500/20',
|
||||
accent: 'text-slate-400',
|
||||
badge: 'bg-slate-800 text-slate-300 border border-slate-600'
|
||||
},
|
||||
blue: {
|
||||
border: 'border-cyan-500/30',
|
||||
glow: 'shadow-cyan-500/20',
|
||||
accent: 'text-cyan-400',
|
||||
badge: 'bg-cyan-950 text-cyan-300 border border-cyan-500/30'
|
||||
},
|
||||
green: {
|
||||
border: 'border-emerald-500/30',
|
||||
glow: 'shadow-emerald-500/20',
|
||||
accent: 'text-emerald-400',
|
||||
badge: 'bg-emerald-950 text-emerald-300 border border-emerald-500/30'
|
||||
},
|
||||
red: {
|
||||
border: 'border-red-500/30',
|
||||
glow: 'shadow-red-500/20',
|
||||
accent: 'text-red-400',
|
||||
badge: 'bg-red-950 text-red-300 border border-red-500/30'
|
||||
},
|
||||
purple: {
|
||||
border: 'border-purple-500/30',
|
||||
glow: 'shadow-purple-500/20',
|
||||
accent: 'text-purple-400',
|
||||
badge: 'bg-purple-950 text-purple-300 border border-purple-500/30'
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const getTechStyle = (color: StatusConfig['color']) => {
|
||||
return TECH_STYLES[color];
|
||||
};
|
||||
|
||||
export const getBadgeVariant = (color: StatusConfig['color']): 'success' | 'primary' | 'danger' | 'default' => {
|
||||
switch (color) {
|
||||
case 'green': return 'success';
|
||||
case 'blue':
|
||||
case 'purple': return 'primary';
|
||||
case 'red': return 'danger';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
// Configuration des priorités
|
||||
export interface PriorityConfig {
|
||||
key: TaskPriority;
|
||||
label: string;
|
||||
icon: string;
|
||||
color: 'blue' | 'yellow' | 'purple' | 'red';
|
||||
order: number;
|
||||
}
|
||||
|
||||
export const PRIORITY_CONFIG: Record<TaskPriority, PriorityConfig> = {
|
||||
low: {
|
||||
key: 'low',
|
||||
label: 'Faible',
|
||||
icon: '🔵',
|
||||
color: 'blue',
|
||||
order: 1
|
||||
},
|
||||
medium: {
|
||||
key: 'medium',
|
||||
label: 'Moyenne',
|
||||
icon: '🟡',
|
||||
color: 'yellow',
|
||||
order: 2
|
||||
},
|
||||
high: {
|
||||
key: 'high',
|
||||
label: 'Élevée',
|
||||
icon: '🟣',
|
||||
color: 'purple',
|
||||
order: 3
|
||||
},
|
||||
urgent: {
|
||||
key: 'urgent',
|
||||
label: 'Urgente',
|
||||
icon: '🔴',
|
||||
color: 'red',
|
||||
order: 4
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Utilitaires pour les priorités
|
||||
export const getPriorityConfig = (priority: TaskPriority): PriorityConfig => {
|
||||
return PRIORITY_CONFIG[priority];
|
||||
};
|
||||
|
||||
export const getAllPriorities = (): PriorityConfig[] => {
|
||||
return Object.values(PRIORITY_CONFIG).sort((a, b) => a.order - b.order);
|
||||
};
|
||||
|
||||
export const getPriorityLabel = (priority: TaskPriority): string => {
|
||||
return PRIORITY_CONFIG[priority].label;
|
||||
};
|
||||
|
||||
export const getPriorityIcon = (priority: TaskPriority): string => {
|
||||
return PRIORITY_CONFIG[priority].icon;
|
||||
};
|
||||
|
||||
export const getPriorityColor = (priority: TaskPriority): PriorityConfig['color'] => {
|
||||
return PRIORITY_CONFIG[priority].color;
|
||||
};
|
||||
|
||||
// Configuration des couleurs HEX pour les priorités (cohérente avec le design)
|
||||
export const PRIORITY_COLOR_MAP = {
|
||||
blue: '#60a5fa', // blue-400 (low priority)
|
||||
yellow: '#fbbf24', // amber-400 (medium priority)
|
||||
purple: '#a78bfa', // violet-400 (high priority)
|
||||
red: '#f87171' // red-400 (urgent priority)
|
||||
} as const;
|
||||
|
||||
// Couleurs alternatives pour les graphiques et charts
|
||||
export const PRIORITY_CHART_COLORS = {
|
||||
'Faible': '#10b981', // green-500 (plus lisible dans les charts)
|
||||
'Moyenne': '#f59e0b', // amber-500
|
||||
'Élevée': '#8b5cf6', // violet-500
|
||||
'Urgente': '#ef4444', // red-500
|
||||
'Non définie': '#6b7280' // gray-500
|
||||
} as const;
|
||||
|
||||
export const getPriorityColorHex = (color: PriorityConfig['color']): string => {
|
||||
return PRIORITY_COLOR_MAP[color];
|
||||
};
|
||||
|
||||
// Fonction pour récupérer la couleur d'un chart basée sur le label
|
||||
export const getPriorityChartColor = (priorityLabel: string): string => {
|
||||
return PRIORITY_CHART_COLORS[priorityLabel as keyof typeof PRIORITY_CHART_COLORS] || PRIORITY_CHART_COLORS['Non définie'];
|
||||
};
|
||||
|
||||
// Configuration des couleurs pour les badges de statut
|
||||
export const STATUS_BADGE_COLORS = {
|
||||
backlog: 'bg-gray-100 text-gray-800',
|
||||
todo: 'bg-gray-100 text-gray-800',
|
||||
in_progress: 'bg-orange-100 text-orange-800',
|
||||
freeze: 'bg-purple-100 text-purple-800',
|
||||
done: 'bg-green-100 text-green-800',
|
||||
cancelled: 'bg-red-100 text-red-800',
|
||||
archived: 'bg-gray-100 text-gray-600'
|
||||
} as const;
|
||||
|
||||
// Fonction pour récupérer les classes CSS d'un badge de statut
|
||||
export const getStatusBadgeClasses = (status: TaskStatus): string => {
|
||||
return STATUS_BADGE_COLORS[status] || STATUS_BADGE_COLORS.todo;
|
||||
};
|
||||
|
||||
// Configuration des couleurs pour les cartes de statistiques du dashboard
|
||||
export const DASHBOARD_STAT_COLORS = {
|
||||
total: {
|
||||
color: 'bg-blue-500',
|
||||
textColor: 'text-blue-600',
|
||||
progressColor: 'bg-blue-500',
|
||||
dotColor: 'bg-blue-500'
|
||||
},
|
||||
todo: {
|
||||
color: 'bg-gray-500',
|
||||
textColor: 'text-gray-600',
|
||||
progressColor: 'bg-gray-500',
|
||||
dotColor: 'bg-gray-500'
|
||||
},
|
||||
inProgress: {
|
||||
color: 'bg-orange-500',
|
||||
textColor: 'text-orange-600',
|
||||
progressColor: 'bg-orange-500',
|
||||
dotColor: 'bg-orange-500'
|
||||
},
|
||||
completed: {
|
||||
color: 'bg-green-500',
|
||||
textColor: 'text-green-600',
|
||||
progressColor: 'bg-green-500',
|
||||
dotColor: 'bg-green-500'
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Fonction pour récupérer les couleurs d'une stat du dashboard
|
||||
export const getDashboardStatColors = (statType: keyof typeof DASHBOARD_STAT_COLORS) => {
|
||||
return DASHBOARD_STAT_COLORS[statType];
|
||||
};
|
||||
362
src/lib/types.ts
Normal file
362
src/lib/types.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
// Types de base pour les tâches
|
||||
// Note: TaskStatus et TaskPriority sont maintenant gérés par la configuration centralisée dans lib/status-config.ts
|
||||
export type TaskStatus = 'backlog' | 'todo' | 'in_progress' | 'done' | 'cancelled' | 'freeze' | 'archived';
|
||||
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
||||
export type TaskSource = 'reminders' | 'jira' | 'manual';
|
||||
|
||||
// Interface centralisée pour les statistiques
|
||||
export interface TaskStats {
|
||||
total: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
backlog: number;
|
||||
cancelled: number;
|
||||
freeze: number;
|
||||
archived: number;
|
||||
completionRate: number;
|
||||
}
|
||||
|
||||
// Interface principale pour les tâches
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
source: TaskSource;
|
||||
sourceId?: string;
|
||||
tags: string[];
|
||||
dueDate?: Date;
|
||||
completedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
// Métadonnées Jira
|
||||
jiraProject?: string;
|
||||
jiraKey?: string;
|
||||
jiraType?: string; // Type de ticket Jira: Story, Task, Bug, Epic, etc.
|
||||
assignee?: string;
|
||||
}
|
||||
|
||||
// Interface pour les tags
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
isPinned?: boolean; // Tag pour objectifs principaux
|
||||
}
|
||||
|
||||
// Types pour les préférences utilisateur
|
||||
export interface KanbanFilters {
|
||||
search?: string;
|
||||
tags?: string[];
|
||||
priorities?: TaskPriority[];
|
||||
showCompleted?: boolean;
|
||||
sortBy?: string;
|
||||
// Filtres spécifiques Jira
|
||||
showJiraOnly?: boolean;
|
||||
hideJiraTasks?: boolean;
|
||||
jiraProjects?: string[];
|
||||
jiraTypes?: string[];
|
||||
[key: string]: string | string[] | TaskPriority[] | boolean | undefined;
|
||||
}
|
||||
|
||||
export interface ViewPreferences {
|
||||
compactView: boolean;
|
||||
swimlanesByTags: boolean;
|
||||
swimlanesMode?: 'tags' | 'priority';
|
||||
showObjectives: boolean;
|
||||
showFilters: boolean;
|
||||
objectivesCollapsed: boolean;
|
||||
theme: 'light' | 'dark';
|
||||
fontSize: 'small' | 'medium' | 'large';
|
||||
[key: string]: boolean | 'tags' | 'priority' | 'light' | 'dark' | 'small' | 'medium' | 'large' | undefined;
|
||||
}
|
||||
|
||||
export interface ColumnVisibility {
|
||||
hiddenStatuses: TaskStatus[];
|
||||
[key: string]: TaskStatus[] | undefined;
|
||||
}
|
||||
|
||||
export interface JiraConfig {
|
||||
baseUrl?: string;
|
||||
email?: string;
|
||||
apiToken?: string;
|
||||
enabled: boolean;
|
||||
projectKey?: string; // Clé du projet à surveiller pour les analytics d'équipe (ex: "MYTEAM")
|
||||
ignoredProjects?: string[]; // Liste des clés de projets à ignorer (ex: ["DEMO", "TEST"])
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
kanbanFilters: KanbanFilters;
|
||||
viewPreferences: ViewPreferences;
|
||||
columnVisibility: ColumnVisibility;
|
||||
jiraConfig: JiraConfig;
|
||||
}
|
||||
|
||||
// Interface pour les logs de synchronisation
|
||||
export interface SyncLog {
|
||||
id: string;
|
||||
source: TaskSource;
|
||||
status: 'success' | 'error';
|
||||
message?: string;
|
||||
tasksSync: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Types pour les rappels macOS
|
||||
export interface MacOSReminder {
|
||||
id: string;
|
||||
title: string;
|
||||
notes?: string;
|
||||
completed: boolean;
|
||||
dueDate?: Date;
|
||||
completionDate?: Date;
|
||||
priority: number; // 0=None, 1=Low, 5=Medium, 9=High
|
||||
list: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// Types pour Jira
|
||||
export interface JiraTask {
|
||||
id: string;
|
||||
key: string;
|
||||
summary: string;
|
||||
description?: string;
|
||||
status: {
|
||||
name: string;
|
||||
category: string;
|
||||
};
|
||||
priority?: {
|
||||
name: string;
|
||||
};
|
||||
assignee?: {
|
||||
displayName: string;
|
||||
emailAddress: string;
|
||||
};
|
||||
project: {
|
||||
key: string;
|
||||
name: string;
|
||||
};
|
||||
issuetype: {
|
||||
name: string; // Story, Task, Bug, Epic, etc.
|
||||
};
|
||||
components?: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
fixVersions?: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
}>;
|
||||
duedate?: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
// Types pour l'analytics Jira
|
||||
export interface JiraAnalytics {
|
||||
project: {
|
||||
key: string;
|
||||
name: string;
|
||||
totalIssues: number;
|
||||
};
|
||||
teamMetrics: {
|
||||
totalAssignees: number;
|
||||
activeAssignees: number;
|
||||
issuesDistribution: AssigneeDistribution[];
|
||||
};
|
||||
velocityMetrics: {
|
||||
currentSprintPoints: number;
|
||||
averageVelocity: number;
|
||||
sprintHistory: SprintVelocity[];
|
||||
};
|
||||
cycleTimeMetrics: {
|
||||
averageCycleTime: number; // en jours
|
||||
cycleTimeByType: CycleTimeByType[];
|
||||
};
|
||||
workInProgress: {
|
||||
byStatus: StatusDistribution[];
|
||||
byAssignee: AssigneeWorkload[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssigneeDistribution {
|
||||
assignee: string;
|
||||
displayName: string;
|
||||
totalIssues: number;
|
||||
completedIssues: number;
|
||||
inProgressIssues: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface SprintVelocity {
|
||||
sprintName: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
completedPoints: number;
|
||||
plannedPoints: number;
|
||||
completionRate: number;
|
||||
}
|
||||
|
||||
export interface CycleTimeByType {
|
||||
issueType: string;
|
||||
averageDays: number;
|
||||
medianDays: number;
|
||||
samples: number;
|
||||
}
|
||||
|
||||
export interface StatusDistribution {
|
||||
status: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface AssigneeWorkload {
|
||||
assignee: string;
|
||||
displayName: string;
|
||||
todoCount: number;
|
||||
inProgressCount: number;
|
||||
reviewCount: number;
|
||||
totalActive: number;
|
||||
}
|
||||
|
||||
// Types pour les filtres avancés
|
||||
export interface JiraAnalyticsFilters {
|
||||
components: string[];
|
||||
fixVersions: string[];
|
||||
issueTypes: string[];
|
||||
statuses: string[];
|
||||
assignees: string[];
|
||||
labels: string[];
|
||||
priorities: string[];
|
||||
dateRange?: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FilterOption {
|
||||
value: string;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface AvailableFilters {
|
||||
components: FilterOption[];
|
||||
fixVersions: FilterOption[];
|
||||
issueTypes: FilterOption[];
|
||||
statuses: FilterOption[];
|
||||
assignees: FilterOption[];
|
||||
labels: FilterOption[];
|
||||
priorities: FilterOption[];
|
||||
}
|
||||
|
||||
// Types pour l'API
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// Types pour les filtres
|
||||
export interface TaskFilters {
|
||||
status?: TaskStatus[];
|
||||
priority?: TaskPriority[];
|
||||
source?: TaskSource[];
|
||||
tags?: string[];
|
||||
assignee?: string;
|
||||
search?: string;
|
||||
dueDate?: {
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
};
|
||||
}
|
||||
|
||||
// Types pour les statistiques d'équipe
|
||||
export interface TeamStats {
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
inProgressTasks: number;
|
||||
velocity: number;
|
||||
burndownData: BurndownPoint[];
|
||||
memberStats: MemberStats[];
|
||||
}
|
||||
|
||||
export interface BurndownPoint {
|
||||
date: Date;
|
||||
remaining: number;
|
||||
completed: number;
|
||||
}
|
||||
|
||||
export interface MemberStats {
|
||||
name: string;
|
||||
email: string;
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
averageCompletionTime: number;
|
||||
}
|
||||
|
||||
// Types d'erreur
|
||||
export class BusinessError extends Error {
|
||||
constructor(message: string, public code?: string) {
|
||||
super(message);
|
||||
this.name = 'BusinessError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string, public field?: string) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
// Types pour les dailies
|
||||
export type DailyCheckboxType = 'task' | 'meeting';
|
||||
|
||||
export interface DailyCheckbox {
|
||||
id: string;
|
||||
date: Date;
|
||||
text: string;
|
||||
isChecked: boolean;
|
||||
type: DailyCheckboxType;
|
||||
order: number;
|
||||
taskId?: string;
|
||||
task?: Task; // Relation optionnelle vers une tâche
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Interface pour créer/modifier une checkbox
|
||||
export interface CreateDailyCheckboxData {
|
||||
date: Date;
|
||||
text: string;
|
||||
type?: DailyCheckboxType;
|
||||
taskId?: string;
|
||||
order?: number;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateDailyCheckboxData {
|
||||
text?: string;
|
||||
isChecked?: boolean;
|
||||
type?: DailyCheckboxType;
|
||||
taskId?: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
// Interface pour récupérer les checkboxes d'une journée
|
||||
export interface DailyView {
|
||||
date: Date;
|
||||
yesterday: DailyCheckbox[]; // Checkboxes de la veille
|
||||
today: DailyCheckbox[]; // Checkboxes du jour
|
||||
}
|
||||
9
src/lib/utils.ts
Normal file
9
src/lib/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Utility function to merge Tailwind CSS classes
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
83
src/lib/workday-utils.ts
Normal file
83
src/lib/workday-utils.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Utilitaires pour la gestion des jours de travail
|
||||
* Logique : Lundi-Vendredi sont les jours de travail
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calcule le jour de travail précédent selon la logique métier :
|
||||
* - Lundi → Vendredi (au lieu de Dimanche)
|
||||
* - Mardi-Vendredi → jour précédent
|
||||
* - Samedi → Vendredi
|
||||
* - Dimanche → Vendredi
|
||||
*/
|
||||
export function getPreviousWorkday(date: Date): Date {
|
||||
const result = new Date(date);
|
||||
result.setHours(0, 0, 0, 0);
|
||||
|
||||
const dayOfWeek = result.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi
|
||||
|
||||
switch (dayOfWeek) {
|
||||
case 1: // Lundi → Vendredi précédent
|
||||
result.setDate(result.getDate() - 3);
|
||||
break;
|
||||
case 0: // Dimanche → Vendredi précédent
|
||||
result.setDate(result.getDate() - 2);
|
||||
break;
|
||||
case 6: // Samedi → Vendredi précédent
|
||||
result.setDate(result.getDate() - 1);
|
||||
break;
|
||||
default: // Mardi-Vendredi → jour précédent
|
||||
result.setDate(result.getDate() - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le jour de travail suivant selon la logique métier :
|
||||
* - Vendredi → Lundi suivant
|
||||
* - Samedi → Lundi suivant
|
||||
* - Dimanche → Lundi suivant
|
||||
* - Lundi-Jeudi → jour suivant
|
||||
*/
|
||||
export function getNextWorkday(date: Date): Date {
|
||||
const result = new Date(date);
|
||||
result.setHours(0, 0, 0, 0);
|
||||
|
||||
const dayOfWeek = result.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi
|
||||
|
||||
switch (dayOfWeek) {
|
||||
case 5: // Vendredi → Lundi suivant
|
||||
result.setDate(result.getDate() + 3);
|
||||
break;
|
||||
case 6: // Samedi → Lundi suivant
|
||||
result.setDate(result.getDate() + 2);
|
||||
break;
|
||||
case 0: // Dimanche → Lundi suivant
|
||||
result.setDate(result.getDate() + 1);
|
||||
break;
|
||||
default: // Lundi-Jeudi → jour suivant
|
||||
result.setDate(result.getDate() + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une date est un jour de travail (Lundi-Vendredi)
|
||||
*/
|
||||
export function isWorkday(date: Date): boolean {
|
||||
const dayOfWeek = date.getDay();
|
||||
return dayOfWeek >= 1 && dayOfWeek <= 5; // Lundi (1) à Vendredi (5)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom du jour en français
|
||||
*/
|
||||
export function getDayName(date: Date): string {
|
||||
const days = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
|
||||
return days[date.getDay()];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user