fix: lint

This commit is contained in:
Julien Froidefond
2025-09-16 22:13:28 +02:00
parent 122a47f232
commit 4f137455f4
19 changed files with 290 additions and 105 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.env
.env.local
.env.production.local
.env.development.local
.git
.gitignore
.next
.vscode
.idea
*.swp
*.swo
.DS_Store
coverage
.nyc_output
*.log
dist
build
.cache
.turbo

80
Dockerfile Normal file
View File

@@ -0,0 +1,80 @@
# Multi-stage Dockerfile for Next.js with Prisma
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN \
if [ -f package-lock.json ]; then npm ci; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set a dummy DATABASE_URL for build time (Prisma needs it to generate client)
ENV DATABASE_URL="file:/tmp/build.db"
# Generate Prisma client
RUN npx prisma generate
# Initialize the database schema for build time
RUN npx prisma migrate deploy || npx prisma db push
# Build the application
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
# Set timezone to Europe/Paris
RUN apk add --no-cache tzdata
RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy the public folder
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy Prisma files
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
# Create data directory for SQLite
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
# Set all ENV vars before switching user
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV TZ=Europe/Paris
USER nextjs
EXPOSE 3000
# Start the application with database migration
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]

View File

@@ -109,8 +109,6 @@
- [x] Auto-création du daily du jour si inexistant - [x] Auto-création du daily du jour si inexistant
- [x] UX améliorée : édition au clic, focus persistant, input large - [x] UX améliorée : édition au clic, focus persistant, input large
- [x] Vue calendar/historique des dailies - [x] Vue calendar/historique des dailies
- [ ] Templates de daily personnalisables
- [ ] Recherche dans l'historique des dailies
### 3.2 Intégration Jira Cloud ### 3.2 Intégration Jira Cloud
- [ ] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud - [ ] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
@@ -10,7 +10,11 @@ interface DailyCalendarProps {
dailyDates: string[]; // Liste des dates qui ont des dailies (format YYYY-MM-DD) dailyDates: string[]; // Liste des dates qui ont des dailies (format YYYY-MM-DD)
} }
export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCalendarProps) { export function DailyCalendar({
currentDate,
onDateSelect,
dailyDates,
}: DailyCalendarProps) {
const [viewDate, setViewDate] = useState(new Date(currentDate)); const [viewDate, setViewDate] = useState(new Date(currentDate));
// Formatage des dates pour comparaison (éviter le décalage timezone) // Formatage des dates pour comparaison (éviter le décalage timezone)
@@ -46,31 +50,32 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
const getDaysInMonth = () => { const getDaysInMonth = () => {
const year = viewDate.getFullYear(); const year = viewDate.getFullYear();
const month = viewDate.getMonth(); const month = viewDate.getMonth();
// Premier jour du mois // Premier jour du mois
const firstDay = new Date(year, month, 1); const firstDay = new Date(year, month, 1);
// Dernier jour du mois // Dernier jour du mois
const lastDay = new Date(year, month + 1, 0); const lastDay = new Date(year, month + 1, 0);
// Premier lundi de la semaine contenant le premier jour // Premier lundi de la semaine contenant le premier jour
const startDate = new Date(firstDay); const startDate = new Date(firstDay);
const dayOfWeek = firstDay.getDay(); const dayOfWeek = firstDay.getDay();
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0 const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0
startDate.setDate(firstDay.getDate() - daysToSubtract); startDate.setDate(firstDay.getDate() - daysToSubtract);
// Générer toutes les dates du calendrier (6 semaines) // Générer toutes les dates du calendrier (6 semaines)
const days = []; const days = [];
const currentDay = new Date(startDate); const currentDay = new Date(startDate);
for (let i = 0; i < 42; i++) { // 6 semaines × 7 jours for (let i = 0; i < 42; i++) {
// 6 semaines × 7 jours
days.push(new Date(currentDay)); days.push(new Date(currentDay));
currentDay.setDate(currentDay.getDate() + 1); currentDay.setDate(currentDay.getDate() + 1);
} }
return { days, firstDay, lastDay }; return { days, firstDay, lastDay };
}; };
const { days, firstDay, lastDay } = getDaysInMonth(); const { days } = getDaysInMonth();
const handleDateClick = (date: Date) => { const handleDateClick = (date: Date) => {
onDateSelect(date); onDateSelect(date);
@@ -96,7 +101,7 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
const formatMonthYear = () => { const formatMonthYear = () => {
return viewDate.toLocaleDateString('fr-FR', { return viewDate.toLocaleDateString('fr-FR', {
month: 'long', month: 'long',
year: 'numeric' year: 'numeric',
}); });
}; };
@@ -114,11 +119,11 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
> >
</Button> </Button>
<h3 className="text-lg font-bold text-[var(--foreground)] capitalize"> <h3 className="text-lg font-bold text-[var(--foreground)] capitalize">
{formatMonthYear()} {formatMonthYear()}
</h3> </h3>
<Button <Button
onClick={goToNextMonth} onClick={goToNextMonth}
variant="ghost" variant="ghost"
@@ -131,12 +136,8 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
{/* Bouton Aujourd'hui */} {/* Bouton Aujourd'hui */}
<div className="mb-4 text-center"> <div className="mb-4 text-center">
<Button <Button onClick={goToToday} variant="primary" size="sm">
onClick={goToToday} Aujourd&apos;hui
variant="primary"
size="sm"
>
Aujourd'hui
</Button> </Button>
</div> </div>
@@ -155,7 +156,6 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
{/* Grille du calendrier */} {/* Grille du calendrier */}
<div className="grid grid-cols-7 gap-1"> <div className="grid grid-cols-7 gap-1">
{days.map((date, index) => { {days.map((date, index) => {
const dateKey = formatDateKey(date);
const isCurrentMonthDay = isCurrentMonth(date); const isCurrentMonthDay = isCurrentMonth(date);
const isTodayDay = isToday(date); const isTodayDay = isToday(date);
const hasCheckboxes = hasDaily(date); const hasCheckboxes = hasDaily(date);
@@ -167,35 +167,30 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
onClick={() => handleDateClick(date)} onClick={() => handleDateClick(date)}
className={` className={`
relative p-2 text-sm rounded transition-all hover:bg-[var(--muted)]/50 relative p-2 text-sm rounded transition-all hover:bg-[var(--muted)]/50
${isCurrentMonthDay ${
? 'text-[var(--foreground)]' isCurrentMonthDay
: 'text-[var(--muted-foreground)]' ? 'text-[var(--foreground)]'
: 'text-[var(--muted-foreground)]'
} }
${isTodayDay ${
? 'bg-[var(--primary)]/20 border border-[var(--primary)]' isTodayDay
: '' ? 'bg-[var(--primary)]/20 border border-[var(--primary)]'
} : ''
${isSelectedDay
? 'bg-[var(--primary)] text-white'
: ''
}
${hasCheckboxes
? 'font-bold'
: ''
} }
${isSelectedDay ? 'bg-[var(--primary)] text-white' : ''}
${hasCheckboxes ? 'font-bold' : ''}
`} `}
> >
{date.getDate()} {date.getDate()}
{/* Indicateur de daily existant */} {/* Indicateur de daily existant */}
{hasCheckboxes && ( {hasCheckboxes && (
<div className={` <div
className={`
absolute bottom-1 right-1 w-2 h-2 rounded-full absolute bottom-1 right-1 w-2 h-2 rounded-full
${isSelectedDay ${isSelectedDay ? 'bg-white' : 'bg-[var(--primary)]'}
? 'bg-white' `}
: 'bg-[var(--primary)]' />
}
`} />
)} )}
</button> </button>
); );
@@ -210,7 +205,7 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border border-[var(--primary)] bg-[var(--primary)]/20"></div> <div className="w-4 h-4 rounded border border-[var(--primary)] bg-[var(--primary)]/20"></div>
<span>Aujourd'hui</span> <span>Aujourd&apos;hui</span>
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -158,7 +158,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
value={formData.color} value={formData.color}
onChange={(e) => { onChange={(e) => {
if (TagsClient.isValidColor(e.target.value)) { if (TagsClient.isValidColor(e.target.value)) {
handleCustomColorChange(e as any); handleCustomColorChange(e as React.ChangeEvent<HTMLInputElement>);
} }
}} }}
placeholder="#RRGGBB" placeholder="#RRGGBB"
@@ -189,7 +189,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag
</label> </label>
</div> </div>
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
Les tâches avec ce tag apparaîtront dans la section "Objectifs Principaux" au-dessus du Kanban Les tâches avec ce tag apparaîtront dans la section &quot;Objectifs Principaux&quot; au-dessus du Kanban
</p> </p>
</div> </div>

View File

@@ -6,7 +6,8 @@ import { useMemo } from 'react';
import { getAllPriorities } from '@/lib/status-config'; import { getAllPriorities } from '@/lib/status-config';
import { SwimlanesBase, SwimlaneData } from './SwimlanesBase'; import { SwimlanesBase, SwimlaneData } from './SwimlanesBase';
interface PrioritySwimlanesoardProps { interface PrioritySwimlanesBoardProps {
loading: boolean;
tasks: Task[]; tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>; onDeleteTask?: (taskId: string) => Promise<void>;
@@ -15,40 +16,37 @@ interface PrioritySwimlanesoardProps {
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
visibleStatuses?: TaskStatus[]; visibleStatuses?: TaskStatus[];
loading?: boolean;
} }
export function PrioritySwimlanesBoard({ export function PrioritySwimlanesBoard({
tasks, tasks,
onCreateTask, onCreateTask,
onDeleteTask, onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle, onUpdateTitle,
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
visibleStatuses, visibleStatuses,
loading = false }: PrioritySwimlanesBoardProps) {
}: PrioritySwimlanesoardProps) {
// Grouper les tâches par priorités et créer les données de swimlanes // Grouper les tâches par priorités et créer les données de swimlanes
const swimlanesData = useMemo((): SwimlaneData[] => { const swimlanesData = useMemo((): SwimlaneData[] => {
const grouped: { [priorityKey: string]: Task[] } = {}; const grouped: { [priorityKey: string]: Task[] } = {};
// Initialiser avec toutes les priorités // Initialiser avec toutes les priorités
getAllPriorities().forEach(priority => { getAllPriorities().forEach((priority) => {
grouped[priority.key] = []; grouped[priority.key] = [];
}); });
tasks.forEach(task => { tasks.forEach((task) => {
if (grouped[task.priority]) { if (grouped[task.priority]) {
grouped[task.priority].push(task); grouped[task.priority].push(task);
} }
}); });
// Convertir en format SwimlaneData en respectant l'ordre de priorité // Convertir en format SwimlaneData en respectant l'ordre de priorité
return getAllPriorities() return getAllPriorities()
.sort((a, b) => b.order - a.order) // Ordre décroissant - plus importantes en haut .sort((a, b) => b.order - a.order) // Ordre décroissant - plus importantes en haut
.map(priority => ({ .map((priority) => ({
key: priority.key, key: priority.key,
label: priority.label, label: priority.label,
icon: priority.icon, icon: priority.icon,
@@ -56,8 +54,8 @@ export function PrioritySwimlanesBoard({
tasks: grouped[priority.key] || [], tasks: grouped[priority.key] || [],
context: { context: {
type: 'priority' as const, type: 'priority' as const,
value: priority.key value: priority.key,
} },
})); }));
}, [tasks]); }, [tasks]);
@@ -74,4 +72,4 @@ export function PrioritySwimlanesBoard({
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}
/> />
); );
} }

View File

@@ -15,34 +15,33 @@ interface SwimlanesboardProps {
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
visibleStatuses?: TaskStatus[]; visibleStatuses?: TaskStatus[];
loading?: boolean; loading: boolean;
} }
export function SwimlanesBoard({ export function SwimlanesBoard({
tasks, tasks,
onCreateTask, onCreateTask,
onDeleteTask, onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle, onUpdateTitle,
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
visibleStatuses, visibleStatuses,
loading = false
}: SwimlanesboardProps) { }: SwimlanesboardProps) {
const { tags: availableTags } = useTasksContext(); const { tags: availableTags } = useTasksContext();
// Grouper les tâches par tags et créer les données de swimlanes // Grouper les tâches par tags et créer les données de swimlanes
const swimlanesData = useMemo((): SwimlaneData[] => { const swimlanesData = useMemo((): SwimlaneData[] => {
const grouped: { [tagName: string]: Task[] } = {}; const grouped: { [tagName: string]: Task[] } = {};
// Ajouter une catégorie pour les tâches sans tags // Ajouter une catégorie pour les tâches sans tags
grouped['Sans tag'] = []; grouped['Sans tag'] = [];
tasks.forEach(task => { tasks.forEach((task) => {
if (!task.tags || task.tags.length === 0) { if (!task.tags || task.tags.length === 0) {
grouped['Sans tag'].push(task); grouped['Sans tag'].push(task);
} else { } else {
task.tags.forEach(tagName => { task.tags.forEach((tagName) => {
if (!grouped[tagName]) { if (!grouped[tagName]) {
grouped[tagName] = []; grouped[tagName] = [];
} }
@@ -50,7 +49,7 @@ export function SwimlanesBoard({
}); });
} }
}); });
// Convertir en format SwimlaneData et trier // Convertir en format SwimlaneData et trier
return Object.entries(grouped) return Object.entries(grouped)
.sort(([a, tasksA], [b, tasksB]) => { .sort(([a, tasksA], [b, tasksB]) => {
@@ -64,7 +63,7 @@ export function SwimlanesBoard({
// Obtenir la couleur du tag // Obtenir la couleur du tag
const getTagColor = (name: string) => { const getTagColor = (name: string) => {
if (name === 'Sans tag') return '#64748b'; // slate-500 if (name === 'Sans tag') return '#64748b'; // slate-500
const tag = availableTags.find(t => t.name === name); const tag = availableTags.find((t) => t.name === name);
return tag?.color || '#64748b'; return tag?.color || '#64748b';
}; };
@@ -73,10 +72,13 @@ export function SwimlanesBoard({
label: tagName, label: tagName,
color: getTagColor(tagName), color: getTagColor(tagName),
tasks: tagTasks, tasks: tagTasks,
context: tagName !== 'Sans tag' ? { context:
type: 'tag' as const, tagName !== 'Sans tag'
value: tagName ? {
} : undefined type: 'tag' as const,
value: tagName,
}
: undefined,
}; };
}); });
}, [tasks, availableTags]); }, [tasks, availableTags]);
@@ -94,4 +96,4 @@ export function SwimlanesBoard({
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}
/> />
); );
} }

View File

@@ -11,6 +11,10 @@ interface HeaderContainerProps {
completed: number; completed: number;
inProgress: number; inProgress: number;
todo: number; todo: number;
backlog: number;
cancelled: number;
freeze: number;
archived: number;
completionRate: number; completionRate: number;
}; };
} }
@@ -18,7 +22,7 @@ interface HeaderContainerProps {
export function HeaderContainer({ title, subtitle, initialStats }: HeaderContainerProps) { export function HeaderContainer({ title, subtitle, initialStats }: HeaderContainerProps) {
const { stats, syncing } = useTasks( const { stats, syncing } = useTasks(
{ limit: 1 }, // Juste pour les stats { limit: 1 }, // Juste pour les stats
{ tasks: [], stats: initialStats } { tasks: [], stats: { ...initialStats, backlog: 0, cancelled: 0, freeze: 0, archived: 0 } }
); );
return ( return (

52
docker-compose.yml Normal file
View File

@@ -0,0 +1,52 @@
version: '3.8'
services:
towercontrol:
build:
context: .
dockerfile: Dockerfile
ports:
- "3006:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=file:/app/data/prod.db
- TZ=Europe/Paris
volumes:
# Volume persistant pour la base SQLite
- sqlite_data:/app/data
# Monter ta DB locale (décommente pour utiliser tes données locales)
- ./prisma/dev.db:/app/data/prod.db
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Service de développement (optionnel)
towercontrol-dev:
build:
context: .
dockerfile: Dockerfile
target: base
ports:
- "3005:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=file:/app/data/dev.db
volumes:
- .:/app
- /app/node_modules
- /app/.next
- sqlite_data_dev:/app/data
command: sh -c "npm install && npx prisma generate && npx prisma migrate deploy && npm run dev"
profiles:
- dev
volumes:
sqlite_data:
driver: local
sqlite_data_dev:
driver: local

View File

@@ -35,6 +35,7 @@ export function useTasks(
completed: 0, completed: 0,
inProgress: 0, inProgress: 0,
todo: 0, todo: 0,
backlog: 0,
cancelled: 0, cancelled: 0,
freeze: 0, freeze: 0,
archived: 0, archived: 0,
@@ -144,6 +145,7 @@ export function useTasks(
completed: updatedTasks.filter(t => t.status === 'done').length, completed: updatedTasks.filter(t => t.status === 'done').length,
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length, inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
todo: updatedTasks.filter(t => t.status === 'todo').length, todo: updatedTasks.filter(t => t.status === 'todo').length,
backlog: updatedTasks.filter(t => t.status === 'backlog').length,
cancelled: updatedTasks.filter(t => t.status === 'cancelled').length, cancelled: updatedTasks.filter(t => t.status === 'cancelled').length,
freeze: updatedTasks.filter(t => t.status === 'freeze').length, freeze: updatedTasks.filter(t => t.status === 'freeze').length,
archived: updatedTasks.filter(t => t.status === 'archived').length, archived: updatedTasks.filter(t => t.status === 'archived').length,
@@ -188,6 +190,7 @@ export function useTasks(
completed: currentTasks.filter(t => t.status === 'done').length, completed: currentTasks.filter(t => t.status === 'done').length,
inProgress: currentTasks.filter(t => t.status === 'in_progress').length, inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
todo: currentTasks.filter(t => t.status === 'todo').length, todo: currentTasks.filter(t => t.status === 'todo').length,
backlog: currentTasks.filter(t => t.status === 'backlog').length,
cancelled: currentTasks.filter(t => t.status === 'cancelled').length, cancelled: currentTasks.filter(t => t.status === 'cancelled').length,
freeze: currentTasks.filter(t => t.status === 'freeze').length, freeze: currentTasks.filter(t => t.status === 'freeze').length,
archived: currentTasks.filter(t => t.status === 'archived').length, archived: currentTasks.filter(t => t.status === 'archived').length,

View File

@@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: 'standalone',
experimental: {
turbo: {
rules: {
'*.sql': ['raw'],
},
},
},
}; };
export default nextConfig; export default nextConfig;

View File

@@ -1,5 +1,5 @@
{ {
"name": "towercontrol-temp", "name": "towercontrol",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -43,10 +43,13 @@ async function seedTestData() {
const task = await tasksService.createTask(taskData); const task = await tasksService.createTask(taskData);
const statusEmoji = { const statusEmoji = {
'backlog': '📋',
'todo': '⏳', 'todo': '⏳',
'in_progress': '🔄', 'in_progress': '🔄',
'freeze': '🧊',
'done': '✅', 'done': '✅',
'cancelled': '❌' 'cancelled': '❌',
'archived': '📦'
}[task.status]; }[task.status];
const priorityEmoji = { const priorityEmoji = {

View File

@@ -1,5 +1,6 @@
import { prisma } from './database'; import { prisma } from './database';
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType } from '@/lib/types'; import { Prisma } from '@prisma/client';
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
/** /**
* Service pour la gestion des checkboxes daily * Service pour la gestion des checkboxes daily
@@ -82,12 +83,18 @@ export class DailyService {
* Met à jour une checkbox * Met à jour une checkbox
*/ */
async updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox> { async updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox> {
const updateData: any = {}; const updateData: Prisma.DailyCheckboxUpdateInput = {};
if (data.text !== undefined) updateData.text = data.text.trim(); if (data.text !== undefined) updateData.text = data.text.trim();
if (data.isChecked !== undefined) updateData.isChecked = data.isChecked; if (data.isChecked !== undefined) updateData.isChecked = data.isChecked;
if (data.type !== undefined) updateData.type = data.type; if (data.type !== undefined) updateData.type = data.type;
if (data.taskId !== undefined) updateData.taskId = data.taskId; if (data.taskId !== undefined) {
if (data.taskId === null) {
updateData.task = { disconnect: true };
} else {
updateData.task = { connect: { id: data.taskId } };
}
}
if (data.order !== undefined) updateData.order = data.order; if (data.order !== undefined) updateData.order = data.order;
const checkbox = await prisma.dailyCheckbox.update({ const checkbox = await prisma.dailyCheckbox.update({
@@ -210,7 +217,7 @@ export class DailyService {
/** /**
* Mappe une checkbox Prisma vers notre interface * Mappe une checkbox Prisma vers notre interface
*/ */
private mapPrismaCheckbox(checkbox: any): DailyCheckbox { private mapPrismaCheckbox(checkbox: Prisma.DailyCheckboxGetPayload<{ include: { task: true } }>): DailyCheckbox {
return { return {
id: checkbox.id, id: checkbox.id,
date: checkbox.date, date: checkbox.date,
@@ -218,23 +225,23 @@ export class DailyService {
isChecked: checkbox.isChecked, isChecked: checkbox.isChecked,
type: checkbox.type as DailyCheckboxType, type: checkbox.type as DailyCheckboxType,
order: checkbox.order, order: checkbox.order,
taskId: checkbox.taskId, taskId: checkbox.taskId || undefined,
task: checkbox.task ? { task: checkbox.task ? {
id: checkbox.task.id, id: checkbox.task.id,
title: checkbox.task.title, title: checkbox.task.title,
description: checkbox.task.description, description: checkbox.task.description || undefined,
status: checkbox.task.status, status: checkbox.task.status as TaskStatus,
priority: checkbox.task.priority, priority: checkbox.task.priority as TaskPriority,
source: checkbox.task.source, source: checkbox.task.source as TaskSource,
sourceId: checkbox.task.sourceId, sourceId: checkbox.task.sourceId || undefined,
tags: [], // Les tags seront chargés séparément si nécessaire tags: [], // Les tags seront chargés séparément si nécessaire
dueDate: checkbox.task.dueDate, dueDate: checkbox.task.dueDate || undefined,
completedAt: checkbox.task.completedAt, completedAt: checkbox.task.completedAt || undefined,
createdAt: checkbox.task.createdAt, createdAt: checkbox.task.createdAt,
updatedAt: checkbox.task.updatedAt, updatedAt: checkbox.task.updatedAt,
jiraProject: checkbox.task.jiraProject, jiraProject: checkbox.task.jiraProject || undefined,
jiraKey: checkbox.task.jiraKey, jiraKey: checkbox.task.jiraKey || undefined,
assignee: checkbox.task.assignee assignee: checkbox.task.assignee || undefined
} : undefined, } : undefined,
createdAt: checkbox.createdAt, createdAt: checkbox.createdAt,
updatedAt: checkbox.updatedAt updatedAt: checkbox.updatedAt

View File

@@ -1,4 +1,5 @@
import { prisma } from './database'; import { prisma } from './database';
import { Prisma } from '@prisma/client';
import { Tag } from '@/lib/types'; import { Tag } from '@/lib/types';
/** /**
@@ -113,7 +114,7 @@ export const tagsService = {
} }
} }
const updateData: any = {}; const updateData: Prisma.TagUpdateInput = {};
if (data.name !== undefined) { if (data.name !== undefined) {
updateData.name = data.name.trim(); updateData.name = data.name.trim();
} }

View File

@@ -2,6 +2,9 @@ import { Metadata } from 'next';
import { DailyPageClient } from './DailyPageClient'; import { DailyPageClient } from './DailyPageClient';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/daily';
// Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Daily - Tower Control', title: 'Daily - Tower Control',
description: 'Gestion quotidienne des tâches et objectifs', description: 'Gestion quotidienne des tâches et objectifs',

View File

@@ -2,6 +2,9 @@ import { tasksService } from '@/services/tasks';
import { tagsService } from '@/services/tags'; import { tagsService } from '@/services/tags';
import { HomePageClient } from '@/components/HomePageClient'; import { HomePageClient } from '@/components/HomePageClient';
// Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic';
export default async function HomePage() { export default async function HomePage() {
// SSR - Récupération des données côté serveur // SSR - Récupération des données côté serveur
const [initialTasks, initialStats, initialTags] = await Promise.all([ const [initialTasks, initialStats, initialTags] = await Promise.all([

View File

@@ -1,6 +1,9 @@
import { tagsService } from '@/services/tags'; import { tagsService } from '@/services/tags';
import { TagsPageClient } from './TagsPageClient'; import { TagsPageClient } from './TagsPageClient';
// Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic';
export default async function TagsPage() { export default async function TagsPage() {
// SSR - Récupération des tags côté serveur // SSR - Récupération des tags côté serveur
const initialTags = await tagsService.getTags(); const initialTags = await tagsService.getTags();

View File

@@ -88,7 +88,8 @@ export function TasksProvider({ children, initialTasks, initialStats, initialTag
compactView: newFilters.compactView || false, compactView: newFilters.compactView || false,
swimlanesByTags: newFilters.swimlanesByTags || false, swimlanesByTags: newFilters.swimlanesByTags || false,
swimlanesMode: newFilters.swimlanesMode || 'tags', swimlanesMode: newFilters.swimlanesMode || 'tags',
showObjectives: true // Toujours visible pour l'instant showObjectives: true, // Toujours visible pour l'instant
showFilters: true // Toujours visible pour l'instant
}); });
}; };