feat: add Weekly Check-in feature with models, UI components, and session management for enhanced team collaboration
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m24s

This commit is contained in:
Julien Froidefond
2026-01-14 10:23:58 +01:00
parent 67d685d346
commit 53ee344ae7
23 changed files with 2753 additions and 24 deletions

View File

@@ -15,10 +15,18 @@ import {
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
import { deleteWeeklyCheckInSession, updateWeeklyCheckInSession } from '@/actions/weekly-checkin';
type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'byPerson';
type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'weekly-checkin' | 'byPerson';
const VALID_TABS: WorkshopType[] = ['all', 'swot', 'motivators', 'year-review', 'byPerson'];
const VALID_TABS: WorkshopType[] = [
'all',
'swot',
'motivators',
'year-review',
'weekly-checkin',
'byPerson',
];
interface ShareUser {
id: string;
@@ -84,12 +92,28 @@ interface YearReviewSession {
workshopType: 'year-review';
}
type AnySession = SwotSession | MotivatorSession | YearReviewSession;
interface WeeklyCheckInSession {
id: string;
title: string;
participant: string;
resolvedParticipant: ResolvedCollaborator;
date: Date;
updatedAt: Date;
isOwner: boolean;
role: 'OWNER' | 'VIEWER' | 'EDITOR';
user: { id: string; name: string | null; email: string };
shares: Share[];
_count: { items: number };
workshopType: 'weekly-checkin';
}
type AnySession = SwotSession | MotivatorSession | YearReviewSession | WeeklyCheckInSession;
interface WorkshopTabsProps {
swotSessions: SwotSession[];
motivatorSessions: MotivatorSession[];
yearReviewSessions: YearReviewSession[];
weeklyCheckInSessions: WeeklyCheckInSession[];
}
// Helper to get resolved collaborator from any session
@@ -98,6 +122,8 @@ function getResolvedCollaborator(session: AnySession): ResolvedCollaborator {
return (session as SwotSession).resolvedCollaborator;
} else if (session.workshopType === 'year-review') {
return (session as YearReviewSession).resolvedParticipant;
} else if (session.workshopType === 'weekly-checkin') {
return (session as WeeklyCheckInSession).resolvedParticipant;
} else {
return (session as MotivatorSession).resolvedParticipant;
}
@@ -141,6 +167,7 @@ export function WorkshopTabs({
swotSessions,
motivatorSessions,
yearReviewSessions,
weeklyCheckInSessions,
}: WorkshopTabsProps) {
const searchParams = useSearchParams();
const router = useRouter();
@@ -165,6 +192,7 @@ export function WorkshopTabs({
...swotSessions,
...motivatorSessions,
...yearReviewSessions,
...weeklyCheckInSessions,
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
// Filter based on active tab (for non-byPerson tabs)
@@ -175,7 +203,9 @@ export function WorkshopTabs({
? swotSessions
: activeTab === 'motivators'
? motivatorSessions
: yearReviewSessions;
: activeTab === 'year-review'
? yearReviewSessions
: weeklyCheckInSessions;
// Separate by ownership
const ownedSessions = filteredSessions.filter((s) => s.isOwner);
@@ -226,6 +256,13 @@ export function WorkshopTabs({
label="Year Review"
count={yearReviewSessions.length}
/>
<TabButton
active={activeTab === 'weekly-checkin'}
onClick={() => setActiveTab('weekly-checkin')}
icon="📝"
label="Weekly Check-in"
count={weeklyCheckInSessions.length}
/>
</div>
{/* Sessions */}
@@ -343,18 +380,29 @@ function SessionCard({ session }: { session: AnySession }) {
const isSwot = session.workshopType === 'swot';
const isYearReview = session.workshopType === 'year-review';
const isWeeklyCheckIn = session.workshopType === 'weekly-checkin';
const href = isSwot
? `/sessions/${session.id}`
: isYearReview
? `/year-review/${session.id}`
: `/motivators/${session.id}`;
const icon = isSwot ? '📊' : isYearReview ? '📅' : '🎯';
: isWeeklyCheckIn
? `/weekly-checkin/${session.id}`
: `/motivators/${session.id}`;
const icon = isSwot ? '📊' : isYearReview ? '📅' : isWeeklyCheckIn ? '📝' : '🎯';
const participant = isSwot
? (session as SwotSession).collaborator
: isYearReview
? (session as YearReviewSession).participant
: (session as MotivatorSession).participant;
const accentColor = isSwot ? '#06b6d4' : isYearReview ? '#f59e0b' : '#8b5cf6';
: isWeeklyCheckIn
? (session as WeeklyCheckInSession).participant
: (session as MotivatorSession).participant;
const accentColor = isSwot
? '#06b6d4'
: isYearReview
? '#f59e0b'
: isWeeklyCheckIn
? '#10b981'
: '#8b5cf6';
const handleDelete = () => {
startTransition(async () => {
@@ -362,7 +410,9 @@ function SessionCard({ session }: { session: AnySession }) {
? await deleteSwotSession(session.id)
: isYearReview
? await deleteYearReviewSession(session.id)
: await deleteMotivatorSession(session.id);
: isWeeklyCheckIn
? await deleteWeeklyCheckInSession(session.id)
: await deleteMotivatorSession(session.id);
if (result.success) {
setShowDeleteModal(false);
@@ -381,10 +431,15 @@ function SessionCard({ session }: { session: AnySession }) {
title: editTitle,
participant: editParticipant,
})
: await updateMotivatorSession(session.id, {
title: editTitle,
participant: editParticipant,
});
: isWeeklyCheckIn
? await updateWeeklyCheckInSession(session.id, {
title: editTitle,
participant: editParticipant,
})
: await updateMotivatorSession(session.id, {
title: editTitle,
participant: editParticipant,
});
if (result.success) {
setShowEditModal(false);
@@ -401,6 +456,8 @@ function SessionCard({ session }: { session: AnySession }) {
setShowEditModal(true);
};
const editParticipantLabel = isSwot ? 'Collaborateur' : 'Participant';
return (
<>
<div className="relative group">
@@ -456,6 +513,17 @@ function SessionCard({ session }: { session: AnySession }) {
<span>·</span>
<span>Année {(session as YearReviewSession).year}</span>
</>
) : isWeeklyCheckIn ? (
<>
<span>{(session as WeeklyCheckInSession)._count.items} items</span>
<span>·</span>
<span>
{new Date((session as WeeklyCheckInSession).date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
})}
</span>
</>
) : (
<span>{(session as MotivatorSession)._count.cards}/10</span>
)}
@@ -570,7 +638,7 @@ function SessionCard({ session }: { session: AnySession }) {
htmlFor="edit-participant"
className="block text-sm font-medium text-foreground mb-1"
>
{isSwot ? 'Collaborateur' : 'Participant'}
{editParticipantLabel}
</label>
<Input
id="edit-participant"

View File

@@ -4,6 +4,7 @@ import { auth } from '@/lib/auth';
import { getSessionsByUserId } from '@/services/sessions';
import { getMotivatorSessionsByUserId } from '@/services/moving-motivators';
import { getYearReviewSessionsByUserId } from '@/services/year-review';
import { getWeeklyCheckInSessionsByUserId } from '@/services/weekly-checkin';
import { Card, Button } from '@/components/ui';
import { WorkshopTabs } from './WorkshopTabs';
@@ -33,12 +34,14 @@ export default async function SessionsPage() {
return null;
}
// Fetch SWOT, Moving Motivators, and Year Review sessions
const [swotSessions, motivatorSessions, yearReviewSessions] = await Promise.all([
getSessionsByUserId(session.user.id),
getMotivatorSessionsByUserId(session.user.id),
getYearReviewSessionsByUserId(session.user.id),
]);
// Fetch SWOT, Moving Motivators, Year Review, and Weekly Check-in sessions
const [swotSessions, motivatorSessions, yearReviewSessions, weeklyCheckInSessions] =
await Promise.all([
getSessionsByUserId(session.user.id),
getMotivatorSessionsByUserId(session.user.id),
getYearReviewSessionsByUserId(session.user.id),
getWeeklyCheckInSessionsByUserId(session.user.id),
]);
// Add type to each session for unified display
const allSwotSessions = swotSessions.map((s) => ({
@@ -56,11 +59,17 @@ export default async function SessionsPage() {
workshopType: 'year-review' as const,
}));
const allWeeklyCheckInSessions = weeklyCheckInSessions.map((s) => ({
...s,
workshopType: 'weekly-checkin' as const,
}));
// Combine and sort by updatedAt
const allSessions = [
...allSwotSessions,
...allMotivatorSessions,
...allYearReviewSessions,
...allWeeklyCheckInSessions,
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
const hasNoSessions = allSessions.length === 0;
@@ -87,11 +96,17 @@ export default async function SessionsPage() {
</Button>
</Link>
<Link href="/year-review/new">
<Button>
<Button variant="outline">
<span>📅</span>
Nouveau Year Review
</Button>
</Link>
<Link href="/weekly-checkin/new">
<Button>
<span>📝</span>
Nouveau Check-in
</Button>
</Link>
</div>
</div>
@@ -104,9 +119,10 @@ export default async function SessionsPage() {
</h2>
<p className="text-muted mb-6 max-w-md mx-auto">
Créez un atelier SWOT pour analyser les forces et faiblesses, un Moving Motivators pour
découvrir les motivations, ou un Year Review pour faire le bilan de l&apos;année.
découvrir les motivations, un Year Review pour faire le bilan de l&apos;année, ou un
Weekly Check-in pour le suivi hebdomadaire.
</p>
<div className="flex gap-3 justify-center">
<div className="flex gap-3 justify-center flex-wrap">
<Link href="/sessions/new">
<Button variant="outline">
<span>📊</span>
@@ -120,11 +136,17 @@ export default async function SessionsPage() {
</Button>
</Link>
<Link href="/year-review/new">
<Button>
<Button variant="outline">
<span>📅</span>
Créer un Year Review
</Button>
</Link>
<Link href="/weekly-checkin/new">
<Button>
<span>📝</span>
Créer un Check-in
</Button>
</Link>
</div>
</Card>
) : (
@@ -133,6 +155,7 @@ export default async function SessionsPage() {
swotSessions={allSwotSessions}
motivatorSessions={allMotivatorSessions}
yearReviewSessions={allYearReviewSessions}
weeklyCheckInSessions={allWeeklyCheckInSessions}
/>
</Suspense>
)}