cleaning hard

This commit is contained in:
alma 2025-05-02 18:19:46 +02:00
parent a4412c081a
commit 35d4e37d79
13 changed files with 771 additions and 862 deletions

View File

@ -1,82 +1,59 @@
# Neah Email Application
# NeahFront9
A modern email client built with Next.js, featuring email composition, viewing, and management capabilities.
This project is a modern Next.js dashboard application with various widgets and components.
## Email Processing Workflow
## Code Refactoring
The application handles email processing through a centralized workflow:
The codebase is being systematically refactored to improve:
1. **Email Fetching**: Emails are fetched through the `/api/courrier` endpoints using user credentials stored in the database.
1. **Code Organization**: Moving from monolithic components to modular, reusable ones
2. **Maintainability**: Implementing custom hooks for data fetching and state management
3. **Performance**: Reducing duplicate code and optimizing renders
4. **Consistency**: Creating unified utilities for common operations
2. **Email Parsing**: Raw email content is parsed using:
- Server-side: `parseEmail` function from `lib/server/email-parser.ts` (which uses `simpleParser` from the `mailparser` library)
- API route: `/api/parse-email` provides a REST interface to the parser
### Completed Refactoring
3. **HTML Sanitization**: Email HTML content is sanitized and processed using:
- `sanitizeHtml` function in `lib/utils/email-utils.ts` (centralized implementation)
- DOMPurify with specific configuration to handle email content safely
The following refactoring tasks have been completed:
4. **Email Display**: Sanitized content is rendered in the UI with proper styling and security measures
#### Utility Modules
5. **Email Composition**: The `ComposeEmail` component handles email creation, replying, and forwarding
- Email is sent through the `/api/courrier/send` endpoint
- **Status Utilities** (`lib/utils/status-utils.ts`): Centralized task status color and label handling
- **Date Utilities** (`lib/utils/date-utils.ts`): Common date validation and formatting functions
## Key Features
#### Custom Hooks
- **Email Fetching and Management**: Connect to IMAP servers and manage email fetching and caching logic
- **Email Composition**: Rich text editor with reply and forwarding capabilities
- **Email Display**: Secure rendering of HTML emails
- **Attachment Handling**: View and download attachments
- **Task Hook** (`hooks/use-tasks.ts`): Reusable hook for fetching and managing task data
- **Calendar Events Hook** (`hooks/use-calendar-events.ts`): Reusable hook for calendar event handling
## Project Structure
#### Updated Components
The project follows a modular structure:
- **Duties/Flow Component** (`components/flow.tsx`): Simplified to use the custom task hook
- **Calendar Component** (`components/calendar.tsx`): Refactored to use the custom calendar events hook
- `/app` - Next.js App Router structure with routes and API endpoints
- `/components` - React components organized by domain
- `/lib` - Core library code:
- `/server` - Server-only code like email parsing
- `/services` - Domain-specific services, including email service
- `/reducers` - State management logic
- `/utils` - Utility functions including the centralized email formatter
### In Progress
## Technologies
- More components will be refactored to follow the same patterns
- State management improvements across the application
- Better UI responsiveness and accessibility
- Next.js 14+ with App Router
- React Server Components
- TailwindCSS for styling
- Mailparser for email parsing
- ImapFlow for email fetching
- DOMPurify for HTML sanitization
- Redis for caching
## API Documentation
## State Management
A documentation endpoint has been created to track code updates:
Email state is managed through React context and reducers, with server data fetched through React Server Components or client-side API calls as needed.
```
GET /api/code-updates
```
# Email Formatting
This returns a JSON object with details of all refactoring changes made to the codebase.
## Centralized Email Formatter
## Development
All email formatting is now handled by a centralized formatter in `lib/utils/email-utils.ts`. This ensures consistent handling of:
To run the development server:
- Reply and forward formatting
- HTML sanitization
- RTL/LTR text direction
- MIME encoding and decoding for email composition
```bash
npm run dev
# or
yarn dev
```
Key functions include:
- `formatForwardedEmail`: Format emails for forwarding
- `formatReplyEmail`: Format emails for replying
- `sanitizeHtml`: Safely sanitize HTML email content
- `formatEmailForReplyOrForward`: Compatibility function for both
- `decodeComposeContent`: Parse MIME content for email composition
- `encodeComposeContent`: Create MIME-formatted content for sending emails
This centralized approach prevents formatting inconsistencies and direction problems when dealing with emails in different languages.
## Deprecated Functions
Several functions have been deprecated and removed in favor of centralized implementations:
- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
// This API serves as documentation for code refactoring steps
export async function GET() {
const headersList = headers();
const updates = [
{
id: 1,
description: "Created consistent task status utilities",
file: "lib/utils/status-utils.ts",
date: "2023-07-15",
details: "Centralized task status color and label handling"
},
{
id: 2,
description: "Created date validation utilities",
file: "lib/utils/date-utils.ts",
date: "2023-07-15",
details: "Added isValidDateString and other date handling utilities"
},
{
id: 3,
description: "Created useTasks custom hook",
file: "hooks/use-tasks.ts",
date: "2023-07-16",
details: "Centralized task fetching logic into a reusable React hook"
},
{
id: 4,
description: "Refactored Duties component in flow.tsx",
file: "components/flow.tsx",
date: "2023-07-16",
details: "Updated to use the new useTasks hook instead of built-in task fetching"
},
{
id: 5,
description: "Created useCalendarEvents custom hook",
file: "hooks/use-calendar-events.ts",
date: "2023-07-16",
details: "Centralized calendar event fetching logic into a reusable React hook"
},
{
id: 6,
description: "Refactored Calendar component",
file: "components/calendar.tsx",
date: "2023-07-16",
details: "Updated to use the new useCalendarEvents hook for improved maintainability"
}
];
return NextResponse.json({ updates });
}

