feat: implement Year Review feature with session management, item categorization, and real-time collaboration
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m7s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m7s
This commit is contained in:
@@ -14,10 +14,11 @@ import {
|
||||
} from '@/components/ui';
|
||||
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
|
||||
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
|
||||
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
|
||||
|
||||
type WorkshopType = 'all' | 'swot' | 'motivators' | 'byPerson';
|
||||
type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'byPerson';
|
||||
|
||||
const VALID_TABS: WorkshopType[] = ['all', 'swot', 'motivators', 'byPerson'];
|
||||
const VALID_TABS: WorkshopType[] = ['all', 'swot', 'motivators', 'year-review', 'byPerson'];
|
||||
|
||||
interface ShareUser {
|
||||
id: string;
|
||||
@@ -68,34 +69,38 @@ interface MotivatorSession {
|
||||
workshopType: 'motivators';
|
||||
}
|
||||
|
||||
type AnySession = SwotSession | MotivatorSession;
|
||||
interface YearReviewSession {
|
||||
id: string;
|
||||
title: string;
|
||||
participant: string;
|
||||
resolvedParticipant: ResolvedCollaborator;
|
||||
year: number;
|
||||
updatedAt: Date;
|
||||
isOwner: boolean;
|
||||
role: 'OWNER' | 'VIEWER' | 'EDITOR';
|
||||
user: { id: string; name: string | null; email: string };
|
||||
shares: Share[];
|
||||
_count: { items: number };
|
||||
workshopType: 'year-review';
|
||||
}
|
||||
|
||||
type AnySession = SwotSession | MotivatorSession | YearReviewSession;
|
||||
|
||||
interface WorkshopTabsProps {
|
||||
swotSessions: SwotSession[];
|
||||
motivatorSessions: MotivatorSession[];
|
||||
}
|
||||
|
||||
// Helper to get participant name from any session
|
||||
function getParticipant(session: AnySession): string {
|
||||
return session.workshopType === 'swot'
|
||||
? (session as SwotSession).collaborator
|
||||
: (session as MotivatorSession).participant;
|
||||
yearReviewSessions: YearReviewSession[];
|
||||
}
|
||||
|
||||
// Helper to get resolved collaborator from any session
|
||||
function getResolvedCollaborator(session: AnySession): ResolvedCollaborator {
|
||||
return session.workshopType === 'swot'
|
||||
? (session as SwotSession).resolvedCollaborator
|
||||
: (session as MotivatorSession).resolvedParticipant;
|
||||
}
|
||||
|
||||
// Get display name for grouping - prefer matched user name
|
||||
function getDisplayName(session: AnySession): string {
|
||||
const resolved = getResolvedCollaborator(session);
|
||||
if (resolved.matchedUser?.name) {
|
||||
return resolved.matchedUser.name;
|
||||
if (session.workshopType === 'swot') {
|
||||
return (session as SwotSession).resolvedCollaborator;
|
||||
} else if (session.workshopType === 'year-review') {
|
||||
return (session as YearReviewSession).resolvedParticipant;
|
||||
} else {
|
||||
return (session as MotivatorSession).resolvedParticipant;
|
||||
}
|
||||
return resolved.raw;
|
||||
}
|
||||
|
||||
// Get grouping key - use matched user ID if available, otherwise normalized raw string
|
||||
@@ -132,7 +137,11 @@ function groupByPerson(sessions: AnySession[]): Map<string, AnySession[]> {
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsProps) {
|
||||
export function WorkshopTabs({
|
||||
swotSessions,
|
||||
motivatorSessions,
|
||||
yearReviewSessions,
|
||||
}: WorkshopTabsProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -152,9 +161,11 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
|
||||
};
|
||||
|
||||
// Combine and sort all sessions
|
||||
const allSessions: AnySession[] = [...swotSessions, ...motivatorSessions].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
const allSessions: AnySession[] = [
|
||||
...swotSessions,
|
||||
...motivatorSessions,
|
||||
...yearReviewSessions,
|
||||
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
|
||||
// Filter based on active tab (for non-byPerson tabs)
|
||||
const filteredSessions =
|
||||
@@ -162,7 +173,9 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
|
||||
? allSessions
|
||||
: activeTab === 'swot'
|
||||
? swotSessions
|
||||
: motivatorSessions;
|
||||
: activeTab === 'motivators'
|
||||
? motivatorSessions
|
||||
: yearReviewSessions;
|
||||
|
||||
// Separate by ownership
|
||||
const ownedSessions = filteredSessions.filter((s) => s.isOwner);
|
||||
@@ -206,6 +219,13 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
|
||||
label="Moving Motivators"
|
||||
count={motivatorSessions.length}
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'year-review'}
|
||||
onClick={() => setActiveTab('year-review')}
|
||||
icon="📅"
|
||||
label="Year Review"
|
||||
count={yearReviewSessions.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sessions */}
|
||||
@@ -316,22 +336,33 @@ function SessionCard({ session }: { session: AnySession }) {
|
||||
const [editParticipant, setEditParticipant] = useState(
|
||||
session.workshopType === 'swot'
|
||||
? (session as SwotSession).collaborator
|
||||
: (session as MotivatorSession).participant
|
||||
: session.workshopType === 'year-review'
|
||||
? (session as YearReviewSession).participant
|
||||
: (session as MotivatorSession).participant
|
||||
);
|
||||
|
||||
const isSwot = session.workshopType === 'swot';
|
||||
const href = isSwot ? `/sessions/${session.id}` : `/motivators/${session.id}`;
|
||||
const icon = isSwot ? '📊' : '🎯';
|
||||
const isYearReview = session.workshopType === 'year-review';
|
||||
const href = isSwot
|
||||
? `/sessions/${session.id}`
|
||||
: isYearReview
|
||||
? `/year-review/${session.id}`
|
||||
: `/motivators/${session.id}`;
|
||||
const icon = isSwot ? '📊' : isYearReview ? '📅' : '🎯';
|
||||
const participant = isSwot
|
||||
? (session as SwotSession).collaborator
|
||||
: (session as MotivatorSession).participant;
|
||||
const accentColor = isSwot ? '#06b6d4' : '#8b5cf6';
|
||||
: isYearReview
|
||||
? (session as YearReviewSession).participant
|
||||
: (session as MotivatorSession).participant;
|
||||
const accentColor = isSwot ? '#06b6d4' : isYearReview ? '#f59e0b' : '#8b5cf6';
|
||||
|
||||
const handleDelete = () => {
|
||||
startTransition(async () => {
|
||||
const result = isSwot
|
||||
? await deleteSwotSession(session.id)
|
||||
: await deleteMotivatorSession(session.id);
|
||||
: isYearReview
|
||||
? await deleteYearReviewSession(session.id)
|
||||
: await deleteMotivatorSession(session.id);
|
||||
|
||||
if (result.success) {
|
||||
setShowDeleteModal(false);
|
||||
@@ -345,10 +376,15 @@ function SessionCard({ session }: { session: AnySession }) {
|
||||
startTransition(async () => {
|
||||
const result = isSwot
|
||||
? await updateSwotSession(session.id, { title: editTitle, collaborator: editParticipant })
|
||||
: await updateMotivatorSession(session.id, {
|
||||
title: editTitle,
|
||||
participant: editParticipant,
|
||||
});
|
||||
: isYearReview
|
||||
? await updateYearReviewSession(session.id, {
|
||||
title: editTitle,
|
||||
participant: editParticipant,
|
||||
})
|
||||
: await updateMotivatorSession(session.id, {
|
||||
title: editTitle,
|
||||
participant: editParticipant,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setShowEditModal(false);
|
||||
@@ -414,6 +450,12 @@ function SessionCard({ session }: { session: AnySession }) {
|
||||
<span>·</span>
|
||||
<span>{(session as SwotSession)._count.actions} actions</span>
|
||||
</>
|
||||
) : isYearReview ? (
|
||||
<>
|
||||
<span>{(session as YearReviewSession)._count.items} items</span>
|
||||
<span>·</span>
|
||||
<span>Année {(session as YearReviewSession).year}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{(session as MotivatorSession)._count.cards}/10</span>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from 'next/link';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { getSessionsByUserId } from '@/services/sessions';
|
||||
import { getMotivatorSessionsByUserId } from '@/services/moving-motivators';
|
||||
import { getYearReviewSessionsByUserId } from '@/services/year-review';
|
||||
import { Card, Button } from '@/components/ui';
|
||||
import { WorkshopTabs } from './WorkshopTabs';
|
||||
|
||||
@@ -32,10 +33,11 @@ export default async function SessionsPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch both SWOT and Moving Motivators sessions
|
||||
const [swotSessions, motivatorSessions] = await Promise.all([
|
||||
// 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),
|
||||
]);
|
||||
|
||||
// Add type to each session for unified display
|
||||
@@ -49,10 +51,17 @@ export default async function SessionsPage() {
|
||||
workshopType: 'motivators' as const,
|
||||
}));
|
||||
|
||||
const allYearReviewSessions = yearReviewSessions.map((s) => ({
|
||||
...s,
|
||||
workshopType: 'year-review' as const,
|
||||
}));
|
||||
|
||||
// Combine and sort by updatedAt
|
||||
const allSessions = [...allSwotSessions, ...allMotivatorSessions].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
const allSessions = [
|
||||
...allSwotSessions,
|
||||
...allMotivatorSessions,
|
||||
...allYearReviewSessions,
|
||||
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
|
||||
const hasNoSessions = allSessions.length === 0;
|
||||
|
||||
@@ -72,11 +81,17 @@ export default async function SessionsPage() {
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/motivators/new">
|
||||
<Button>
|
||||
<Button variant="outline">
|
||||
<span>🎯</span>
|
||||
Nouveau Motivators
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/year-review/new">
|
||||
<Button>
|
||||
<span>📅</span>
|
||||
Nouveau Year Review
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,8 +103,8 @@ export default async function SessionsPage() {
|
||||
Commencez votre premier atelier
|
||||
</h2>
|
||||
<p className="text-muted mb-6 max-w-md mx-auto">
|
||||
Créez un atelier SWOT pour analyser les forces et faiblesses, ou un Moving Motivators
|
||||
pour découvrir les motivations de vos collaborateurs.
|
||||
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'année.
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Link href="/sessions/new">
|
||||
@@ -99,16 +114,26 @@ export default async function SessionsPage() {
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/motivators/new">
|
||||
<Button>
|
||||
<Button variant="outline">
|
||||
<span>🎯</span>
|
||||
Créer un Moving Motivators
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/year-review/new">
|
||||
<Button>
|
||||
<span>📅</span>
|
||||
Créer un Year Review
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Suspense fallback={<WorkshopTabsSkeleton />}>
|
||||
<WorkshopTabs swotSessions={allSwotSessions} motivatorSessions={allMotivatorSessions} />
|
||||
<WorkshopTabs
|
||||
swotSessions={allSwotSessions}
|
||||
motivatorSessions={allMotivatorSessions}
|
||||
yearReviewSessions={allYearReviewSessions}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user