Compare commits
10 Commits
739b0bf87d
...
74b1b2e838
| Author | SHA1 | Date | |
|---|---|---|---|
| 74b1b2e838 | |||
| 73219c89fb | |||
| 30c2b6cc1e | |||
| 51bc187374 | |||
| 3e869bf8ad | |||
| 3b212d6dda | |||
| 9b8c9efbd6 | |||
| 11c770da9c | |||
| 220dcf87b9 | |||
| 6b8d3c42f7 |
@@ -2,9 +2,9 @@ import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
||||
import { getWeatherSessionById } from '@/services/weather';
|
||||
import { getWeatherSessionById, getPreviousWeatherEntriesForUsers } from '@/services/weather';
|
||||
import { getUserTeams } from '@/services/teams';
|
||||
import { WeatherBoard, WeatherLiveWrapper, WeatherInfoPanel } from '@/components/weather';
|
||||
import { WeatherBoard, WeatherLiveWrapper, WeatherInfoPanel, WeatherAverageBar } from '@/components/weather';
|
||||
import { Badge } from '@/components/ui';
|
||||
import { EditableWeatherTitle } from '@/components/ui/EditableWeatherTitle';
|
||||
|
||||
@@ -20,15 +20,22 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
||||
return null;
|
||||
}
|
||||
|
||||
const [session, userTeams] = await Promise.all([
|
||||
getWeatherSessionById(id, authSession.user.id),
|
||||
getUserTeams(authSession.user.id),
|
||||
]);
|
||||
const session = await getWeatherSessionById(id, authSession.user.id);
|
||||
|
||||
if (!session) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const allUserIds = [
|
||||
session.user.id,
|
||||
...session.shares.map((s: { userId: string }) => s.userId),
|
||||
];
|
||||
|
||||
const [previousEntries, userTeams] = await Promise.all([
|
||||
getPreviousWeatherEntriesForUsers(session.id, session.date, allUserIds),
|
||||
getUserTeams(authSession.user.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-7xl px-4 py-8">
|
||||
{/* Header */}
|
||||
@@ -80,14 +87,10 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
||||
canEdit={session.canEdit}
|
||||
userTeams={userTeams}
|
||||
>
|
||||
<WeatherAverageBar entries={session.entries} />
|
||||
<WeatherBoard
|
||||
sessionId={session.id}
|
||||
currentUserId={authSession.user.id}
|
||||
currentUser={{
|
||||
id: authSession.user.id,
|
||||
name: authSession.user.name ?? null,
|
||||
email: authSession.user.email ?? '',
|
||||
}}
|
||||
entries={session.entries}
|
||||
shares={session.shares}
|
||||
owner={{
|
||||
@@ -96,6 +99,7 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
||||
email: session.user.email ?? '',
|
||||
}}
|
||||
canEdit={session.canEdit}
|
||||
previousEntries={Object.fromEntries(previousEntries)}
|
||||
/>
|
||||
</WeatherLiveWrapper>
|
||||
</main>
|
||||
|
||||
44
src/components/weather/WeatherAverageBar.tsx
Normal file
44
src/components/weather/WeatherAverageBar.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
// src/components/weather/WeatherAverageBar.tsx
|
||||
import { getAverageEmoji } from '@/lib/weather-utils';
|
||||
|
||||
interface WeatherEntry {
|
||||
performanceEmoji: string | null;
|
||||
moralEmoji: string | null;
|
||||
fluxEmoji: string | null;
|
||||
valueCreationEmoji: string | null;
|
||||
}
|
||||
|
||||
interface WeatherAverageBarProps {
|
||||
entries: WeatherEntry[];
|
||||
}
|
||||
|
||||
const AXES = [
|
||||
{ key: 'performanceEmoji' as const, label: 'Performance' },
|
||||
{ key: 'moralEmoji' as const, label: 'Moral' },
|
||||
{ key: 'fluxEmoji' as const, label: 'Flux' },
|
||||
{ key: 'valueCreationEmoji' as const, label: 'Création de valeur' },
|
||||
];
|
||||
|
||||
export function WeatherAverageBar({ entries }: WeatherAverageBarProps) {
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<span className="text-xs font-medium text-muted uppercase tracking-wide">
|
||||
Moyenne équipe
|
||||
</span>
|
||||
{AXES.map(({ key, label }) => {
|
||||
const avg = getAverageEmoji(entries.map((e) => e[key]));
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1"
|
||||
>
|
||||
<span className="text-lg leading-none">{avg ?? '—'}</span>
|
||||
<span className="text-xs text-muted">{label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,14 +28,16 @@ interface Share {
|
||||
};
|
||||
}
|
||||
|
||||
type PreviousEntry = {
|
||||
performanceEmoji: string | null;
|
||||
moralEmoji: string | null;
|
||||
fluxEmoji: string | null;
|
||||
valueCreationEmoji: string | null;
|
||||
};
|
||||
|
||||
interface WeatherBoardProps {
|
||||
sessionId: string;
|
||||
currentUserId: string;
|
||||
currentUser: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
entries: WeatherEntry[];
|
||||
shares: Share[];
|
||||
owner: {
|
||||
@@ -44,6 +46,7 @@ interface WeatherBoardProps {
|
||||
email: string;
|
||||
};
|
||||
canEdit: boolean;
|
||||
previousEntries: Record<string, PreviousEntry>;
|
||||
}
|
||||
|
||||
export function WeatherBoard({
|
||||
@@ -53,6 +56,7 @@ export function WeatherBoard({
|
||||
shares,
|
||||
owner,
|
||||
canEdit,
|
||||
previousEntries,
|
||||
}: WeatherBoardProps) {
|
||||
// Get all users who have access: owner + shared users
|
||||
const allUsers = useMemo(() => {
|
||||
@@ -137,6 +141,7 @@ export function WeatherBoard({
|
||||
currentUserId={currentUserId}
|
||||
entry={entry}
|
||||
canEdit={canEdit}
|
||||
previousEntry={previousEntries[entry.userId] ?? null}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -5,29 +5,7 @@ import { createOrUpdateWeatherEntry } from '@/actions/weather';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
|
||||
const WEATHER_EMOJIS = [
|
||||
{ emoji: '', label: 'Aucun' },
|
||||
{ emoji: '☀️', label: 'Soleil' },
|
||||
{ emoji: '🌤️', label: 'Soleil derrière nuage' },
|
||||
{ emoji: '⛅', label: 'Soleil et nuages' },
|
||||
{ emoji: '☁️', label: 'Nuages' },
|
||||
{ emoji: '🌦️', label: 'Soleil et pluie' },
|
||||
{ emoji: '🌧️', label: 'Pluie' },
|
||||
{ emoji: '⛈️', label: 'Orage et pluie' },
|
||||
{ emoji: '🌩️', label: 'Éclair' },
|
||||
{ emoji: '❄️', label: 'Neige' },
|
||||
{ emoji: '🌨️', label: 'Neige qui tombe' },
|
||||
{ emoji: '🌪️', label: 'Tornade' },
|
||||
{ emoji: '🌫️', label: 'Brouillard' },
|
||||
{ emoji: '🌈', label: 'Arc-en-ciel' },
|
||||
{ emoji: '🌊', label: 'Vague' },
|
||||
{ emoji: '🔥', label: 'Feu' },
|
||||
{ emoji: '💨', label: 'Vent' },
|
||||
{ emoji: '⭐', label: 'Étoile' },
|
||||
{ emoji: '🌟', label: 'Étoile brillante' },
|
||||
{ emoji: '✨', label: 'Étincelles' },
|
||||
];
|
||||
import { WEATHER_EMOJIS, getEmojiEvolution } from '@/lib/weather-utils';
|
||||
|
||||
interface WeatherEntry {
|
||||
id: string;
|
||||
@@ -44,14 +22,74 @@ interface WeatherEntry {
|
||||
};
|
||||
}
|
||||
|
||||
type PreviousEntry = {
|
||||
performanceEmoji: string | null;
|
||||
moralEmoji: string | null;
|
||||
fluxEmoji: string | null;
|
||||
valueCreationEmoji: string | null;
|
||||
};
|
||||
|
||||
interface WeatherCardProps {
|
||||
sessionId: string;
|
||||
currentUserId: string;
|
||||
entry: WeatherEntry;
|
||||
canEdit: boolean;
|
||||
previousEntry?: PreviousEntry | null;
|
||||
}
|
||||
|
||||
export function WeatherCard({ sessionId, currentUserId, entry, canEdit }: WeatherCardProps) {
|
||||
function EvolutionIndicator({
|
||||
current,
|
||||
previous,
|
||||
}: {
|
||||
current: string | null;
|
||||
previous: string | null | undefined;
|
||||
}) {
|
||||
const direction = getEmojiEvolution(current, previous);
|
||||
if (direction === null) return null;
|
||||
|
||||
if (direction === 'up') {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold shrink-0"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--success) 18%, transparent)',
|
||||
color: 'var(--success)',
|
||||
}}
|
||||
title="Amélioration"
|
||||
>
|
||||
↑
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (direction === 'down') {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold shrink-0"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--destructive) 18%, transparent)',
|
||||
color: 'var(--destructive)',
|
||||
}}
|
||||
title="Dégradation"
|
||||
>
|
||||
↓
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold shrink-0"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--muted) 15%, transparent)',
|
||||
color: 'var(--muted)',
|
||||
}}
|
||||
title="Stable"
|
||||
>
|
||||
→
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function WeatherCard({ sessionId, currentUserId, entry, canEdit, previousEntry }: WeatherCardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [notes, setNotes] = useState(entry.notes || '');
|
||||
const [performanceEmoji, setPerformanceEmoji] = useState(entry.performanceEmoji || null);
|
||||
@@ -64,7 +102,6 @@ export function WeatherCard({ sessionId, currentUserId, entry, canEdit }: Weathe
|
||||
|
||||
// Sync local state with props when they change (e.g., from SSE refresh)
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setNotes(entry.notes || '');
|
||||
setPerformanceEmoji(entry.performanceEmoji || null);
|
||||
setMoralEmoji(entry.moralEmoji || null);
|
||||
@@ -139,66 +176,90 @@ export function WeatherCard({ sessionId, currentUserId, entry, canEdit }: Weathe
|
||||
</td>
|
||||
|
||||
{/* Performance */}
|
||||
<td className="w-24 px-2 py-3">
|
||||
<td className="w-28 px-2 py-3">
|
||||
{canEditThis ? (
|
||||
<Select
|
||||
value={performanceEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('performance', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit mx-auto"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Select
|
||||
value={performanceEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('performance', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<EvolutionIndicator current={performanceEmoji} previous={previousEntry?.performanceEmoji ?? null} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{performanceEmoji || '-'}</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-2xl">{performanceEmoji || '-'}</span>
|
||||
<EvolutionIndicator current={performanceEmoji} previous={previousEntry?.performanceEmoji ?? null} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Moral */}
|
||||
<td className="w-24 px-2 py-3">
|
||||
<td className="w-28 px-2 py-3">
|
||||
{canEditThis ? (
|
||||
<Select
|
||||
value={moralEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('moral', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit mx-auto"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Select
|
||||
value={moralEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('moral', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<EvolutionIndicator current={moralEmoji} previous={previousEntry?.moralEmoji ?? null} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{moralEmoji || '-'}</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-2xl">{moralEmoji || '-'}</span>
|
||||
<EvolutionIndicator current={moralEmoji} previous={previousEntry?.moralEmoji ?? null} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Flux */}
|
||||
<td className="w-24 px-2 py-3">
|
||||
<td className="w-28 px-2 py-3">
|
||||
{canEditThis ? (
|
||||
<Select
|
||||
value={fluxEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('flux', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit mx-auto"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Select
|
||||
value={fluxEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('flux', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<EvolutionIndicator current={fluxEmoji} previous={previousEntry?.fluxEmoji ?? null} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{fluxEmoji || '-'}</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-2xl">{fluxEmoji || '-'}</span>
|
||||
<EvolutionIndicator current={fluxEmoji} previous={previousEntry?.fluxEmoji ?? null} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Création de valeur */}
|
||||
<td className="w-24 px-2 py-3">
|
||||
<td className="w-28 px-2 py-3">
|
||||
{canEditThis ? (
|
||||
<Select
|
||||
value={valueCreationEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('valueCreation', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit mx-auto"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Select
|
||||
value={valueCreationEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('valueCreation', e.target.value || null)}
|
||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||
size="sm"
|
||||
wrapperClassName="!w-fit"
|
||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
||||
/>
|
||||
<EvolutionIndicator current={valueCreationEmoji} previous={previousEntry?.valueCreationEmoji ?? null} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{valueCreationEmoji || '-'}</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-2xl">{valueCreationEmoji || '-'}</span>
|
||||
<EvolutionIndicator current={valueCreationEmoji} previous={previousEntry?.valueCreationEmoji ?? null} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ export { WeatherBoard } from './WeatherBoard';
|
||||
export { WeatherCard } from './WeatherCard';
|
||||
export { WeatherLiveWrapper } from './WeatherLiveWrapper';
|
||||
export { WeatherInfoPanel } from './WeatherInfoPanel';
|
||||
export { WeatherAverageBar } from './WeatherAverageBar';
|
||||
|
||||
63
src/lib/weather-utils.ts
Normal file
63
src/lib/weather-utils.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// src/lib/weather-utils.ts
|
||||
|
||||
export const WEATHER_EMOJIS = [
|
||||
{ emoji: '', label: 'Aucun' },
|
||||
{ emoji: '☀️', label: 'Soleil' },
|
||||
{ emoji: '🌤️', label: 'Soleil derrière nuage' },
|
||||
{ emoji: '⛅', label: 'Soleil et nuages' },
|
||||
{ emoji: '☁️', label: 'Nuages' },
|
||||
{ emoji: '🌦️', label: 'Soleil et pluie' },
|
||||
{ emoji: '🌧️', label: 'Pluie' },
|
||||
{ emoji: '⛈️', label: 'Orage et pluie' },
|
||||
{ emoji: '🌩️', label: 'Éclair' },
|
||||
{ emoji: '❄️', label: 'Neige' },
|
||||
{ emoji: '🌨️', label: 'Neige qui tombe' },
|
||||
{ emoji: '🌪️', label: 'Tornade' },
|
||||
{ emoji: '🌫️', label: 'Brouillard' },
|
||||
{ emoji: '🌈', label: 'Arc-en-ciel' },
|
||||
{ emoji: '🌊', label: 'Vague' },
|
||||
{ emoji: '🔥', label: 'Feu' },
|
||||
{ emoji: '💨', label: 'Vent' },
|
||||
{ emoji: '⭐', label: 'Étoile' },
|
||||
{ emoji: '🌟', label: 'Étoile brillante' },
|
||||
{ emoji: '✨', label: 'Étincelles' },
|
||||
];
|
||||
|
||||
// Score = 1-based index in the list. Empty string = no value (excluded from scoring).
|
||||
export function getEmojiScore(emoji: string | null | undefined): number | null {
|
||||
if (!emoji) return null;
|
||||
const idx = WEATHER_EMOJIS.findIndex((e) => e.emoji === emoji);
|
||||
if (idx <= 0) return null; // idx 0 = '' (no value), or not found
|
||||
return idx; // 1 = ☀️ (best), 19 = ✨ (worst)
|
||||
}
|
||||
|
||||
// Returns the emoji whose score is closest to the average of all scored emojis.
|
||||
// Returns null if no scored emojis.
|
||||
export function getAverageEmoji(emojis: (string | null | undefined)[]): string | null {
|
||||
const scores = emojis.map(getEmojiScore).filter((s): s is number => s !== null);
|
||||
if (scores.length === 0) return null;
|
||||
const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
|
||||
// Find emoji with index closest to avg (skip index 0 = empty)
|
||||
let closest: { emoji: string; dist: number } | null = null;
|
||||
for (let i = 1; i < WEATHER_EMOJIS.length; i++) {
|
||||
const dist = Math.abs(i - avg);
|
||||
if (closest === null || dist < closest.dist) {
|
||||
closest = { emoji: WEATHER_EMOJIS[i].emoji, dist };
|
||||
}
|
||||
}
|
||||
return closest?.emoji ?? null;
|
||||
}
|
||||
|
||||
// Returns evolution direction: 'up' (amélioration), 'down' (dégradation), 'same', or null (no previous).
|
||||
export function getEmojiEvolution(
|
||||
current: string | null | undefined,
|
||||
previous: string | null | undefined
|
||||
): 'up' | 'down' | 'same' | null {
|
||||
const currentScore = getEmojiScore(current);
|
||||
const previousScore = getEmojiScore(previous);
|
||||
if (currentScore === null || previousScore === null) return null;
|
||||
const delta = currentScore - previousScore;
|
||||
if (delta < 0) return 'up'; // lower score = better weather
|
||||
if (delta > 0) return 'down'; // higher score = worse weather
|
||||
return 'same';
|
||||
}
|
||||
@@ -177,6 +177,78 @@ export async function deleteWeatherEntry(sessionId: string, userId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Returns the most recent WeatherEntry per userId from any session BEFORE sessionDate,
|
||||
// excluding the current session. Returned as a map userId → entry.
|
||||
export async function getPreviousWeatherEntriesForUsers(
|
||||
excludeSessionId: string,
|
||||
sessionDate: Date,
|
||||
userIds: string[]
|
||||
): Promise<
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
performanceEmoji: string | null;
|
||||
moralEmoji: string | null;
|
||||
fluxEmoji: string | null;
|
||||
valueCreationEmoji: string | null;
|
||||
}
|
||||
>
|
||||
> {
|
||||
if (userIds.length === 0) return new Map();
|
||||
|
||||
const entries = await prisma.weatherEntry.findMany({
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
sessionId: { not: excludeSessionId },
|
||||
session: { date: { lt: sessionDate } },
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
performanceEmoji: true,
|
||||
moralEmoji: true,
|
||||
fluxEmoji: true,
|
||||
valueCreationEmoji: true,
|
||||
session: { select: { date: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// Sort by session.date desc (Prisma orderBy on relation is unreliable with SQLite)
|
||||
entries.sort((a, b) => {
|
||||
const dateA = a.session.date.getTime();
|
||||
const dateB = b.session.date.getTime();
|
||||
if (dateB !== dateA) return dateB - dateA; // most recent first
|
||||
return a.userId.localeCompare(b.userId);
|
||||
});
|
||||
|
||||
// For each user, use the most recent previous value PER AXIS (fallback if latest session has null)
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
performanceEmoji: string | null;
|
||||
moralEmoji: string | null;
|
||||
fluxEmoji: string | null;
|
||||
valueCreationEmoji: string | null;
|
||||
}
|
||||
>();
|
||||
for (const entry of entries) {
|
||||
const existing = map.get(entry.userId);
|
||||
const base = existing ?? {
|
||||
performanceEmoji: null as string | null,
|
||||
moralEmoji: null as string | null,
|
||||
fluxEmoji: null as string | null,
|
||||
valueCreationEmoji: null as string | null,
|
||||
};
|
||||
if (!existing) map.set(entry.userId, base);
|
||||
if (base.performanceEmoji == null && entry.performanceEmoji != null) base.performanceEmoji = entry.performanceEmoji;
|
||||
if (base.moralEmoji == null && entry.moralEmoji != null) base.moralEmoji = entry.moralEmoji;
|
||||
if (base.fluxEmoji == null && entry.fluxEmoji != null) base.fluxEmoji = entry.fluxEmoji;
|
||||
if (base.valueCreationEmoji == null && entry.valueCreationEmoji != null)
|
||||
base.valueCreationEmoji = entry.valueCreationEmoji;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Session Sharing
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user