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:
Julien Froidefond
2025-09-21 10:26:35 +02:00
parent 9dc1fafa76
commit 4152b0bdfc
130 changed files with 360 additions and 413 deletions

211
src/lib/backup-utils.ts Normal file
View 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
View 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'
};

View 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
View 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
View 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
View 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
View 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
View 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()];
}