View File

@ -1,129 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
interface Task {
id: string;
headline: string;
projectName: string;
projectId: number;
status: number;
dueDate: string | null;
milestone: string | null;
details: string | null;
createdOn: string;
editedOn: string | null;
assignedTo: number[];
}
export default function Flow() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const getStatusLabel = (status: number): string => {
switch (status) {
case 1:
return 'New';
case 2:
return 'In Progress';
case 3:
return 'Done';
case 4:
return 'In Progress';
case 5:
return 'Done';
default:
return 'Unknown';
}
};
const getStatusColor = (status: number): string => {
switch (status) {
case 1:
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
case 2:
case 4:
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
case 3:
case 5:
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
}
};
useEffect(() => {
const fetchTasks = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/leantime/tasks');
if (!response.ok) {
throw new Error('Failed to fetch tasks');
}
const data = await response.json();
if (data.tasks && Array.isArray(data.tasks)) {
// Sort tasks by creation date (oldest first)
const sortedTasks = data.tasks.sort((a: Task, b: Task) => {
const dateA = new Date(a.createdOn).getTime();
const dateB = new Date(b.createdOn).getTime();
return dateA - dateB;
});
setTasks(sortedTasks);
} else {
console.error('Invalid tasks data format:', data);
setError('Invalid tasks data format');
}
} catch (err) {
console.error('Error fetching tasks:', err);
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchTasks();
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
if (tasks.length === 0) {
return <div>No tasks found</div>;
}
return (
<div className="grid grid-cols-1 gap-4">
{tasks.map((task) => (
<div
key={task.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow p-4"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{task.headline}
</h3>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{task.projectName}
</p>
</div>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
task.status
)}`}
>
{getStatusLabel(task.status)}
</span>
</div>
</div>
))}
</div>
);
}

View File

@ -36,7 +36,7 @@ export default function Home() {
<QuoteCard />
</div>
<div className="col-span-3">
<Calendar />
<Calendar limit={5} showMore={true} showRefresh={true} />
</div>
<div className="col-span-3">
<News />

View File

@ -1,195 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { format, isToday, isTomorrow, addDays } from "date-fns";
import { fr } from "date-fns/locale";
import { CalendarIcon, ClockIcon, ChevronRight } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useSession } from "next-auth/react";
type Event = {
id: string;
title: string;
start: string;
end: string;
isAllDay: boolean;
calendarId: string;
calendarName?: string;
calendarColor?: string;
};
export function CalendarWidget() {
const { data: session } = useSession();
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Ne charger les événements que si l'utilisateur est connecté
if (!session) return;
const fetchUpcomingEvents = async () => {
try {
setLoading(true);
// Récupérer d'abord les calendriers de l'utilisateur
const calendarsRes = await fetch("/api/calendars");
if (!calendarsRes.ok) {
throw new Error("Impossible de charger les calendriers");
}
const calendars = await calendarsRes.json();
if (calendars.length === 0) {
setEvents([]);
setLoading(false);
return;
}
// Date actuelle et date dans 7 jours
const now = new Date();
// @ts-ignore
const nextWeek = addDays(now, 7);
// Récupérer les événements pour chaque calendrier
const allEventsPromises = calendars.map(async (calendar: any) => {
const eventsRes = await fetch(
`/api/calendars/${
calendar.id
}/events?start=${now.toISOString()}&end=${nextWeek.toISOString()}`
);
if (!eventsRes.ok) {
console.warn(
`Impossible de charger les événements du calendrier ${calendar.id}`
);
return [];
}
const events = await eventsRes.json();
// Ajouter les informations du calendrier à chaque événement
return events.map((event: any) => ({
...event,
calendarName: calendar.name,
calendarColor: calendar.color,
}));
});
// Attendre toutes les requêtes d'événements
const allEventsArrays = await Promise.all(allEventsPromises);
// Fusionner tous les événements en un seul tableau
const allEvents = allEventsArrays.flat();
// Trier par date de début
const sortedEvents = allEvents.sort(
(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
);
// Limiter à 5 événements
setEvents(sortedEvents.slice(0, 5));
} catch (err) {
console.error("Erreur lors du chargement des événements:", err);
setError("Impossible de charger les événements à venir");
} finally {
setLoading(false);
}
};
fetchUpcomingEvents();
}, [session]);
// Formater la date d'un événement pour l'affichage
const formatEventDate = (date: string, isAllDay: boolean) => {
const eventDate = new Date(date);
let dateString = "";
// @ts-ignore
if (isToday(eventDate)) {
dateString = "Aujourd'hui";
// @ts-ignore
} else if (isTomorrow(eventDate)) {
dateString = "Demain";
} else {
// @ts-ignore
dateString = format(eventDate, "EEEE d MMMM", { locale: fr });
}
if (!isAllDay) {
// @ts-ignore
dateString += ` · ${format(eventDate, "HH:mm", { locale: fr })}`;
}
return dateString;
};
return (
<Card className='transition-transform duration-500 ease-in-out transform hover:scale-105'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<CardTitle className='text-lg font-medium'>
Événements à venir
</CardTitle>
<Link href='/calendar' passHref>
<Button variant='ghost' size='sm' className='h-8 w-8 p-0'>
<ChevronRight className='h-4 w-4' />
<span className='sr-only'>Voir le calendrier</span>
</Button>
</Link>
</CardHeader>
<CardContent className='pb-3'>
{loading ? (
<div className='flex items-center justify-center py-4'>
<div className='h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent' />
<span className='ml-2 text-sm text-muted-foreground'>
Chargement...
</span>
</div>
) : error ? (
<p className='text-sm text-red-500'>{error}</p>
) : events.length === 0 ? (
<p className='text-sm text-muted-foreground py-2'>
Aucun événement à venir cette semaine
</p>
) : (
<div className='space-y-3'>
{events.map((event) => (
<div
key={event.id}
className='flex items-start space-x-3 rounded-md border border-muted p-2'
>
<div
className='h-3 w-3 flex-shrink-0 rounded-full mt-1'
style={{ backgroundColor: event.calendarColor || "#0082c9" }}
/>
<div className='flex-1 min-w-0'>
<h5
className='text-sm font-medium truncate'
title={event.title}
>
{event.title}
</h5>
<div className='flex items-center text-xs text-muted-foreground mt-1'>
<CalendarIcon className='h-3 w-3 mr-1' />
<span>{formatEventDate(event.start, event.isAllDay)}</span>
</div>
</div>
</div>
))}
<Link href='/calendar' passHref>
<Button
size='sm'
className='w-full transition-all ease-in-out duration-500 bg-muted text-black hover:text-white hover:bg-primary'
>
Voir tous les événements
</Button>
</Link>
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -1,107 +1,77 @@
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { RefreshCw, Calendar as CalendarIcon } from "lucide-react";
import { RefreshCw, Calendar as CalendarIcon, ChevronRight } from "lucide-react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { format, isToday, isTomorrow } from "date-fns";
import { fr } from "date-fns/locale";
import { useCalendarEvents, CalendarEvent } from "@/hooks/use-calendar-events";
interface Event {
id: string;
title: string;
start: string;
end: string;
allDay: boolean;
calendar: string;
calendarColor: string;
interface CalendarProps {
limit?: number;
showMore?: boolean;
showRefresh?: boolean;
cardClassName?: string;
}
export function Calendar() {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
export function Calendar({
limit = 5,
showMore = true,
showRefresh = true,
cardClassName = "transition-transform duration-500 ease-in-out transform hover:scale-105 bg-white/95 backdrop-blur-sm border-0 shadow-lg"
}: CalendarProps) {
const { events, loading, error, refresh } = useCalendarEvents({ limit });
const router = useRouter();
const fetchEvents = async () => {
setLoading(true);
try {
const response = await fetch('/api/calendars');
if (!response.ok) {
throw new Error('Failed to fetch events');
}
const calendarsData = await response.json();
console.log('Calendar Widget - Fetched calendars:', calendarsData);
const formatEventDate = (date: Date | string, isAllDay: boolean) => {
const eventDate = date instanceof Date ? date : new Date(date);
let dateString = "";
// Get current date at the start of the day
const now = new Date();
now.setHours(0, 0, 0, 0);
// Extract and process events from all calendars
const allEvents = calendarsData.flatMap((calendar: any) =>
(calendar.events || []).map((event: any) => ({
id: event.id,
title: event.title,
start: event.start,
end: event.end,
allDay: event.isAllDay,
calendar: calendar.name,
calendarColor: calendar.color
}))
);
// Filter for upcoming events
const upcomingEvents = allEvents
.filter((event: any) => new Date(event.start) >= now)
.sort((a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime())
.slice(0, 7);
console.log('Calendar Widget - Processed events:', upcomingEvents);
setEvents(upcomingEvents);
setError(null);
} catch (err) {
console.error('Error fetching events:', err);
setError('Failed to load events');
} finally {
setLoading(false);
if (isToday(eventDate)) {
dateString = "Today";
} else if (isTomorrow(eventDate)) {
dateString = "Tomorrow";
} else {
dateString = format(eventDate, "EEEE d MMMM", { locale: fr });
}
};
useEffect(() => {
fetchEvents();
}, []);
if (!isAllDay) {
dateString += ` · ${format(eventDate, "HH:mm", { locale: fr })}`;
}
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit',
month: 'short'
}).format(date);
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('fr-FR', {
hour: '2-digit',
minute: '2-digit',
}).format(date);
return dateString;
};
return (
<Card className="transition-transform duration-500 ease-in-out transform hover:scale-105 bg-white/95 backdrop-blur-sm border-0 shadow-lg">
<Card className={cardClassName}>
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<CalendarIcon className="h-5 w-5 text-gray-600" />
Agenda
Calendar
</CardTitle>
<Button
variant="ghost"
size="icon"
onClick={() => fetchEvents()}
className="h-7 w-7 p-0 hover:bg-gray-100/50 rounded-full"
>
<RefreshCw className="h-3.5 w-3.5 text-gray-600" />
</Button>
<div className="flex items-center gap-1">
{showRefresh && (
<Button
variant="ghost"
size="icon"
onClick={refresh}
className="h-7 w-7 p-0 hover:bg-gray-100/50 rounded-full"
disabled={loading}
>
<RefreshCw className="h-3.5 w-3.5 text-gray-600" />
</Button>
)}
{showMore && (
<Link href='/agenda' passHref>
<Button variant='ghost' size='sm' className='h-8 w-8 p-0'>
<ChevronRight className='h-4 w-4' />
<span className='sr-only'>View calendar</span>
</Button>
</Link>
)}
</div>
</CardHeader>
<CardContent className="p-3">
{loading ? (
@ -123,21 +93,21 @@ export function Calendar() {
<div
className="flex-shrink-0 w-14 h-14 rounded-lg flex flex-col items-center justify-center border"
style={{
backgroundColor: `${event.calendarColor}10`,
borderColor: event.calendarColor
backgroundColor: `${event.calendarColor || '#4F46E5'}10`,
borderColor: event.calendarColor || '#4F46E5'
}}
>
<span
className="text-[10px] font-medium"
style={{ color: event.calendarColor }}
style={{ color: event.calendarColor || '#4F46E5' }}
>
{formatDate(event.start)}
{format(event.start instanceof Date ? event.start : new Date(event.start), 'MMM', { locale: fr })}
</span>
<span
className="text-[10px] font-bold mt-0.5"
style={{ color: event.calendarColor }}
style={{ color: event.calendarColor || '#4F46E5' }}
>
{formatTime(event.start)}
{format(event.start instanceof Date ? event.start : new Date(event.start), 'dd', { locale: fr })}
</span>
</div>
<div className="flex-1 min-w-0 space-y-1">
@ -145,25 +115,36 @@ export function Calendar() {
<p className="text-sm font-medium text-gray-800 line-clamp-2 flex-1">
{event.title}
</p>
{!event.allDay && (
{!event.isAllDay && (
<span className="text-[10px] text-gray-500 whitespace-nowrap">
{formatTime(event.start)} - {formatTime(event.end)}
{format(event.start instanceof Date ? event.start : new Date(event.start), 'HH:mm', { locale: fr })} - {format(event.end instanceof Date ? event.end : new Date(event.end), 'HH:mm', { locale: fr })}
</span>
)}
</div>
<div
className="flex items-center text-[10px] px-1.5 py-0.5 rounded-md"
style={{
backgroundColor: `${event.calendarColor}10`,
color: event.calendarColor
backgroundColor: `${event.calendarColor || '#4F46E5'}10`,
color: event.calendarColor || '#4F46E5'
}}
>
<span className="truncate">{event.calendar}</span>
<span className="truncate">{event.calendarName || 'Calendar'}</span>
</div>
</div>
</div>
</div>
))}
{showMore && (
<Link href='/agenda' passHref>
<Button
size='sm'
className='w-full transition-all ease-in-out duration-500 bg-gray-100 text-gray-700 hover:bg-gray-200 mt-2'
>
View all events
</Button>
</Link>
)}
</div>
)}
</CardContent>

View File

@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { RefreshCw, MessageSquare, Mail, MailOpen, Loader2 } from "lucide-react";
import Link from 'next/link';
import { useSession } from "next-auth/react";
interface Email {
id: string;
@ -17,28 +18,43 @@ interface Email {
folder: string;
}
interface EmailResponse {
emails: Email[];
mailUrl: string;
error?: string;
interface EmailProps {
limit?: number;
showTitle?: boolean;
showRefresh?: boolean;
showMoreLink?: boolean;
folder?: string;
cardClassName?: string;
title?: string;
}
export function Email() {
export function Email({
limit = 5,
showTitle = true,
showRefresh = true,
showMoreLink = true,
folder = "INBOX",
cardClassName = "h-full",
title = "Unread Emails"
}: EmailProps) {
const [emails, setEmails] = useState<Email[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [mailUrl, setMailUrl] = useState<string | null>(null);
const { status } = useSession();
useEffect(() => {
fetchEmails();
}, []);
if (status === "authenticated") {
fetchEmails();
}
}, [status, folder, limit]);
const fetchEmails = async (isRefresh = false) => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/courrier?folder=INBOX&page=1&perPage=5');
const response = await fetch(`/api/courrier?folder=${folder}&page=1&perPage=${limit}`);
if (!response.ok) {
throw new Error('Failed to fetch emails');
}
@ -52,20 +68,19 @@ export function Email() {
// Transform data format if needed
const transformedEmails = data.emails.map((email: any) => ({
id: email.id,
subject: email.subject,
subject: email.subject || '(No subject)',
from: email.from[0]?.address || '',
fromName: email.from[0]?.name || '',
date: email.date,
read: email.flags.seen,
starred: email.flags.flagged,
folder: email.folder
})).slice(0, 5); // Only show the first 5 emails
})).slice(0, limit);
setEmails(transformedEmails);
setMailUrl('/courrier');
}
} catch (error) {
console.error('Error fetching emails:', error);
setError('Failed to load emails');
setEmails([]);
} finally {
@ -85,68 +100,88 @@ export function Email() {
}
};
return (
<Card className="h-full">
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<MessageSquare className="h-5 w-5 text-gray-600" />
Emails non lus
</CardTitle>
<Button
variant="ghost"
size="icon"
onClick={() => fetchEmails(true)}
disabled={loading}
>
{loading ?
<Loader2 className="h-4 w-4 animate-spin" /> :
<RefreshCw className="h-4 w-4" />
}
</Button>
</CardHeader>
<CardContent className="p-4">
{error ? (
<div className="text-center py-4 text-gray-500">
{error}
</div>
) : loading && emails.length === 0 ? (
<div className="text-center py-6 flex flex-col items-center">
<Loader2 className="h-6 w-6 animate-spin text-gray-400 mb-2" />
<p className="text-gray-500">Chargement des emails...</p>
</div>
) : emails.length === 0 ? (
<div className="text-center py-6">
<p className="text-gray-500">Aucun email non lu</p>
</div>
) : (
<div className="space-y-3">
{emails.map((email) => (
<div key={email.id} className="flex items-start gap-3 py-1 border-b border-gray-100 last:border-0">
<div className="pt-1">
{email.read ?
<MailOpen className="h-4 w-4 text-gray-400" /> :
<Mail className="h-4 w-4 text-blue-500" />
}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between">
<p className="font-medium truncate" style={{maxWidth: '180px'}}>{email.fromName || email.from.split('@')[0]}</p>
<p className="text-xs text-gray-500">{formatDate(email.date)}</p>
</div>
<p className="text-sm text-gray-700 truncate">{email.subject}</p>
</div>
const renderContent = () => {
if (error) {
return (
<div className="text-center py-4 text-gray-500">
{error}
</div>
);
}
if (loading && emails.length === 0) {
return (
<div className="text-center py-6 flex flex-col items-center">
<Loader2 className="h-6 w-6 animate-spin text-gray-400 mb-2" />
<p className="text-gray-500">Loading emails...</p>
</div>
);
}
if (emails.length === 0) {
return (
<div className="text-center py-6">
<p className="text-gray-500">No emails found</p>
</div>
);
}
return (
<div className="space-y-3">
{emails.map((email) => (
<div key={email.id} className="flex items-start gap-3 py-1 border-b border-gray-100 last:border-0">
<div className="pt-1">
{email.read ?
<MailOpen className="h-4 w-4 text-gray-400" /> :
<Mail className="h-4 w-4 text-blue-500" />
}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between">
<p className="font-medium truncate" style={{maxWidth: '180px'}}>{email.fromName || email.from.split('@')[0]}</p>
<p className="text-xs text-gray-500">{formatDate(email.date)}</p>
</div>
))}
{mailUrl && (
<div className="pt-2">
<Link href={mailUrl} className="text-sm text-blue-600 hover:text-blue-800">
Voir tous les emails
</Link>
</div>
)}
<p className="text-sm text-gray-700 truncate">{email.subject}</p>
</div>
</div>
))}
{showMoreLink && mailUrl && (
<div className="pt-2">
<Link href={mailUrl} className="text-sm text-blue-600 hover:text-blue-800">
View all emails
</Link>
</div>
)}
</div>
);
};
return (
<Card className={cardClassName}>
{showTitle && (
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<MessageSquare className="h-5 w-5 text-gray-600" />
{title}
</CardTitle>
{showRefresh && (
<Button
variant="ghost"
size="icon"
onClick={() => fetchEmails(true)}
disabled={loading}
>
{loading ?
<Loader2 className="h-4 w-4 animate-spin" /> :
<RefreshCw className="h-4 w-4" />
}
</Button>
)}
</CardHeader>
)}
<CardContent className="p-4">
{renderContent()}
</CardContent>
</Card>
);

View File

@ -1,10 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { RefreshCw, MessageSquare } from "lucide-react";
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<MessageSquare className="h-5 w-5 text-gray-600" />
Emails non lus
</CardTitle>
</CardHeader>

View File

@ -1,172 +1,36 @@
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { RefreshCw, Share2, Folder } from "lucide-react";
import { RefreshCw, Share2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { getTaskStatusColor, getTaskStatusLabel } from "@/lib/utils/status-utils";
import { useTasks, Task } from "@/hooks/use-tasks";
interface Task {
id: number;
headline: string;
description: string;
dateToFinish: string | null;
projectId: number;
projectName: string;
status: number;
editorId?: string;
editorFirstname?: string;
editorLastname?: string;
authorFirstname: string;
authorLastname: string;
milestoneHeadline?: string;
editTo?: string;
editFrom?: string;
type?: string;
dependingTicketId?: number | null;
interface FlowProps {
limit?: number;
showRefresh?: boolean;
showHeader?: boolean;
cardClassName?: string;
}
interface ProjectSummary {
name: string;
tasks: {
status: number;
count: number;
}[];
}
interface TaskWithDate extends Task {
validDate?: Date;
}
export function Duties() {
const [tasks, setTasks] = useState<TaskWithDate[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const getStatusLabel = (status: number): string => {
switch (status) {
case 1: return 'New';
case 2: return 'Blocked';
case 3: return 'In Progress';
case 4: return 'Waiting for Approval';
case 5: return 'Done';
default: return 'Unknown';
}
};
const getStatusColor = (status: number): string => {
switch (status) {
case 1: return 'bg-blue-500'; // New - blue
case 2: return 'bg-red-500'; // Blocked - red
case 3: return 'bg-yellow-500'; // In Progress - yellow
case 4: return 'bg-purple-500'; // Waiting for Approval - purple
case 5: return 'bg-gray-500'; // Done - gray
default: return 'bg-gray-300';
}
};
const formatDate = (dateStr: string): string => {
if (!dateStr || dateStr === '0000-00-00 00:00:00') return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '';
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
} catch {
return '';
}
};
const getValidDate = (task: Task): string | null => {
if (task.dateToFinish && task.dateToFinish !== '0000-00-00 00:00:00') {
return task.dateToFinish;
}
return null;
};
const fetchTasks = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/leantime/tasks');
if (!response.ok) {
throw new Error('Failed to fetch tasks');
}
const data = await response.json();
console.log('Raw API response:', data);
if (!Array.isArray(data)) {
console.warn('No tasks found in response', data as unknown);
setTasks([]);
return;
}
// Filter out tasks with status Done (5) and sort by dateToFinish
const sortedTasks = data
.filter((task: Task) => {
// Filter out any task (main or subtask) that has status Done (5)
const isNotDone = task.status !== 5;
if (!isNotDone) {
console.log(`Filtering out Done task ${task.id} (type: ${task.type || 'main'}, status: ${task.status})`);
} else {
console.log(`Keeping task ${task.id}: status=${task.status} (${getStatusLabel(task.status)}), type=${task.type || 'main'}`);
}
return isNotDone;
})
.sort((a: Task, b: Task) => {
// First sort by dateToFinish (oldest first)
const dateA = getValidDate(a);
const dateB = getValidDate(b);
// If both dates are valid, compare them
if (dateA && dateB) {
const timeA = new Date(dateA).getTime();
const timeB = new Date(dateB).getTime();
if (timeA !== timeB) {
return timeA - timeB;
}
}
// If only one date is valid, put the task with a date first
if (dateA) return -1;
if (dateB) return 1;
// If dates are equal or neither has a date, sort by status (4 before others)
if (a.status === 4 && b.status !== 4) return -1;
if (b.status === 4 && a.status !== 4) return 1;
// If status is also equal, maintain original order
return 0;
});
console.log('Sorted and filtered tasks:', sortedTasks.map(t => ({
id: t.id,
date: t.dateToFinish,
status: t.status,
type: t.type || 'main'
})));
setTasks(sortedTasks.slice(0, 7));
} catch (error) {
console.error('Error fetching tasks:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
// Update the TaskDate component to handle dates better
const TaskDate = ({ task }: { task: TaskWithDate }) => {
const dateStr = task.dateToFinish;
if (!dateStr || dateStr === '0000-00-00 00:00:00') {
export function Duties({
limit = 5,
showRefresh = true,
showHeader = true,
cardClassName = "transition-transform duration-500 ease-in-out transform hover:scale-105 bg-white/95 backdrop-blur-sm border-0 shadow-lg"
}: FlowProps) {
const { tasks, loading, error, refresh } = useTasks({
limit,
includeDone: false,
sortField: 'dateToFinish',
sortDirection: 'asc'
});
// TaskDate component
const TaskDate = ({ task }: { task: Task }) => {
const dateStr = task.dateToFinish || task.dueDate;
if (!dateStr) {
return (
<div className="flex flex-col items-center">
<span className="text-[10px] text-gray-600 font-medium">NO</span>
@ -205,7 +69,6 @@ export function Duties() {
</div>
);
} catch (error) {
console.error('Error formatting date for task', task.id, error);
return (
<div className="flex flex-col items-center">
<span className="text-[10px] text-gray-600 font-medium">ERR</span>
@ -215,61 +78,92 @@ export function Duties() {
}
};
const renderContent = () => {
if (loading) {
return (
<div className="flex items-center justify-center py-6">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
</div>
);
}
if (error) {
return (
<div className="text-xs text-red-500 text-center py-3">{error}</div>
);
}
if (tasks.length === 0) {
return (
<div className="text-xs text-gray-500 text-center py-6">No tasks with due dates found</div>
);
}
return (
<div className="space-y-2 max-h-[400px] overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent">
{tasks.map((task) => (
<div
key={task.id}
className="p-2 rounded-lg bg-white shadow-sm hover:shadow-md transition-all duration-200 border border-gray-100"
>
<div className="flex gap-2">
<div className="flex-shrink-0 w-14 h-14 rounded-md flex items-center justify-center">
<TaskDate task={task} />
</div>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-gray-800 line-clamp-2 flex-1">
{task.headline}
</p>
<div className={`px-2 py-1 rounded-full w-3 h-3 ${getTaskStatusColor(task.status)}`}
title={getTaskStatusLabel(task.status)} />
</div>
<div className="flex items-center gap-1">
<Badge variant="outline" className="text-[10px] bg-gray-50">
{task.projectName}
</Badge>
{task.milestoneHeadline && (
<Badge variant="outline" className="text-[10px] bg-blue-50">
{task.milestoneHeadline}
</Badge>
)}
</div>
</div>
</div>
</div>
))}
</div>
);
};
if (!showHeader) {
return renderContent();
}
return (
<Card className="transition-transform duration-500 ease-in-out transform hover:scale-105 bg-white/95 backdrop-blur-sm border-0 shadow-lg">
<Card className={cardClassName}>
<CardHeader className="flex flex-row items-center justify-between pb-2 border-b border-gray-100">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Share2 className="h-5 w-5 text-gray-600" />
Devoirs
Tasks
</CardTitle>
<Button
variant="ghost"
size="icon"
onClick={() => fetchTasks()}
className="h-7 w-7 p-0 hover:bg-gray-100/50 rounded-full"
>
<RefreshCw className="h-3.5 w-3.5 text-gray-600" />
</Button>
{showRefresh && (
<Button
variant="ghost"
size="icon"
onClick={refresh}
className="h-7 w-7 p-0 hover:bg-gray-100/50 rounded-full"
disabled={loading}
>
<RefreshCw className={`h-3.5 w-3.5 text-gray-600 ${loading ? 'animate-spin' : ''}`} />
</Button>
)}
</CardHeader>
<CardContent className="p-3">
{loading ? (
<div className="flex items-center justify-center py-6">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
</div>
) : error ? (
<div className="text-xs text-red-500 text-center py-3">{error}</div>
) : tasks.length === 0 ? (
<div className="text-xs text-gray-500 text-center py-6">No tasks with due dates found</div>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent">
{tasks.map((task) => (
<div
key={task.id}
className="p-2 rounded-lg bg-white shadow-sm hover:shadow-md transition-all duration-200 border border-gray-100"
>
<div className="flex gap-2">
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-blue-50 flex flex-col items-center justify-center border border-blue-100">
<TaskDate task={task} />
</div>
<div className="flex-1 min-w-0 space-y-1">
<a
href={`https://agilite.slm-lab.net/tickets/showTicket/${task.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700 font-medium block text-sm line-clamp-2"
>
{task.headline}
</a>
<div className="flex items-center text-gray-500 text-[10px] bg-gray-50 px-1.5 py-0.5 rounded-md">
<Folder className="h-2.5 w-2.5 mr-1 opacity-70" />
<span className="truncate">{task.projectName}</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
{renderContent()}
</CardContent>
</Card>
);

View File

@ -1,106 +1,121 @@
import { useState, useEffect } from "react";
import { Event } from "@prisma/client";
import { useSession } from "next-auth/react";
export function useCalendarEvents(calendarId: string, start: Date, end: Date) {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
export interface CalendarEvent {
id: string;
title: string;
start: Date | string;
end: Date | string;
isAllDay: boolean;
calendarId?: string;
calendarName?: string;
calendarColor?: string;
}
// Charger les événements
const fetchEvents = async () => {
if (!calendarId) return;
export interface UseCalendarEventsOptions {
limit?: number;
includeAllDay?: boolean;
includeMultiDay?: boolean;
filterCalendarIds?: string[];
}
try {
export function useCalendarEvents({
limit = 10,
includeAllDay = true,
includeMultiDay = true,
filterCalendarIds
}: UseCalendarEventsOptions = {}) {
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const { status } = useSession();
const refresh = () => {
setRefreshTrigger(prev => prev + 1);
};
useEffect(() => {
const fetchEvents = async () => {
if (status !== "authenticated") {
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch('/api/calendars');
if (!response.ok) {
throw new Error('Failed to fetch events');
}
const calendarsData = await response.json();
const response = await fetch(
`/api/calendars/${calendarId}/events?` +
`start=${start.toISOString()}&end=${end.toISOString()}`
);
// Get current date at the start of the day
const now = new Date();
now.setHours(0, 0, 0, 0);
if (!response.ok) {
throw new Error(`Erreur ${response.status}: ${await response.text()}`);
// Extract and process events from all calendars
const allEvents = calendarsData.flatMap((calendar: any) =>
// Skip calendars if filtering is applied
(filterCalendarIds && !filterCalendarIds.includes(calendar.id)) ? [] :
(calendar.events || []).map((event: any) => ({
id: event.id,
title: event.title,
start: new Date(event.start),
end: new Date(event.end),
isAllDay: event.isAllDay || false,
calendarId: calendar.id,
calendarName: calendar.name,
calendarColor: calendar.color
}))
);
// Filter for upcoming events
const upcomingEvents = allEvents
.filter((event: CalendarEvent) => {
// Skip past events
if (new Date(event.start) < now) {
return false;
}
// Filter all-day events if not included
if (!includeAllDay && event.isAllDay) {
return false;
}
// Filter multi-day events if not included
if (!includeMultiDay) {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
// Check if the event spans multiple days
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
if (startDate.getTime() !== endDate.getTime()) {
return false;
}
}
return true;
})
.sort((a: CalendarEvent, b: CalendarEvent) =>
new Date(a.start).getTime() - new Date(b.start).getTime()
);
setEvents(limit ? upcomingEvents.slice(0, limit) : upcomingEvents);
setError(null);
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to fetch events');
} finally {
setLoading(false);
}
};
const data = await response.json();
setEvents(data);
} catch (err) {
console.error("Erreur lors du chargement des événements:", err);
setError(err as Error);
} finally {
setLoading(false);
}
};
// Créer un événement
const createEvent = async (eventData: any) => {
const response = await fetch(`/api/calendars/${calendarId}/events`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventData),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return await response.json();
};
// Mettre à jour un événement
const updateEvent = async (eventData: any) => {
const response = await fetch(
`/api/calendars/${calendarId}/events/${eventData.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventData),
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return await response.json();
};
// Supprimer un événement
const deleteEvent = async (eventId: string) => {
const response = await fetch(
`/api/calendars/${calendarId}/events/${eventId}`,
{
method: "DELETE",
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return true;
};
// Charger les événements quand le calendrier ou les dates changent
useEffect(() => {
fetchEvents();
}, [calendarId, start.toISOString(), end.toISOString()]);
}, [status, limit, includeAllDay, includeMultiDay, filterCalendarIds, refreshTrigger]);
return {
events,
loading,
error,
refresh: fetchEvents,
createEvent,
updateEvent,
deleteEvent,
};
return { events, loading, error, refresh };
}

140
hooks/use-tasks.ts Normal file
View File

@ -0,0 +1,140 @@
import { useState, useEffect } from "react";
import { isValidDateString } from "@/lib/utils/date-utils";
export interface Task {
id: number | string;
headline: string;
description?: string;
dateToFinish?: string | null;
dueDate?: string | null;
projectId: number;
projectName: string;
status: number;
editorId?: string;
editorFirstname?: string;
editorLastname?: string;
authorFirstname?: string;
authorLastname?: string;
milestone?: string | null;
milestoneHeadline?: string;
editTo?: string;
editFrom?: string;
type?: string;
dependingTicketId?: number | null;
createdOn?: string;
editedOn?: string | null;
assignedTo?: number[];
}
export interface UseTasksOptions {
limit?: number;
includeDone?: boolean;
sortField?: 'dateToFinish' | 'createdOn' | 'status';
sortDirection?: 'asc' | 'desc';
}
export function useTasks({
limit = 10,
includeDone = false,
sortField = 'dateToFinish',
sortDirection = 'asc'
}: UseTasksOptions = {}) {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const refresh = () => {
setRefreshTrigger(prev => prev + 1);
};
useEffect(() => {
const fetchTasks = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/leantime/tasks');
if (!response.ok) {
throw new Error('Failed to fetch tasks');
}
const data = await response.json();
if (!Array.isArray(data) && !Array.isArray(data.tasks)) {
setTasks([]);
return;
}
// Handle both API response formats
const tasksArray = Array.isArray(data) ? data : data.tasks;
// Filter and sort tasks
const filteredTasks = tasksArray
.filter((task: Task) => {
// Include done tasks only if specified
if (!includeDone && task.status === 5) {
return false;
}
return true;
})
.sort((a: Task, b: Task) => {
// Sort by date
if (sortField === 'dateToFinish') {
const dateA = getValidDate(a);
const dateB = getValidDate(b);
// If both dates are valid, compare them
if (dateA && dateB) {
const timeA = new Date(dateA).getTime();
const timeB = new Date(dateB).getTime();
if (timeA !== timeB) {
return sortDirection === 'asc' ? timeA - timeB : timeB - timeA;
}
}
// If only one date is valid, put the task with a date first
if (dateA) return sortDirection === 'asc' ? -1 : 1;
if (dateB) return sortDirection === 'asc' ? 1 : -1;
}
// Sort by created date
if (sortField === 'createdOn') {
const dateA = a.createdOn ? new Date(a.createdOn).getTime() : 0;
const dateB = b.createdOn ? new Date(b.createdOn).getTime() : 0;
return sortDirection === 'asc' ? dateA - dateB : dateB - dateA;
}
// Sort by status
if (sortField === 'status') {
const statusA = a.status || 0;
const statusB = b.status || 0;
return sortDirection === 'asc' ? statusA - statusB : statusB - statusA;
}
return 0;
});
setTasks(limit ? filteredTasks.slice(0, limit) : filteredTasks);
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
} finally {
setLoading(false);
}
};
fetchTasks();
}, [limit, includeDone, sortField, sortDirection, refreshTrigger]);
return { tasks, loading, error, refresh };
}
// Helper function
function getValidDate(task: Task): string | null {
if (task.dateToFinish && isValidDateString(task.dateToFinish)) {
return task.dateToFinish;
}
if (task.dueDate && isValidDateString(task.dueDate)) {
return task.dueDate;
}
return null;
}

92
lib/utils/date-utils.ts Normal file
View File

@ -0,0 +1,92 @@
import { format, isToday, isTomorrow, formatDistance } from 'date-fns';
import { fr } from 'date-fns/locale';
/**
* Format a date as a short date string (e.g., "15 jun.")
*/
export function formatShortDate(date: Date | string): string {
const dateObj = date instanceof Date ? date : new Date(date);
return format(dateObj, 'd MMM', { locale: fr });
}
/**
* Format a time as a 24-hour time string (e.g., "14:30")
*/
export function formatTime(date: Date | string): string {
const dateObj = date instanceof Date ? date : new Date(date);
return format(dateObj, 'HH:mm', { locale: fr });
}
/**
* Format a date as a relative string (e.g., "Today", "Tomorrow", "Monday")
*/
export function formatRelativeDate(date: Date | string, includeTime: boolean = false): string {
const dateObj = date instanceof Date ? date : new Date(date);
let dateString = "";
if (isToday(dateObj)) {
dateString = "Today";
} else if (isTomorrow(dateObj)) {
dateString = "Tomorrow";
} else {
dateString = format(dateObj, 'EEEE d MMMM', { locale: fr });
}
if (includeTime) {
dateString += ` · ${format(dateObj, 'HH:mm', { locale: fr })}`;
}
return dateString;
}
/**
* Format a date as a relative time ago (e.g., "5 minutes ago", "2 hours ago")
*/
export function formatTimeAgo(date: Date | string): string {
const dateObj = date instanceof Date ? date : new Date(date);
return formatDistance(dateObj, new Date(), {
addSuffix: true,
locale: fr,
});
}
/**
* Format a date for display in isoformat
*/
export function formatISODate(date: Date | string): string {
const dateObj = date instanceof Date ? date : new Date(date);
return dateObj.toISOString();
}
/**
* Check if a date string is valid
*/
export function isValidDateString(dateStr: string): boolean {
if (!dateStr || dateStr === '0000-00-00 00:00:00') return false;
try {
const date = new Date(dateStr);
return !isNaN(date.getTime());
} catch (e) {
return false;
}
}
/**
* Get a formatted month name
*/
export function getMonthName(month: number, short: boolean = false): string {
const date = new Date();
date.setMonth(month);
return format(date, short ? 'MMM' : 'MMMM', { locale: fr });
}
/**
* Get a formatted day name
*/
export function getDayName(day: number, short: boolean = false): string {
const date = new Date();
date.setDate(day);
return format(date, short ? 'EEE' : 'EEEE', { locale: fr });
}

55
lib/utils/status-utils.ts Normal file
View File

@ -0,0 +1,55 @@
/**
* Task status labels
*/
export function getTaskStatusLabel(status: number): string {
switch (status) {
case 1: return 'New';
case 2: return 'Blocked';
case 3: return 'In Progress';
case 4: return 'Waiting for Approval';
case 5: return 'Done';
default: return 'Unknown';
}
}
/**
* Task status colors (tailwind classes)
*/
export function getTaskStatusColor(status: number): string {
switch (status) {
case 1: return 'bg-blue-500'; // New - blue
case 2: return 'bg-red-500'; // Blocked - red
case 3: return 'bg-yellow-500'; // In Progress - yellow
case 4: return 'bg-purple-500'; // Waiting for Approval - purple
case 5: return 'bg-gray-500'; // Done - gray
default: return 'bg-gray-300';
}
}
/**
* Task status badge classes
*/
export function getTaskStatusBadgeClass(status: number): string {
switch (status) {
case 1: return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
case 2: return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
case 3: return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
case 4: return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';
case 5: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
}
}
/**
* Email status colors
*/
export function getEmailStatusColor(status: 'read' | 'unread' | 'draft' | 'sent' | 'flagged'): string {
switch (status) {
case 'read': return 'text-gray-400';
case 'unread': return 'text-blue-500';
case 'draft': return 'text-amber-500';
case 'sent': return 'text-green-500';
case 'flagged': return 'text-red-500';
default: return 'text-gray-400';
}
}