chore: prettier everywhere

This commit is contained in:
Julien Froidefond
2025-10-09 13:40:03 +02:00
parent f8100ae3e9
commit d9cf9a2655
303 changed files with 15420 additions and 9391 deletions

View File

@@ -1,80 +1,83 @@
import { NextAuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { usersService } from "@/services/users"
import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { usersService } from '@/services/users';
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials",
name: 'credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
return null;
}
try {
// Chercher l'utilisateur dans la base de données
const user = await usersService.getUserByEmail(credentials.email)
const user = await usersService.getUserByEmail(credentials.email);
if (!user) {
return null
return null;
}
// Vérifier le mot de passe
const isValidPassword = await usersService.verifyPassword(
credentials.password,
user.password
)
);
if (!isValidPassword) {
return null
return null;
}
return {
id: user.id,
email: user.email,
name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.email,
name:
user.name ||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
user.email,
firstName: user.firstName || undefined,
lastName: user.lastName || undefined,
avatar: user.avatar || undefined,
role: user.role,
}
};
} catch (error) {
console.error('Auth error:', error)
return null
console.error('Auth error:', error);
return null;
}
}
})
},
}),
],
pages: {
signIn: "/login",
signIn: '/login',
},
session: {
strategy: "jwt",
strategy: 'jwt',
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.firstName = user.firstName
token.lastName = user.lastName
token.avatar = user.avatar
token.role = user.role
token.id = user.id;
token.firstName = user.firstName;
token.lastName = user.lastName;
token.avatar = user.avatar;
token.role = user.role;
}
return token
return token;
},
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string
session.user.firstName = token.firstName as string | undefined
session.user.lastName = token.lastName as string | undefined
session.user.avatar = token.avatar as string | undefined
session.user.role = token.role as string
session.user.id = token.id as string;
session.user.firstName = token.firstName as string | undefined;
session.user.lastName = token.lastName as string | undefined;
session.user.avatar = token.avatar as string | undefined;
session.user.role = token.role as string;
}
return session
return session;
},
},
}
};

View File

