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 Link from 'next/link';
|
||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
|
||||||
import { getWeatherSessionById } from '@/services/weather';
|
import { getWeatherSessionById, getPreviousWeatherEntriesForUsers } from '@/services/weather';
|
||||||
import { getUserTeams } from '@/services/teams';
|
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 { Badge } from '@/components/ui';
|
||||||
import { EditableWeatherTitle } from '@/components/ui/EditableWeatherTitle';
|
import { EditableWeatherTitle } from '@/components/ui/EditableWeatherTitle';
|
||||||
|
|
||||||
@@ -20,15 +20,22 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [session, userTeams] = await Promise.all([
|
const session = await getWeatherSessionById(id, authSession.user.id);
|
||||||
getWeatherSessionById(id, authSession.user.id),
|
|
||||||
getUserTeams(authSession.user.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
notFound();
|
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 (
|
return (
|
||||||
<main className="mx-auto max-w-7xl px-4 py-8">
|
<main className="mx-auto max-w-7xl px-4 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -80,14 +87,10 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
>
|
>
|
||||||
|
<WeatherAverageBar entries={session.entries} />
|
||||||
<WeatherBoard
|
<WeatherBoard
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
currentUserId={authSession.user.id}
|
currentUserId={authSession.user.id}
|
||||||
currentUser={{
|
|
||||||
id: authSession.user.id,
|
|
||||||
name: authSession.user.name ?? null,
|
|
||||||
email: authSession.user.email ?? '',
|
|
||||||
}}
|
|
||||||
entries={session.entries}
|
entries={session.entries}
|
||||||
shares={session.shares}
|
shares={session.shares}
|
||||||
owner={{
|
owner={{
|
||||||
@@ -96,6 +99,7 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
email: session.user.email ?? '',
|
email: session.user.email ?? '',
|
||||||
}}
|
}}
|
||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
|
previousEntries={Object.fromEntries(previousEntries)}
|
||||||
/>
|
/>
|
||||||
</WeatherLiveWrapper>
|
</WeatherLiveWrapper>
|
||||||
</main>
|
</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 {
|
interface WeatherBoardProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
currentUser: {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
entries: WeatherEntry[];
|
entries: WeatherEntry[];
|
||||||
shares: Share[];
|
shares: Share[];
|
||||||
owner: {
|
owner: {
|
||||||
@@ -44,6 +46,7 @@ interface WeatherBoardProps {
|
|||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
previousEntries: Record<string, PreviousEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WeatherBoard({
|
export function WeatherBoard({
|
||||||
@@ -53,6 +56,7 @@ export function WeatherBoard({
|
|||||||
shares,
|
shares,
|
||||||
owner,
|
owner,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
previousEntries,
|
||||||
}: WeatherBoardProps) {
|
}: WeatherBoardProps) {
|
||||||
// Get all users who have access: owner + shared users
|
// Get all users who have access: owner + shared users
|
||||||
const allUsers = useMemo(() => {
|
const allUsers = useMemo(() => {
|
||||||
@@ -137,6 +141,7 @@ export function WeatherBoard({
|
|||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
|
previousEntry={previousEntries[entry.userId] ?? null}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -5,29 +5,7 @@ import { createOrUpdateWeatherEntry } from '@/actions/weather';
|
|||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import { Textarea } from '@/components/ui/Textarea';
|
import { Textarea } from '@/components/ui/Textarea';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
|
import { WEATHER_EMOJIS, getEmojiEvolution } from '@/lib/weather-utils';
|
||||||
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' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface WeatherEntry {
|
interface WeatherEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -44,14 +22,74 @@ interface WeatherEntry {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreviousEntry = {
|
||||||
|
performanceEmoji: string | null;
|
||||||
|
moralEmoji: string | null;
|
||||||
|
fluxEmoji: string | null;
|
||||||
|
valueCreationEmoji: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
interface WeatherCardProps {
|
interface WeatherCardProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
entry: WeatherEntry;
|
entry: WeatherEntry;
|
||||||
canEdit: boolean;
|
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 [isPending, startTransition] = useTransition();
|
||||||
const [notes, setNotes] = useState(entry.notes || '');
|
const [notes, setNotes] = useState(entry.notes || '');
|
||||||
const [performanceEmoji, setPerformanceEmoji] = useState(entry.performanceEmoji || null);
|
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)
|
// Sync local state with props when they change (e.g., from SSE refresh)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setNotes(entry.notes || '');
|
setNotes(entry.notes || '');
|
||||||
setPerformanceEmoji(entry.performanceEmoji || null);
|
setPerformanceEmoji(entry.performanceEmoji || null);
|
||||||
setMoralEmoji(entry.moralEmoji || null);
|
setMoralEmoji(entry.moralEmoji || null);
|
||||||
@@ -139,66 +176,90 @@ export function WeatherCard({ sessionId, currentUserId, entry, canEdit }: Weathe
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Performance */}
|
{/* Performance */}
|
||||||
<td className="w-24 px-2 py-3">
|
<td className="w-28 px-2 py-3">
|
||||||
{canEditThis ? (
|
{canEditThis ? (
|
||||||
<Select
|
<div className="flex items-center justify-center gap-1">
|
||||||
value={performanceEmoji || ''}
|
<Select
|
||||||
onChange={(e) => handleEmojiChange('performance', e.target.value || null)}
|
value={performanceEmoji || ''}
|
||||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
onChange={(e) => handleEmojiChange('performance', e.target.value || null)}
|
||||||
size="sm"
|
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||||
wrapperClassName="!w-fit mx-auto"
|
size="sm"
|
||||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
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>
|
</td>
|
||||||
|
|
||||||
{/* Moral */}
|
{/* Moral */}
|
||||||
<td className="w-24 px-2 py-3">
|
<td className="w-28 px-2 py-3">
|
||||||
{canEditThis ? (
|
{canEditThis ? (
|
||||||
<Select
|
<div className="flex items-center justify-center gap-1">
|
||||||
value={moralEmoji || ''}
|
<Select
|
||||||
onChange={(e) => handleEmojiChange('moral', e.target.value || null)}
|
value={moralEmoji || ''}
|
||||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
onChange={(e) => handleEmojiChange('moral', e.target.value || null)}
|
||||||
size="sm"
|
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||||
wrapperClassName="!w-fit mx-auto"
|
size="sm"
|
||||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
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>
|
</td>
|
||||||
|
|
||||||
{/* Flux */}
|
{/* Flux */}
|
||||||
<td className="w-24 px-2 py-3">
|
<td className="w-28 px-2 py-3">
|
||||||
{canEditThis ? (
|
{canEditThis ? (
|
||||||
<Select
|
<div className="flex items-center justify-center gap-1">
|
||||||
value={fluxEmoji || ''}
|
<Select
|
||||||
onChange={(e) => handleEmojiChange('flux', e.target.value || null)}
|
value={fluxEmoji || ''}
|
||||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
onChange={(e) => handleEmojiChange('flux', e.target.value || null)}
|
||||||
size="sm"
|
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||||
wrapperClassName="!w-fit mx-auto"
|
size="sm"
|
||||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
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>
|
</td>
|
||||||
|
|
||||||
{/* Création de valeur */}
|
{/* Création de valeur */}
|
||||||
<td className="w-24 px-2 py-3">
|
<td className="w-28 px-2 py-3">
|
||||||
{canEditThis ? (
|
{canEditThis ? (
|
||||||
<Select
|
<div className="flex items-center justify-center gap-1">
|
||||||
value={valueCreationEmoji || ''}
|
<Select
|
||||||
onChange={(e) => handleEmojiChange('valueCreation', e.target.value || null)}
|
value={valueCreationEmoji || ''}
|
||||||
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
onChange={(e) => handleEmojiChange('valueCreation', e.target.value || null)}
|
||||||
size="sm"
|
options={WEATHER_EMOJIS.map(({ emoji }) => ({ value: emoji, label: emoji }))}
|
||||||
wrapperClassName="!w-fit mx-auto"
|
size="sm"
|
||||||
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
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>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export { WeatherBoard } from './WeatherBoard';
|
|||||||
export { WeatherCard } from './WeatherCard';
|
export { WeatherCard } from './WeatherCard';
|
||||||
export { WeatherLiveWrapper } from './WeatherLiveWrapper';
|
export { WeatherLiveWrapper } from './WeatherLiveWrapper';
|
||||||
export { WeatherInfoPanel } from './WeatherInfoPanel';
|
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
|
// Session Sharing
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user