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

View File

@@ -0,0 +1,114 @@
import { httpClient } from './base/http-client';
import { BackupInfo, BackupConfig } from '@/services/backup';
export interface BackupListResponse {
backups: BackupInfo[];
scheduler: {
isRunning: boolean;
isEnabled: boolean;
interval: string;
nextBackup: string | null;
maxBackups: number;
backupPath: string;
};
config: BackupConfig;
}
export class BackupClient {
private baseUrl = '/backups';
/**
* Liste toutes les sauvegardes disponibles et l'état du scheduler
*/
async listBackups(): Promise<BackupListResponse> {
const response = await httpClient.get<{ data: BackupListResponse }>(this.baseUrl);
return response.data;
}
/**
* Crée une nouvelle sauvegarde manuelle
*/
async createBackup(force: boolean = false): Promise<BackupInfo | null> {
const response = await httpClient.post<{ data?: BackupInfo; skipped?: boolean; message?: string }>(this.baseUrl, {
action: 'create',
force
});
if (response.skipped) {
return null; // Backup was skipped
}
return response.data!;
}
/**
* Vérifie l'intégrité de la base de données
*/
async verifyDatabase(): Promise<void> {
await httpClient.post(this.baseUrl, {
action: 'verify'
});
}
/**
* Met à jour la configuration des sauvegardes
*/
async updateConfig(config: Partial<BackupConfig>): Promise<BackupConfig> {
const response = await httpClient.post<{ data: BackupConfig }>(this.baseUrl, {
action: 'config',
config
});
return response.data;
}
/**
* Démarre ou arrête le planificateur automatique
*/
async toggleScheduler(enabled: boolean): Promise<{
isRunning: boolean;
isEnabled: boolean;
interval: string;
nextBackup: string | null;
maxBackups: number;
backupPath: string;
}> {
const response = await httpClient.post<{ data: {
isRunning: boolean;
isEnabled: boolean;
interval: string;
nextBackup: string | null;
maxBackups: number;
backupPath: string;
} }>(this.baseUrl, {
action: 'scheduler',
enabled
});
return response.data;
}
/**
* Supprime une sauvegarde
*/
async deleteBackup(filename: string): Promise<void> {
await httpClient.delete(`${this.baseUrl}/${filename}`);
}
/**
* Restaure une sauvegarde (développement uniquement)
*/
async restoreBackup(filename: string): Promise<void> {
await httpClient.post(`${this.baseUrl}/${filename}`, {
action: 'restore'
});
}
/**
* Récupère les logs de backup
*/
async getBackupLogs(maxLines: number = 100): Promise<string[]> {
const response = await httpClient.get<{ data: { logs: string[] } }>(`${this.baseUrl}?action=logs&maxLines=${maxLines}`);
return response.data.logs;
}
}
export const backupClient = new BackupClient();

View File

@@ -0,0 +1,79 @@
/**
* Client HTTP de base pour toutes les requêtes API
*/
export class HttpClient {
private baseUrl: string;
constructor(baseUrl: string = '') {
this.baseUrl = baseUrl;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error(`HTTP Request failed: ${url}`, error);
throw error;
}
}
async get<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = params
? `${endpoint}?${new URLSearchParams(params)}`
: endpoint;
return this.request<T>(url, { method: 'GET' });
}
async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
});
}
async delete<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = params
? `${endpoint}?${new URLSearchParams(params)}`
: endpoint;
return this.request<T>(url, { method: 'DELETE' });
}
}
// Instance par défaut
export const httpClient = new HttpClient('/api');

158
src/clients/daily-client.ts Normal file
View File

