feat: implement Weather Workshop feature with models, UI components, and session management for enhanced team visibility and personal well-being tracking
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m16s

This commit is contained in:
Julien Froidefond
2026-02-03 18:08:06 +01:00
parent 3a2eb83197
commit 163caa398c
20 changed files with 2287 additions and 28 deletions

View File

@@ -16,8 +16,9 @@ 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';
import { deleteWeatherSession, updateWeatherSession } from '@/actions/weather';
type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'weekly-checkin' | 'byPerson';
type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'weekly-checkin' | 'weather' | 'byPerson';
const VALID_TABS: WorkshopType[] = [
'all',
@@ -25,6 +26,7 @@ const VALID_TABS: WorkshopType[] = [
'motivators',
'year-review',
'weekly-checkin',
'weather',
'byPerson',
];
@@ -107,13 +109,27 @@ interface WeeklyCheckInSession {
workshopType: 'weekly-checkin';
}
type AnySession = SwotSession | MotivatorSession | YearReviewSession | WeeklyCheckInSession;
interface WeatherSession {
id: string;
title: string;
date: Date;
updatedAt: Date;
isOwner: boolean;
role: 'OWNER' | 'VIEWER' | 'EDITOR';
user: { id: string; name: string | null; email: string };
shares: Share[];
_count: { entries: number };
workshopType: 'weather';
}
type AnySession = SwotSession | MotivatorSession | YearReviewSession | WeeklyCheckInSession | WeatherSession;
interface WorkshopTabsProps {
swotSessions: SwotSession[];
motivatorSessions: MotivatorSession[];
yearReviewSessions: YearReviewSession[];
weeklyCheckInSessions: WeeklyCheckInSession[];
weatherSessions: WeatherSession[];
}
// Helper to get resolved collaborator from any session
@@ -124,6 +140,17 @@ function getResolvedCollaborator(session: AnySession): ResolvedCollaborator {
return (session as YearReviewSession).resolvedParticipant;
} else if (session.workshopType === 'weekly-checkin') {
return (session as WeeklyCheckInSession).resolvedParticipant;
} else if (session.workshopType === 'weather') {
// For weather sessions, use the owner as the "participant" since it's a personal weather
const weatherSession = session as WeatherSession;
return {
raw: weatherSession.user.name || weatherSession.user.email,
matchedUser: {
id: weatherSession.user.id,
email: weatherSession.user.email,
name: weatherSession.user.name,
},
};
} else {
return (session as MotivatorSession).resolvedParticipant;
}
@@ -168,6 +195,7 @@ export function WorkshopTabs({
motivatorSessions,
yearReviewSessions,
weeklyCheckInSessions,
weatherSessions,
}: WorkshopTabsProps) {
const searchParams = useSearchParams();
const router = useRouter();
@@ -193,6 +221,7 @@ export function WorkshopTabs({
...motivatorSessions,
...yearReviewSessions,
...weeklyCheckInSessions,
...weatherSessions,
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
// Filter based on active tab (for non-byPerson tabs)
@@ -205,7 +234,9 @@ export function WorkshopTabs({
? motivatorSessions
: activeTab === 'year-review'
? yearReviewSessions
: weeklyCheckInSessions;
: activeTab === 'weekly-checkin'
? weeklyCheckInSessions
: weatherSessions;
// Separate by ownership
const ownedSessions = filteredSessions.filter((s) => s.isOwner);
@@ -263,6 +294,13 @@ export function WorkshopTabs({
label="Weekly Check-in"
count={weeklyCheckInSessions.length}
/>
<TabButton
active={activeTab === 'weather'}
onClick={() => setActiveTab('weather')}
icon="🌤️"
label="Météo"
count={weatherSessions.length}
/>
</div>
{/* Sessions */}
@@ -375,34 +413,43 @@ function SessionCard({ session }: { session: AnySession }) {
? (session as SwotSession).collaborator
: session.workshopType === 'year-review'
? (session as YearReviewSession).participant
: (session as MotivatorSession).participant
: session.workshopType === 'weather'
? ''
: (session as MotivatorSession).participant
);
const isSwot = session.workshopType === 'swot';
const isYearReview = session.workshopType === 'year-review';
const isWeeklyCheckIn = session.workshopType === 'weekly-checkin';
const isWeather = session.workshopType === 'weather';
const href = isSwot
? `/sessions/${session.id}`
: isYearReview
? `/year-review/${session.id}`
: isWeeklyCheckIn
? `/weekly-checkin/${session.id}`
: `/motivators/${session.id}`;
const icon = isSwot ? '📊' : isYearReview ? '📅' : isWeeklyCheckIn ? '📝' : '🎯';
: isWeather
? `/weather/${session.id}`
: `/motivators/${session.id}`;
const icon = isSwot ? '📊' : isYearReview ? '📅' : isWeeklyCheckIn ? '📝' : isWeather ? '🌤️' : '🎯';
const participant = isSwot
? (session as SwotSession).collaborator
: isYearReview
? (session as YearReviewSession).participant
: isWeeklyCheckIn
? (session as WeeklyCheckInSession).participant
: (session as MotivatorSession).participant;
: isWeather
? (session as WeatherSession).user.name || (session as WeatherSession).user.email
: (session as MotivatorSession).participant;
const accentColor = isSwot
? '#06b6d4'
: isYearReview
? '#f59e0b'
: isWeeklyCheckIn
? '#10b981'
: '#8b5cf6';
: isWeather
? '#3b82f6'
: '#8b5cf6';
const handleDelete = () => {
startTransition(async () => {
@@ -412,7 +459,9 @@ function SessionCard({ session }: { session: AnySession }) {
? await deleteYearReviewSession(session.id)
: isWeeklyCheckIn
? await deleteWeeklyCheckInSession(session.id)
: await deleteMotivatorSession(session.id);
: isWeather
? await deleteWeatherSession(session.id)
: await deleteMotivatorSession(session.id);
if (result.success) {
setShowDeleteModal(false);
@@ -436,10 +485,12 @@ function SessionCard({ session }: { session: AnySession }) {
title: editTitle,
participant: editParticipant,
})
: await updateMotivatorSession(session.id, {
title: editTitle,
participant: editParticipant,
});
: isWeather
? await updateWeatherSession(session.id, { title: editTitle })
: await updateMotivatorSession(session.id, {
title: editTitle,
participant: editParticipant,
});
if (result.success) {
setShowEditModal(false);
@@ -456,7 +507,7 @@ function SessionCard({ session }: { session: AnySession }) {
setShowEditModal(true);
};
const editParticipantLabel = isSwot ? 'Collaborateur' : 'Participant';
const editParticipantLabel = isSwot ? 'Collaborateur' : isWeather ? '' : 'Participant';
return (
<>
@@ -524,6 +575,17 @@ function SessionCard({ session }: { session: AnySession }) {
})}
</span>
</>
) : isWeather ? (
<>
<span>{(session as WeatherSession)._count.entries} membres</span>
<span>·</span>
<span>
{new Date((session as WeatherSession).date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
})}
</span>
</>
) : (
<span>{(session as MotivatorSession)._count.cards}/10</span>
)}
@@ -640,13 +702,15 @@ function SessionCard({ session }: { session: AnySession }) {
>
{editParticipantLabel}
</label>
<Input
id="edit-participant"
value={editParticipant}
onChange={(e) => setEditParticipant(e.target.value)}
placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'}
required
/>
{!isWeather && (
<Input
id="edit-participant"
value={editParticipant}
onChange={(e) => setEditParticipant(e.target.value)}
placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'}
required
/>
)}
</div>
<ModalFooter>
<Button
@@ -659,7 +723,7 @@ function SessionCard({ session }: { session: AnySession }) {
</Button>
<Button
type="submit"
disabled={isPending || !editTitle.trim() || !editParticipant.trim()}
disabled={isPending || !editTitle.trim() || (!isWeather && !editParticipant.trim())}
>
{isPending ? 'Enregistrement...' : 'Enregistrer'}
</Button>