@@ -43,8 +43,8 @@ export class BackupUtils {
if (process.env.BACKUP_STORAGE_PATH) {
return path.resolve(process.cwd(), process.env.BACKUP_STORAGE_PATH);
}
return process.env.NODE_ENV === 'production'
return process.env.NODE_ENV === 'production'
? path.join(process.cwd(), 'data', 'backups')
: path.join(process.cwd(), 'backups');
}
@@ -52,14 +52,17 @@ export class BackupUtils {
/**
* Crée une sauvegarde SQLite en utilisant la commande .backup
*/
static async createSQLiteBackup(sourcePath: string, backupPath: string): Promise<void> {
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}'"`;
@@ -67,7 +70,10 @@ export class BackupUtils {
console.log(`✅ SQLite backup created using CLI: ${backupPath}`);
return;
} catch (cliError) {
console.warn(`⚠️ SQLite CLI backup failed, trying copy method:`, cliError);
console.warn(
`⚠️ SQLite CLI backup failed, trying copy method:`,
cliError
);
}
// Méthode 2: Copie simple du fichier (fallback)
@@ -84,7 +90,7 @@ export class BackupUtils {
*/
static async compressFile(filePath: string): Promise<string> {
const compressedPath = `${filePath}.gz`;
try {
const command = `gzip -c "${filePath}" > "${compressedPath}"`;
await execAsync(command);
@@ -99,7 +105,10 @@ export class BackupUtils {
/**
* Décompresse un fichier gzip temporairement
*/
static async decompressFileTemp(compressedPath: string, tempPath: string): Promise<void> {
static async decompressFileTemp(
compressedPath: string,
tempPath: string
): Promise<void> {
try {
await execAsync(`gunzip -c "${compressedPath}" > "${tempPath}"`);
} catch (error) {
@@ -114,12 +123,12 @@ export class BackupUtils {
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]}`;
}
@@ -138,31 +147,40 @@ export class BackupUtils {
/**
* Parse le nom de fichier de backup pour extraire les métadonnées
*/
static parseBackupFilename(filename: string): { type: 'manual' | 'automatic'; date: Date | null } {
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)/);
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)/);
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');
const isoString = dateMatch[2].replace(
/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/,
'T$1:$2:$3.$4Z'
);
date = parseDate(isoString);
}
return { type, date };
}
@@ -178,17 +196,17 @@ export class BackupUtils {
* Écrit une entrée dans le fichier de log
*/
static async writeLogEntry(
logPath: string,
type: 'manual' | 'automatic',
action: 'created' | 'skipped' | 'failed',
details: string,
logPath: string,
type: 'manual' | 'automatic',
action: 'created' | 'skipped' | 'failed',
details: string,
extra?: { hash?: string; size?: number; previousHash?: string }
): Promise<void> {
try {
const date = formatDateForDisplay(getToday(), 'DISPLAY_LONG');
let logEntry = `[${date}] ${type.toUpperCase()} BACKUP ${action.toUpperCase()}: ${details}`;
if (extra) {
if (extra.hash) {
logEntry += ` | Hash: ${extra.hash.substring(0, 12)}...`;
@@ -200,9 +218,9 @@ export class BackupUtils {
logEntry += ` | Previous: ${extra.previousHash.substring(0, 12)}...`;
}
}
logEntry += '\n';
await fs.appendFile(logPath, logEntry);
} catch (error) {
console.error('Error writing to backup log:', error);

View File

@@ -30,25 +30,32 @@ export interface AppConfig {
const defaultConfig: AppConfig = {
app: {
name: 'TowerControl',
version: '2.0.0'
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')
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'
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),
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
}
}
apiToken: process.env.JIRA_API_TOKEN,
},
},
};
/**
@@ -64,5 +71,5 @@ export function getConfig(): AppConfig {
export const DEBUG_CONFIG = {
isDevelopment: process.env.NODE_ENV === 'development',
verboseLogging: process.env.VERBOSE_LOGGING === 'true',
enableDevTools: process.env.NODE_ENV === 'development'
enableDevTools: process.env.NODE_ENV === 'development',
};

View File

@@ -3,21 +3,32 @@
* Regroupe toutes les fonctions de formatage, manipulation et validation de dates
*/
import { format, startOfDay, endOfDay, isValid, formatDistanceToNow as formatDistanceToNowFns } from 'date-fns';
import {
format,
startOfDay,
endOfDay,
isValid,
formatDistanceToNow as formatDistanceToNowFns,
} from 'date-fns';
import { fr } from 'date-fns/locale';
// Re-export des utilitaires workday existants
export { getPreviousWorkday, getNextWorkday, isWorkday, getDayName } from './workday-utils';
export {
getPreviousWorkday,
getNextWorkday,
isWorkday,
getDayName,
} from './workday-utils';
/**
* Formats de dates standardisés
*/
export const DATE_FORMATS = {
API: 'yyyy-MM-dd', // Format API (YYYY-MM-DD)
DISPLAY_SHORT: 'dd/MM/yy', // Format court (01/12/25)
API: 'yyyy-MM-dd', // Format API (YYYY-MM-DD)
DISPLAY_SHORT: 'dd/MM/yy', // Format court (01/12/25)
DISPLAY_LONG: 'EEEE d MMMM yyyy', // Format long (lundi 1 décembre 2025)
DISPLAY_MEDIUM: 'dd/MM/yyyy', // Format moyen (01/12/2025)
ISO: "yyyy-MM-dd'T'HH:mm:ss.SSSxxx" // Format ISO complet
ISO: "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", // Format ISO complet
} as const;
/**
@@ -39,7 +50,7 @@ export function formatDateForAPI(date: Date | string): string {
if (!ensuredDate || !isValid(ensuredDate)) {
throw new Error('Date invalide fournie à formatDateForAPI');
}
const year = ensuredDate.getFullYear();
const month = String(ensuredDate.getMonth() + 1).padStart(2, '0');
const day = String(ensuredDate.getDate()).padStart(2, '0');
@@ -49,12 +60,15 @@ export function formatDateForAPI(date: Date | string): string {
/**
* Formate une date pour l'affichage en français
*/
export function formatDateForDisplay(date: Date | string, formatType: keyof typeof DATE_FORMATS = 'DISPLAY_MEDIUM'): string {
export function formatDateForDisplay(
date: Date | string,
formatType: keyof typeof DATE_FORMATS = 'DISPLAY_MEDIUM'
): string {
const ensuredDate = ensureDate(date);
if (!ensuredDate || !isValid(ensuredDate)) {
throw new Error('Date invalide fournie à formatDateForDisplay');
}
return format(ensuredDate, DATE_FORMATS[formatType], { locale: fr });
}
@@ -73,7 +87,7 @@ export function getDaysAgo(date: Date | string): number {
if (!ensuredDate) {
throw new Error('Date invalide fournie à getDaysAgo');
}
const today = getToday();
const diffTime = today.getTime() - normalizeDate(ensuredDate).getTime();
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
@@ -92,9 +106,11 @@ export function formatDateLong(date: Date): string {
export function isToday(date: Date | string): boolean {
const ensuredDate = ensureDate(date);
if (!ensuredDate) return false;
const today = new Date();
return normalizeDate(ensuredDate).getTime() === normalizeDate(today).getTime();
return (
normalizeDate(ensuredDate).getTime() === normalizeDate(today).getTime()
);
}
/**
@@ -103,10 +119,12 @@ export function isToday(date: Date | string): boolean {
export function isYesterday(date: Date | string): boolean {
const ensuredDate = ensureDate(date);
if (!ensuredDate) return false;
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return normalizeDate(ensuredDate).getTime() === normalizeDate(yesterday).getTime();
return (
normalizeDate(ensuredDate).getTime() === normalizeDate(yesterday).getTime()
);
}
/**
@@ -115,10 +133,13 @@ export function isYesterday(date: Date | string): boolean {
export function isSameDay(date1: Date | string, date2: Date | string): boolean {
const ensuredDate1 = ensureDate(date1);
const ensuredDate2 = ensureDate(date2);
if (!ensuredDate1 || !ensuredDate2) return false;
return normalizeDate(ensuredDate1).getTime() === normalizeDate(ensuredDate2).getTime();
return (
normalizeDate(ensuredDate1).getTime() ===
normalizeDate(ensuredDate2).getTime()
);
}
/**
@@ -147,7 +168,11 @@ export function createDate(date: Date): Date {
/**
* Crée une date à partir de composants année/mois/jour
*/
export function createDateFromParts(year: number, month: number, day: number): Date {
export function createDateFromParts(
year: number,
month: number,
day: number
): Date {
return new Date(year, month - 1, day); // month est 0-indexé en JavaScript
}
@@ -155,7 +180,9 @@ export function createDateFromParts(year: number, month: number, day: number): D
* Convertit une date pour un input datetime-local (gestion timezone)
*/
export function formatDateForDateTimeInput(date: Date): string {
const adjustedDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
const adjustedDate = new Date(
date.getTime() - date.getTimezoneOffset() * 60000
);
return adjustedDate.toISOString().slice(0, 16);
}
@@ -210,7 +237,7 @@ export function isValidAPIDate(dateString: string): boolean {
if (!regex.test(dateString)) {
return false;
}
try {
const date = parseDate(dateString);
return formatDateForAPI(date) === dateString;
@@ -240,40 +267,46 @@ export function formatDateSmart(date: Date): string {
if (isToday(date)) {
return "Aujourd'hui";
}
if (isYesterday(date)) {
return "Hier";
return 'Hier';
}
return formatDateForDisplay(date, 'DISPLAY_MEDIUM');
}
/**
* Génère un titre intelligent pour une date (avec emojis)
*/
export function generateDateTitle(date: Date, emoji: string = '📅'): { emoji: string; text: string } {
export function generateDateTitle(
date: Date,
emoji: string = '📅'
): { emoji: string; text: string } {
if (isToday(date)) {
return { emoji, text: 'Aujourd\'hui' };
return { emoji, text: "Aujourd'hui" };
}
if (isYesterday(date)) {
return { emoji, text: 'Hier' };
}
return { emoji, text: formatDateShort(date) };
}
/**
* Formate la distance depuis maintenant en français
*/
export function formatDistanceToNow(date: Date, options?: { addSuffix?: boolean }): string {
export function formatDistanceToNow(
date: Date,
options?: { addSuffix?: boolean }
): string {
if (!isValid(date)) {
throw new Error('Date invalide fournie à formatDistanceToNow');
}
return formatDistanceToNowFns(date, {
locale: fr,
addSuffix: options?.addSuffix ?? true
addSuffix: options?.addSuffix ?? true,
});
}
@@ -281,16 +314,18 @@ export function formatDistanceToNow(date: Date, options?: { addSuffix?: boolean
* S'assure qu'une valeur est un objet Date valide
* Convertit les chaînes de caractères en Date si nécessaire
*/
export function ensureDate(value: Date | string | null | undefined): Date | null {
export function ensureDate(
value: Date | string | null | undefined
): Date | null {
if (value === null || value === undefined) {
return null;
}
// Si c'est déjà un objet Date valide
if (value instanceof Date && isValid(value)) {
return value;
}
// Si c'est une chaîne de caractères, essayer de la parser
if (typeof value === 'string') {
try {
@@ -301,7 +336,7 @@ export function ensureDate(value: Date | string | null | undefined): Date | null
return null;
}
}
// Si c'est un objet Date mais invalide, essayer de le recréer
if (value instanceof Date) {
try {
@@ -313,7 +348,7 @@ export function ensureDate(value: Date | string | null | undefined): Date | null
console.warn('Objet Date invalide détecté');
}
}
console.warn('Valeur de date non supportée:', value);
return null;
}
@@ -321,7 +356,10 @@ export function ensureDate(value: Date | string | null | undefined): Date | null
/**
* S'assure qu'une valeur est un objet Date valide, avec une valeur par défaut
*/
export function ensureDateWithDefault(value: Date | string | null | undefined, defaultValue: Date = new Date()): Date {
export function ensureDateWithDefault(
value: Date | string | null | undefined,
defaultValue: Date = new Date()
): Date {
const result = ensureDate(value);
return result || defaultValue;
}

View File

@@ -7,7 +7,7 @@ export type PeriodFilter = '7d' | '30d' | '3m' | 'current';
* Filtre les analytics Jira selon la période sélectionnée
*/
export function filterAnalyticsByPeriod(
analytics: JiraAnalytics,
analytics: JiraAnalytics,
period: PeriodFilter
): JiraAnalytics {
const now = getToday();
@@ -39,37 +39,50 @@ export function filterAnalyticsByPeriod(
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
}
averageVelocity:
currentSprint.length > 0 ? currentSprint[0].completedPoints : 0,
},
};
}
/**
* Filtre les analytics par date de cutoff
*/
function filterAnalyticsByDate(analytics: JiraAnalytics, cutoffDate: Date): JiraAnalytics {
function filterAnalyticsByDate(
analytics: JiraAnalytics,
cutoffDate: Date
): JiraAnalytics {
// Filtrer l'historique des sprints
const filteredSprintHistory = analytics.velocityMetrics.sprintHistory.filter(sprint => {
const sprintEndDate = parseDate(sprint.endDate);
return sprintEndDate >= cutoffDate;
});
const filteredSprintHistory = analytics.velocityMetrics.sprintHistory.filter(
(sprint) => {
const sprintEndDate = parseDate(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);
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;
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
@@ -78,8 +91,8 @@ function filterAnalyticsByDate(analytics: JiraAnalytics, cutoffDate: Date): Jira
velocityMetrics: {
...analytics.velocityMetrics,
sprintHistory,
averageVelocity
}
averageVelocity,
},
};
}
@@ -114,31 +127,31 @@ export function getPeriodInfo(period: PeriodFilter): {
return {
label: 'Derniers 7 jours',
description: 'Vue hebdomadaire des métriques',
icon: '📅'
icon: '📅',
};
case '30d':
return {
label: 'Derniers 30 jours',
description: 'Vue mensuelle des métriques',
icon: '📊'
icon: '📊',
};
case '3m':
return {
label: 'Derniers 3 mois',
description: 'Vue trimestrielle des métriques',
icon: '📈'
icon: '📈',
};
case 'current':
return {
label: 'Sprint actuel',
description: 'Focus sur le sprint en cours',
icon: '🎯'
icon: '🎯',
};
default:
return {
label: 'Période inconnue',
description: '',
icon: '❓'
icon: '❓',
};
}
}

View File

@@ -1,7 +1,13 @@
import { Task, TaskPriority } from './types';
import { getPriorityConfig } from './status-config';
export type SortField = 'priority' | 'tags' | 'createdAt' | 'updatedAt' | 'dueDate' | 'title';
export type SortField =
| 'priority'
| 'tags'
| 'createdAt'
| 'updatedAt'
| 'dueDate'
| 'title';
export type SortDirection = 'asc' | 'desc';
export interface SortConfig {
@@ -24,70 +30,70 @@ export const SORT_OPTIONS: SortOption[] = [
label: 'Priorité (Urgente → Faible)',
field: 'priority',
direction: 'desc',
iconName: 'flame'
iconName: 'flame',
},
{
key: 'priority-asc',
label: 'Priorité (Faible → Urgente)',
field: 'priority',
direction: 'asc',
iconName: 'circle'
iconName: 'circle',
},
{
key: 'tags-asc',
label: 'Tags (A → Z)',
field: 'tags',
direction: 'asc',
iconName: 'tag'
iconName: 'tag',
},
{
key: 'title-asc',
label: 'Titre (A → Z)',
field: 'title',
direction: 'asc',
iconName: 'file-text'
iconName: 'file-text',
},
{
key: 'title-desc',
label: 'Titre (Z → A)',
field: 'title',
direction: 'desc',
iconName: 'file-text'
iconName: 'file-text',
},
{
key: 'createdAt-desc',
label: 'Date création (Récent → Ancien)',
field: 'createdAt',
direction: 'desc',
iconName: 'calendar'
iconName: 'calendar',
},
{
key: 'createdAt-asc',
label: 'Date création (Ancien → Récent)',
field: 'createdAt',
direction: 'asc',
iconName: 'calendar'
iconName: 'calendar',
},
{
key: 'dueDate-asc',
label: 'Échéance (Proche → Lointaine)',
field: 'dueDate',
direction: 'asc',
iconName: 'clock'
iconName: 'clock',
},
{
key: 'dueDate-desc',
label: 'Échéance (Lointaine → Proche)',
field: 'dueDate',
direction: 'desc',
iconName: 'clock'
}
iconName: 'clock',
},
];
// Tri par défaut : Priorité (desc) puis Tags (asc)
export const DEFAULT_SORT: SortConfig[] = [
{ field: 'priority', direction: 'desc' },
{ field: 'tags', direction: 'asc' }
{ field: 'tags', direction: 'asc' },
];
/**
@@ -97,7 +103,7 @@ 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;
}
@@ -108,7 +114,9 @@ function compareValues<T>(a: T, b: T, direction: SortDirection): number {
function getPriorityValue(priority: TaskPriority): number {
const config = getPriorityConfig(priority);
if (!config) {
console.warn(`⚠️ Priorité inconnue: ${priority}, utilisation de 'medium' par défaut`);
console.warn(
`⚠️ Priorité inconnue: ${priority}, utilisation de 'medium' par défaut`
);
return getPriorityConfig('medium').order;
}
return config.order;
@@ -126,7 +134,7 @@ function getFirstTag(task: Task): string {
*/
function compareTasksByField(a: Task, b: Task, sortConfig: SortConfig): number {
const { field, direction } = sortConfig;
switch (field) {
case 'priority':
return compareValues(
@@ -134,42 +142,38 @@ function compareTasksByField(a: Task, b: Task, sortConfig: SortConfig): number {
getPriorityValue(b.priority),
direction
);
case 'tags':
return compareValues(
getFirstTag(a),
getFirstTag(b),
direction
);
return compareValues(getFirstTag(a), getFirstTag(b), direction);
case 'title':
return compareValues(
a.title.toLowerCase(),
b.title.toLowerCase(),
direction
);
case 'createdAt':
return compareValues(
a.createdAt.getTime(),
b.createdAt.getTime(),
direction
);
case 'updatedAt':
return compareValues(
a.updatedAt.getTime(),
b.updatedAt.getTime(),
direction
);
case 'dueDate':
return compareValues(
a.dueDate ? a.dueDate.getTime() : null,
b.dueDate ? b.dueDate.getTime() : null,
direction
);
default:
return 0;
}
@@ -178,7 +182,10 @@ function compareTasksByField(a: Task, b: Task, sortConfig: SortConfig): number {
/**
* Trie un tableau de tâches selon une configuration de tri multiple
*/
export function sortTasks(tasks: Task[], sortConfigs: SortConfig[] = DEFAULT_SORT): Task[] {
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);
@@ -194,12 +201,15 @@ export function sortTasks(tasks: Task[], sortConfigs: SortConfig[] = DEFAULT_SOR
* 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);
return SORT_OPTIONS.find((option) => option.key === key);
}
/**
* Utilitaire pour créer une clé de tri
*/
export function createSortKey(field: SortField, direction: SortDirection): string {
export function createSortKey(
field: SortField,
direction: SortDirection
): string {
return `${field}-${direction}`;
}

View File

@@ -14,50 +14,50 @@ export const STATUS_CONFIG: Record<TaskStatus, StatusConfig> = {
label: 'Backlog',
icon: '📋',
color: 'gray',
order: 0
order: 0,
},
todo: {
key: 'todo',
label: 'À faire',
icon: '⚡',
color: 'gray',
order: 1
order: 1,
},
in_progress: {
key: 'in_progress',
label: 'En cours',
icon: '⚙️',
color: 'blue',
order: 2
order: 2,
},
freeze: {
key: 'freeze',
label: 'Gelé',
icon: '🧊',
color: 'purple',
order: 3
order: 3,
},
done: {
key: 'done',
label: 'Terminé',
icon: '✓',
color: 'green',
order: 4
order: 4,
},
cancelled: {
key: 'cancelled',
label: 'Annulé',
icon: '✕',
color: 'red',
order: 5
order: 5,
},
archived: {
key: 'archived',
label: 'Archivé',
icon: '📦',
color: 'gray',
order: 6
}
order: 6,
},
} as const;
// Utilitaires pour récupérer facilement les infos
@@ -87,45 +87,51 @@ export const TECH_STYLES = {
border: 'border-slate-700',
glow: 'shadow-slate-500/20',
accent: 'text-slate-400',
badge: 'bg-slate-800 text-slate-300 border border-slate-600'
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'
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'
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'
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'
}
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' => {
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';
case 'green':
return 'success';
case 'blue':
case 'purple':
return 'primary';
case 'red':
return 'danger';
default:
return 'default';
}
};
@@ -144,29 +150,29 @@ export const PRIORITY_CONFIG: Record<TaskPriority, PriorityConfig> = {
label: 'Faible',
icon: '🔵',
color: 'blue',
order: 1
order: 1,
},
medium: {
key: 'medium',
label: 'Moyenne',
icon: '🟡',
color: 'yellow',
order: 2
order: 2,
},
high: {
key: 'high',
label: 'Élevée',
icon: '🟣',
color: 'purple',
order: 3
order: 3,
},
urgent: {
key: 'urgent',
label: 'Urgente',
icon: '🔴',
color: 'red',
order: 4
}
order: 4,
},
} as const;
// Utilitaires pour les priorités
@@ -186,25 +192,27 @@ export const getPriorityIcon = (priority: TaskPriority): string => {
return PRIORITY_CONFIG[priority].icon;
};
export const getPriorityColor = (priority: TaskPriority): PriorityConfig['color'] => {
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)
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
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 => {
@@ -213,18 +221,22 @@ export const getPriorityColorHex = (color: PriorityConfig['color']): string => {
// 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'];
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',
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'
archived: 'bg-gray-100 text-gray-600',
} as const;
// Fonction pour récupérer les classes CSS d'un badge de statut
@@ -238,29 +250,31 @@ export const DASHBOARD_STAT_COLORS = {
color: 'bg-blue-500',
textColor: 'text-blue-600',
progressColor: 'bg-blue-500',
dotColor: 'bg-blue-500'
dotColor: 'bg-blue-500',
},
todo: {
color: 'bg-gray-500',
color: 'bg-gray-500',
textColor: 'text-gray-600',
progressColor: 'bg-gray-500',
dotColor: 'bg-gray-500'
dotColor: 'bg-gray-500',
},
inProgress: {
color: 'bg-orange-500',
textColor: 'text-orange-600',
textColor: 'text-orange-600',
progressColor: 'bg-orange-500',
dotColor: 'bg-orange-500'
dotColor: 'bg-orange-500',
},
completed: {
color: 'bg-green-500',
textColor: 'text-green-600',
progressColor: 'bg-green-500',
dotColor: 'bg-green-500'
}
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) => {
export const getDashboardStatColors = (
statType: keyof typeof DASHBOARD_STAT_COLORS
) => {
return DASHBOARD_STAT_COLORS[statType];
};

View File

@@ -31,7 +31,7 @@ export function generateTagColor(tagName: string): string {
for (let i = 0; i < tagName.length; i++) {
hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
}
return TAG_COLORS[Math.abs(hash) % TAG_COLORS.length];
}
@@ -62,6 +62,8 @@ export function getTagColor(tagName: string, existingColor?: string): string {
/**
* Génère un tableau de couleurs pour les graphiques en respectant les couleurs des tags
*/
export function generateChartColors(tags: Array<{ name: string; color?: string }>): string[] {
return tags.map(tag => getTagColor(tag.name, tag.color));
export function generateChartColors(
tags: Array<{ name: string; color?: string }>
): string[] {
return tags.map((tag) => getTagColor(tag.name, tag.color));
}

View File

@@ -139,7 +139,6 @@ export interface JiraConfig {
ignoredProjects?: string[]; // Liste des clés de projets à ignorer (ex: ["DEMO", "TEST"])
}
export interface UserPreferences {
kanbanFilters: KanbanFilters;
viewPreferences: ViewPreferences;

View File

@@ -1,20 +1,32 @@
// ===== CONFIGURATION DES THÈMES =====
// Types de thèmes
export type Theme = 'light' | 'dark' | 'dracula' | 'monokai' | 'nord' | 'gruvbox' | 'tokyo_night' | 'catppuccin' | 'rose_pine' | 'one_dark' | 'material' | 'solarized';
export type Theme =
| 'light'
| 'dark'
| 'dracula'
| 'monokai'
| 'nord'
| 'gruvbox'
| 'tokyo_night'
| 'catppuccin'
| 'rose_pine'
| 'one_dark'
| 'material'
| 'solarized';
// Configuration des thèmes
export const THEME_CONFIG = {
// Thème par défaut
default: 'dark' as Theme,
// Thème light
light: 'light' as Theme,
// Liste de tous les thèmes dark disponibles
darkThemes: [
'dark',
'dracula',
'dracula',
'monokai',
'nord',
'gruvbox',
@@ -23,14 +35,14 @@ export const THEME_CONFIG = {
'rose_pine',
'one_dark',
'material',
'solarized'
'solarized',
] as Theme[],
// Tous les thèmes disponibles
allThemes: [
'light',
'dark',
'dracula',
'dracula',
'monokai',
'nord',
'gruvbox',
@@ -39,24 +51,63 @@ export const THEME_CONFIG = {
'rose_pine',
'one_dark',
'material',
'solarized'
] as Theme[]
'solarized',
] as Theme[],
} as const;
// Métadonnées des thèmes pour l'affichage
export const THEME_METADATA: Record<Theme, { name: string; description: string; icon: string }> = {
export const THEME_METADATA: Record<
Theme,
{ name: string; description: string; icon: string }
> = {
light: { name: 'Light', description: 'Thème clair par défaut', icon: '☀️' },
dark: { name: 'Dark', description: 'Thème sombre classique', icon: '🌙' },
dracula: { name: 'Dracula', description: 'Inspiré du thème Dracula', icon: '🧛' },
monokai: { name: 'Monokai', description: 'Inspiré du thème Monokai', icon: '🎨' },
dracula: {
name: 'Dracula',
description: 'Inspiré du thème Dracula',
icon: '🧛',
},
monokai: {
name: 'Monokai',
description: 'Inspiré du thème Monokai',
icon: '🎨',
},
nord: { name: 'Nord', description: 'Palette Nord arctique', icon: '❄️' },
gruvbox: { name: 'Gruvbox', description: 'Palette Gruvbox retro', icon: '🎭' },
tokyo_night: { name: 'Tokyo Night', description: 'Nuit tokyoïte', icon: '🌃' },
catppuccin: { name: 'Catppuccin', description: 'Palette pastel douce', icon: '🐱' },
rose_pine: { name: 'Rose Pine', description: 'Palette rose et pin', icon: '🌹' },
one_dark: { name: 'One Dark', description: 'Inspiré d\'Atom One Dark', icon: '🌑' },
material: { name: 'Material', description: 'Inspiré de Material Design', icon: '📱' },
solarized: { name: 'Solarized', description: 'Palette Solarized', icon: '💊' }
gruvbox: {
name: 'Gruvbox',
description: 'Palette Gruvbox retro',
icon: '🎭',
},
tokyo_night: {
name: 'Tokyo Night',
description: 'Nuit tokyoïte',
icon: '🌃',
},
catppuccin: {
name: 'Catppuccin',
description: 'Palette pastel douce',
icon: '🐱',
},
rose_pine: {
name: 'Rose Pine',
description: 'Palette rose et pin',
icon: '🌹',
},
one_dark: {
name: 'One Dark',
description: "Inspiré d'Atom One Dark",
icon: '🌑',
},
material: {
name: 'Material',
description: 'Inspiré de Material Design',
icon: '📱',
},
solarized: {
name: 'Solarized',
description: 'Palette Solarized',
icon: '💊',
},
};
// Fonctions utilitaires pour les thèmes
@@ -74,7 +125,13 @@ export const isDarkTheme = (theme: Theme): boolean => {
};
export const getThemeMetadata = (theme: Theme) => {
return THEME_METADATA[theme] || { name: theme, description: 'Thème personnalisé', icon: '🎨' };
return (
THEME_METADATA[theme] || {
name: theme,
description: 'Thème personnalisé',
icon: '🎨',
}
);
};
// ===== CONFIGURATION DES BACKGROUNDS =====
@@ -85,118 +142,134 @@ export const PRESET_BACKGROUNDS = [
id: 'none',
name: 'Aucun fond',
description: 'Fond par défaut du système',
preview: 'var(--background)'
preview: 'var(--background)',
},
{
id: 'theme-subtle',
name: 'Dégradé subtil',
description: 'Dégradé très léger et discret',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--primary) 8%, var(--background)) 0%, color-mix(in srgb, var(--accent) 5%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--primary) 8%, var(--background)) 0%, color-mix(in srgb, var(--accent) 5%, var(--background)) 100%)',
},
{
id: 'theme-primary',
name: 'Dégradé primaire',
description: 'Dégradé avec la couleur primaire',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--primary) 25%, var(--background)) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--primary) 25%, var(--background)) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)',
},
{
id: 'theme-accent',
name: 'Dégradé accent',
description: 'Dégradé avec la couleur accent',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 25%, var(--background)) 0%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--accent) 25%, var(--background)) 0%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)',
},
{
id: 'theme-success',
name: 'Dégradé vert',
description: 'Dégradé avec la couleur de succès',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--success) 25%, var(--background)) 0%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--success) 25%, var(--background)) 0%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)',
},
{
id: 'theme-purple',
name: 'Dégradé violet',
description: 'Dégradé avec la couleur violette',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--purple) 15%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--purple) 15%, var(--background)) 100%)',
},
{
id: 'theme-diagonal',
name: 'Dégradé diagonal',
description: 'Dégradé diagonal dynamique',
preview: 'linear-gradient(45deg, color-mix(in srgb, var(--primary) 20%, var(--background)) 0%, color-mix(in srgb, var(--accent) 20%, var(--background)) 50%, color-mix(in srgb, var(--success) 20%, var(--background)) 100%)'
preview:
'linear-gradient(45deg, color-mix(in srgb, var(--primary) 20%, var(--background)) 0%, color-mix(in srgb, var(--accent) 20%, var(--background)) 50%, color-mix(in srgb, var(--success) 20%, var(--background)) 100%)',
},
{
id: 'theme-radial',
name: 'Dégradé radial',
description: 'Dégradé radial depuis le centre',
preview: 'radial-gradient(circle, color-mix(in srgb, var(--primary) 20%, var(--background)) 0%, color-mix(in srgb, var(--accent) 10%, var(--background)) 70%, var(--background) 100%)'
preview:
'radial-gradient(circle, color-mix(in srgb, var(--primary) 20%, var(--background)) 0%, color-mix(in srgb, var(--accent) 10%, var(--background)) 70%, var(--background) 100%)',
},
{
id: 'theme-sunset',
name: 'Dégradé coucher',
description: 'Dégradé inspiré du coucher de soleil',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 25%, var(--background)) 0%, color-mix(in srgb, var(--purple) 20%, var(--background)) 50%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--accent) 25%, var(--background)) 0%, color-mix(in srgb, var(--purple) 20%, var(--background)) 50%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)',
},
{
id: 'theme-ocean',
name: 'Dégradé océan',
description: 'Dégradé inspiré de l\'océan',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--primary) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 50%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)'
description: "Dégradé inspiré de l'océan",
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--primary) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 50%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)',
},
{
id: 'theme-forest',
name: 'Dégradé forêt',
description: 'Dégradé inspiré de la forêt',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--success) 25%, var(--background)) 0%, color-mix(in srgb, var(--green) 20%, var(--background)) 50%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)'
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--success) 25%, var(--background)) 0%, color-mix(in srgb, var(--green) 20%, var(--background)) 50%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)',
},
{
id: 'theme-galaxy',
name: 'Dégradé galaxie',
description: 'Dégradé inspiré de la galaxie',
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 100%)'
}
preview:
'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 100%)',
},
] as const;
// Liste des backgrounds pour le cycle (IDs seulement)
export const BACKGROUND_CYCLE = PRESET_BACKGROUNDS.map(bg => bg.id);
export const BACKGROUND_CYCLE = PRESET_BACKGROUNDS.map((bg) => bg.id);
// Types pour les backgrounds
export type BackgroundId = typeof PRESET_BACKGROUNDS[number]['id'];
export type BackgroundId = (typeof PRESET_BACKGROUNDS)[number]['id'];
// Fonctions utilitaires pour les backgrounds
export const getBackgroundById = (id: BackgroundId) => {
return PRESET_BACKGROUNDS.find(bg => bg.id === id);
return PRESET_BACKGROUNDS.find((bg) => bg.id === id);
};
export const isPresetBackground = (id: string): id is BackgroundId => {
return PRESET_BACKGROUNDS.some(bg => bg.id === id);
return PRESET_BACKGROUNDS.some((bg) => bg.id === id);
};
export const getNextBackground = (currentBackground: string, customImages: string[] = []): string => {
export const getNextBackground = (
currentBackground: string,
customImages: string[] = []
): string => {
const allBackgrounds = [...BACKGROUND_CYCLE];
// Ajouter toutes les images personnalisées
customImages.forEach(url => {
if (!allBackgrounds.includes(url as typeof BACKGROUND_CYCLE[number])) {
allBackgrounds.push(url as typeof BACKGROUND_CYCLE[number]);
customImages.forEach((url) => {
if (!allBackgrounds.includes(url as (typeof BACKGROUND_CYCLE)[number])) {
allBackgrounds.push(url as (typeof BACKGROUND_CYCLE)[number]);
}
});
const currentIndex = allBackgrounds.findIndex(bg => bg === currentBackground);
const currentIndex = allBackgrounds.findIndex(
(bg) => bg === currentBackground
);
// Si on ne trouve pas l'index, c'est qu'on est sur "none" (undefined)
let actualCurrentIndex = currentIndex;
if (currentIndex === -1) {
actualCurrentIndex = -1; // On est sur "none", on va commencer à l'index 0
}
const nextIndex = (actualCurrentIndex + 1) % allBackgrounds.length;
const nextBackground = allBackgrounds[nextIndex];
// Si on est sur "none" (undefined) et qu'on va vers "none", on va vers le suivant
if (currentBackground === undefined && nextBackground === 'none') {
const nextNextIndex = (nextIndex + 1) % allBackgrounds.length;
return allBackgrounds[nextNextIndex];
}
return nextBackground;
};
@@ -204,7 +277,7 @@ export const getNextBackground = (currentBackground: string, customImages: strin
// Mapping des noms de backgrounds pour l'affichage
export const BACKGROUND_NAMES: Record<string, string> = {
'none': 'Aucun fond',
none: 'Aucun fond',
'theme-subtle': 'Dégradé subtil',
'theme-primary': 'Dégradé primaire',
'theme-accent': 'Dégradé accent',
@@ -215,23 +288,23 @@ export const BACKGROUND_NAMES: Record<string, string> = {
'theme-sunset': 'Dégradé coucher',
'theme-ocean': 'Dégradé océan',
'theme-forest': 'Dégradé forêt',
'theme-galaxy': 'Dégradé galaxie'
'theme-galaxy': 'Dégradé galaxie',
};
// Mapping des noms de thèmes pour l'affichage
export const THEME_NAMES: Record<Theme, string> = {
'light': 'Thème clair',
'dark': 'Thème sombre',
'dracula': 'Thème Dracula',
'monokai': 'Thème Monokai',
'nord': 'Thème Nord',
'gruvbox': 'Thème Gruvbox',
'tokyo_night': 'Thème Tokyo Night',
'catppuccin': 'Thème Catppuccin',
'rose_pine': 'Thème Rose Pine',
'one_dark': 'Thème One Dark',
'material': 'Thème Material',
'solarized': 'Thème Solarized'
light: 'Thème clair',
dark: 'Thème sombre',
dracula: 'Thème Dracula',
monokai: 'Thème Monokai',
nord: 'Thème Nord',
gruvbox: 'Thème Gruvbox',
tokyo_night: 'Thème Tokyo Night',
catppuccin: 'Thème Catppuccin',
rose_pine: 'Thème Rose Pine',
one_dark: 'Thème One Dark',
material: 'Thème Material',
solarized: 'Thème Solarized',
};
// Fonctions utilitaires pour les métadonnées
@@ -250,5 +323,5 @@ export const getThemeDescription = (theme: Theme): string => {
// Icônes pour les toasts
export const TOAST_ICONS = {
background: '🎨',
theme: '🎭'
theme: '🎭',
} as const;

View File

@@ -7,20 +7,20 @@
* 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
* - 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
case 0: // Dimanche → Vendredi précédent
result.setDate(result.getDate() - 2);
break;
case 6: // Samedi → Vendredi précédent
@@ -30,7 +30,7 @@ export function getPreviousWorkday(date: Date): Date {
result.setDate(result.getDate() - 1);
break;
}
return result;
}
@@ -38,15 +38,15 @@ export function getPreviousWorkday(date: Date): Date {
* Calcule le jour de travail suivant selon la logique métier :
* - Vendredi → Lundi suivant
* - Samedi → Lundi suivant
* - Dimanche → 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);
@@ -61,7 +61,7 @@ export function getNextWorkday(date: Date): Date {
result.setDate(result.getDate() + 1);
break;
}
return result;
}
@@ -77,7 +77,14 @@ export function isWorkday(date: Date): boolean {
* Retourne le nom du jour en français
*/
export function getDayName(date: Date): string {
const days = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
const days = [
'Dimanche',
'Lundi',
'Mardi',
'Mercredi',
'Jeudi',
'Vendredi',
'Samedi',
];
return days[date.getDay()];
}