fix: lint
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
80
Dockerfile
Normal 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"]
|
||||||
2
TODO.md
2
TODO.md
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -62,7 +66,8 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -70,7 +75,7 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
|
|||||||
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',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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'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'hui</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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 "Objectifs Principaux" au-dessus du Kanban
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +16,6 @@ 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({
|
||||||
@@ -27,19 +27,17 @@ export function PrioritySwimlanesBoard({
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -48,7 +46,7 @@ export function PrioritySwimlanesBoard({
|
|||||||
// 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]);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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({
|
||||||
@@ -27,7 +27,6 @@ export function SwimlanesBoard({
|
|||||||
onUpdateStatus,
|
onUpdateStatus,
|
||||||
compactView = false,
|
compactView = false,
|
||||||
visibleStatuses,
|
visibleStatuses,
|
||||||
loading = false
|
|
||||||
}: SwimlanesboardProps) {
|
}: SwimlanesboardProps) {
|
||||||
const { tags: availableTags } = useTasksContext();
|
const { tags: availableTags } = useTasksContext();
|
||||||
|
|
||||||
@@ -38,11 +37,11 @@ export function SwimlanesBoard({
|
|||||||
// 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] = [];
|
||||||
}
|
}
|
||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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
52
docker-compose.yml
Normal 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
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "towercontrol-temp",
|
"name": "towercontrol",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user