@@ -0,0 +1,158 @@
import { httpClient } from './base/http-client';
import { DailyCheckbox, DailyView, Task } from '@/lib/types';
// Types pour les réponses API (avec dates en string)
interface ApiCheckbox {
id: string;
date: string;
text: string;
isChecked: boolean;
type: 'task' | 'meeting';
order: number;
taskId?: string;
task?: Task;
createdAt: string;
updatedAt: string;
}
interface ApiDailyView {
date: string;
yesterday: ApiCheckbox[];
today: ApiCheckbox[];
}
interface ApiHistoryItem {
date: string;
checkboxes: ApiCheckbox[];
}
export interface DailyHistoryFilters {
limit?: number;
}
export interface DailySearchFilters {
query: string;
limit?: number;
}
// Types conservés pour la compatibilité des hooks d'historique et de recherche
export interface ReorderCheckboxesData {
date: Date;
checkboxIds: string[];
}
/**
* Client HTTP pour les données Daily (lecture seule)
* Les mutations sont gérées par les server actions dans actions/daily.ts
*/
export class DailyClient {
/**
* Récupère la vue daily d'aujourd'hui (hier + aujourd'hui)
*/
async getTodaysDailyView(): Promise<DailyView> {
const result = await httpClient.get<ApiDailyView>('/daily');
return this.transformDailyViewDates(result);
}
/**
* Récupère la vue daily pour une date donnée
*/
async getDailyView(date: Date): Promise<DailyView> {
const dateStr = this.formatDateForAPI(date);
const result = await httpClient.get<ApiDailyView>(`/daily?date=${dateStr}`);
return this.transformDailyViewDates(result);
}
/**
* Récupère l'historique des checkboxes
*/
async getCheckboxHistory(filters?: DailyHistoryFilters): Promise<{ date: Date; checkboxes: DailyCheckbox[] }[]> {
const params = new URLSearchParams({ action: 'history' });
if (filters?.limit) params.append('limit', filters.limit.toString());
const result = await httpClient.get<ApiHistoryItem[]>(`/daily?${params}`);
return result.map(item => ({
date: new Date(item.date),
checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
}));
}
/**
* Recherche dans les checkboxes
*/
async searchCheckboxes(filters: DailySearchFilters): Promise<DailyCheckbox[]> {
const params = new URLSearchParams({
action: 'search',
q: filters.query
});
if (filters.limit) params.append('limit', filters.limit.toString());
const result = await httpClient.get<ApiCheckbox[]>(`/daily?${params}`);
return result.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb));
}
/**
* Formate une date pour l'API (évite les décalages timezone)
*/
formatDateForAPI(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`; // YYYY-MM-DD
}
/**
* Transforme les dates string d'une checkbox en objets Date
*/
private transformCheckboxDates(checkbox: ApiCheckbox): DailyCheckbox {
return {
...checkbox,
date: new Date(checkbox.date),
createdAt: new Date(checkbox.createdAt),
updatedAt: new Date(checkbox.updatedAt)
};
}
/**
* Transforme les dates string d'une vue daily en objets Date
*/
private transformDailyViewDates(view: ApiDailyView): DailyView {
return {
date: new Date(view.date),
yesterday: view.yesterday.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)),
today: view.today.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
};
}
/**
* Récupère la vue daily d'une date relative (hier, aujourd'hui, demain)
*/
async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> {
const date = new Date();
switch (relative) {
case 'yesterday':
date.setDate(date.getDate() - 1);
break;
case 'tomorrow':
date.setDate(date.getDate() + 1);
break;
// 'today' ne change rien
}
return this.getDailyView(date);
}
/**
* Récupère toutes les dates qui ont des dailies
*/
async getDailyDates(): Promise<string[]> {
const response = await httpClient.get<{ dates: string[] }>('/daily/dates');
return response.dates;
}
}
// Instance singleton du client
export const dailyClient = new DailyClient();

View File

@@ -0,0 +1,36 @@
/**
* Client pour l'API Jira
*/
import { HttpClient } from './base/http-client';
import { JiraSyncResult } from '@/services/jira';
export interface JiraConnectionStatus {
connected: boolean;
message: string;
details?: string;
}
export class JiraClient extends HttpClient {
constructor() {
super('/api/jira');
}
/**
* Teste la connexion à Jira
*/
async testConnection(): Promise<JiraConnectionStatus> {
return this.get<JiraConnectionStatus>('/sync');
}
/**
* Lance la synchronisation manuelle des tickets Jira
*/
async syncTasks(): Promise<JiraSyncResult> {
const response = await this.post<{ data: JiraSyncResult }>('/sync');
return response.data;
}
}
// Instance singleton
export const jiraClient = new JiraClient();

View File

@@ -0,0 +1,61 @@
import { httpClient } from './base/http-client';
import { JiraConfig } from '@/lib/types';
export interface JiraConfigResponse {
jiraConfig: JiraConfig;
}
export interface SaveJiraConfigRequest {
baseUrl: string;
email: string;
apiToken: string;
projectKey?: string;
ignoredProjects?: string[];
}
export interface SaveJiraConfigResponse {
success: boolean;
message: string;
jiraConfig: JiraConfig;
}
class JiraConfigClient {
private readonly basePath = '/user-preferences/jira-config';
/**
* Récupère la configuration Jira actuelle
*/
async getJiraConfig(): Promise<JiraConfig> {
const response = await httpClient.get<JiraConfigResponse>(this.basePath);
return response.jiraConfig;
}
/**
* Sauvegarde la configuration Jira
*/
async saveJiraConfig(config: SaveJiraConfigRequest): Promise<SaveJiraConfigResponse> {
return httpClient.put<SaveJiraConfigResponse>(this.basePath, config);
}
/**
* Supprime la configuration Jira (remet à zéro)
*/
async deleteJiraConfig(): Promise<{ success: boolean; message: string }> {
return httpClient.delete(this.basePath);
}
/**
* Valide l'existence d'un projet Jira
*/
async validateProject(projectKey: string): Promise<{
success: boolean;
exists: boolean;
projectName?: string;
error?: string;
message: string;
}> {
return httpClient.post('/jira/validate-project', { projectKey });
}
}
export const jiraConfigClient = new JiraConfigClient();

136
src/clients/tags-client.ts Normal file
View File

@@ -0,0 +1,136 @@
import { HttpClient } from './base/http-client';
import { Tag } from '@/lib/types';
// Types pour les requêtes (now only used for validation - CRUD operations moved to server actions)
export interface TagFilters {
q?: string; // Recherche par nom
popular?: boolean; // Tags les plus utilisés
limit?: number; // Limite de résultats
}
// Types pour les réponses
export interface TagsResponse {
data: Array<Tag & { usage: number }>;
message: string;
}
export interface TagResponse {
data: Tag;
message: string;
}
export interface PopularTag extends Tag {
usage: number;
}
export interface PopularTagsResponse {
data: PopularTag[];
message: string;
}
/**
* Client HTTP pour la gestion des tags
*/
export class TagsClient extends HttpClient {
constructor() {
super('/api/tags');
}
/**
* Récupère tous les tags
*/
async getTags(filters?: TagFilters): Promise<TagsResponse> {
const params: Record<string, string> = {};
if (filters?.q) {
params.q = filters.q;
}
if (filters?.popular) {
params.popular = 'true';
}
if (filters?.limit) {
params.limit = filters.limit.toString();
}
return this.get<TagsResponse>('', Object.keys(params).length > 0 ? params : undefined);
}
/**
* Récupère les tags populaires (les plus utilisés)
*/
async getPopularTags(limit: number = 10): Promise<PopularTagsResponse> {
return this.get<PopularTagsResponse>('', { popular: 'true', limit: limit.toString() });
}
/**
* Recherche des tags par nom (pour autocomplete)
*/
async searchTags(query: string, limit: number = 10): Promise<TagsResponse> {
return this.get<TagsResponse>('', { q: query, limit: limit.toString() });
}
/**
* Récupère un tag par son ID
*/
async getTagById(id: string): Promise<TagResponse> {
return this.get<TagResponse>(`/${id}`);
}
// CRUD operations removed - now handled by server actions in /actions/tags.ts
/**
* Valide le format d'une couleur hexadécimale
*/
static isValidColor(color: string): boolean {
return /^#[0-9A-F]{6}$/i.test(color);
}
/**
* Génère une couleur aléatoire pour un nouveau tag
*/
static generateRandomColor(): string {
const colors = [
'#3B82F6', // Blue
'#EF4444', // Red
'#10B981', // Green
'#F59E0B', // Yellow
'#8B5CF6', // Purple
'#EC4899', // Pink
'#06B6D4', // Cyan
'#84CC16', // Lime
'#F97316', // Orange
'#6366F1', // Indigo
];
return colors[Math.floor(Math.random() * colors.length)];
}
/**
* Valide les données d'un tag (utilisé par les formulaires avant server actions)
*/
static validateTagData(data: { name?: string; color?: string }): string[] {
const errors: string[] = [];
if (!data.name || typeof data.name !== 'string') {
errors.push('Le nom du tag est requis');
} else if (data.name.trim().length === 0) {
errors.push('Le nom du tag ne peut pas être vide');
} else if (data.name.length > 50) {
errors.push('Le nom du tag ne peut pas dépasser 50 caractères');
}
if (!data.color || typeof data.color !== 'string') {
errors.push('La couleur du tag est requise');
} else if (!this.isValidColor(data.color)) {
errors.push('La couleur doit être au format hexadécimal (#RRGGBB)');
}
return errors;
}
}
// Instance singleton
export const tagsClient = new TagsClient();

View File

@@ -0,0 +1,81 @@
import { httpClient } from './base/http-client';
import { Task, TaskStatus, TaskPriority, TaskStats, DailyCheckbox } from '@/lib/types';
export interface TaskFilters {
status?: TaskStatus[];
source?: string[];
search?: string;
limit?: number;
offset?: number;
}
export interface TasksResponse {
success: boolean;
data: Task[];
stats: TaskStats;
count: number;
}
export interface CreateTaskData {
title: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
tags?: string[];
dueDate?: Date;
}
export interface UpdateTaskData {
taskId: string;
title?: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
tags?: string[];
dueDate?: Date;
}
/**
* Client pour la gestion des tâches
*/
export class TasksClient {
/**
* Récupère toutes les tâches avec filtres
*/
async getTasks(filters?: TaskFilters): Promise<TasksResponse> {
const params: Record<string, string> = {};
if (filters?.status) {
params.status = filters.status.join(',');
}
if (filters?.source) {
params.source = filters.source.join(',');
}
if (filters?.search) {
params.search = filters.search;
}
if (filters?.limit) {
params.limit = filters.limit.toString();
}
if (filters?.offset) {
params.offset = filters.offset.toString();
}
return httpClient.get<TasksResponse>('/tasks', params);
}
/**
* Récupère les daily checkboxes liées à une tâche
*/
async getTaskCheckboxes(taskId: string): Promise<DailyCheckbox[]> {
const response = await httpClient.get<{ data: DailyCheckbox[] }>(`/tasks/${taskId}/checkboxes`);
return response.data;
}
// Note: Les méthodes createTask, updateTask et deleteTask ont été migrées vers Server Actions
// Voir /src/actions/tasks.ts pour createTask, updateTask, updateTaskTitle, updateTaskStatus, deleteTask
}
// Instance singleton
export const tasksClient = new TasksClient();

View File

@@ -0,0 +1,28 @@
import { httpClient } from './base/http-client';
import { UserPreferences } from '@/lib/types';
export interface UserPreferencesResponse {
success: boolean;
data?: UserPreferences;
message?: string;
error?: string;
}
/**
* Client HTTP pour les préférences utilisateur (lecture seule)
* Les mutations sont gérées par les server actions dans actions/preferences.ts
*/
export const userPreferencesClient = {
/**
* Récupère toutes les préférences utilisateur
*/
async getPreferences(): Promise<UserPreferences> {
const response = await httpClient.get<UserPreferencesResponse>('/user-preferences');
if (!response.success || !response.data) {
throw new Error(response.error || 'Erreur lors de la récupération des préférences');
}
return response.data;
}
};