All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m12s
Reintroduced the WeatherAverageBar component in the WeatherSessionPage to display team averages. Updated the styling of the WeatherAverageBar for improved spacing. Enhanced the EvolutionIndicator component to use dynamic background colors for better visibility of status indicators.
286 lines
9.7 KiB
TypeScript
286 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useTransition, useEffect } from 'react';
|
|
import { createOrUpdateWeatherEntry } from '@/actions/weather';
|
|
import { Avatar } from '@/components/ui/Avatar';
|
|
import { Textarea } from '@/components/ui/Textarea';
|
|
import { Select } from '@/components/ui/Select';
|
|
import { WEATHER_EMOJIS, getEmojiEvolution } from '@/lib/weather-utils';
|
|
|
|
interface WeatherEntry {
|
|
id: string;
|
|
userId: string;
|
|
performanceEmoji: string | null;
|
|
moralEmoji: string | null;
|
|
fluxEmoji: string | null;
|
|
valueCreationEmoji: string | null;
|
|
notes: string | null;
|
|
user: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
const [moralEmoji, setMoralEmoji] = useState(entry.moralEmoji || null);
|
|
const [fluxEmoji, setFluxEmoji] = useState(entry.fluxEmoji || null);
|
|
const [valueCreationEmoji, setValueCreationEmoji] = useState(entry.valueCreationEmoji || null);
|
|
|
|
const isCurrentUser = entry.userId === currentUserId;
|
|
const canEditThis = canEdit && isCurrentUser;
|
|
|
|
// Sync local state with props when they change (e.g., from SSE refresh)
|
|
useEffect(() => {
|
|
setNotes(entry.notes || '');
|
|
setPerformanceEmoji(entry.performanceEmoji || null);
|
|
setMoralEmoji(entry.moralEmoji || null);
|
|
setFluxEmoji(entry.fluxEmoji || null);
|
|
setValueCreationEmoji(entry.valueCreationEmoji || null);
|
|
}, [entry.notes, entry.performanceEmoji, entry.moralEmoji, entry.fluxEmoji, entry.valueCreationEmoji]);
|
|
|
|
function handleEmojiChange(axis: 'performance' | 'moral' | 'flux' | 'valueCreation', emoji: string | null) {
|
|
if (!canEditThis) return;
|
|
|
|
// Calculate new values
|
|
const newPerformanceEmoji = axis === 'performance' ? emoji : performanceEmoji;
|
|
const newMoralEmoji = axis === 'moral' ? emoji : moralEmoji;
|
|
const newFluxEmoji = axis === 'flux' ? emoji : fluxEmoji;
|
|
const newValueCreationEmoji = axis === 'valueCreation' ? emoji : valueCreationEmoji;
|
|
|
|
// Update local state immediately
|
|
if (axis === 'performance') {
|
|
setPerformanceEmoji(emoji);
|
|
} else if (axis === 'moral') {
|
|
setMoralEmoji(emoji);
|
|
} else if (axis === 'flux') {
|
|
setFluxEmoji(emoji);
|
|
} else if (axis === 'valueCreation') {
|
|
setValueCreationEmoji(emoji);
|
|
}
|
|
|
|
// Save to server with new values
|
|
startTransition(async () => {
|
|
await createOrUpdateWeatherEntry(sessionId, {
|
|
performanceEmoji: newPerformanceEmoji,
|
|
moralEmoji: newMoralEmoji,
|
|
fluxEmoji: newFluxEmoji,
|
|
valueCreationEmoji: newValueCreationEmoji,
|
|
notes,
|
|
});
|
|
});
|
|
}
|
|
|
|
function handleNotesChange(newNotes: string) {
|
|
if (!canEditThis) return;
|
|
setNotes(newNotes);
|
|
}
|
|
|
|
function handleNotesBlur() {
|
|
if (!canEditThis) return;
|
|
startTransition(async () => {
|
|
await createOrUpdateWeatherEntry(sessionId, {
|
|
performanceEmoji,
|
|
moralEmoji,
|
|
fluxEmoji,
|
|
valueCreationEmoji,
|
|
notes,
|
|
});
|
|
});
|
|
}
|
|
|
|
// For current user without entry, we need to get user info from somewhere
|
|
// For now, we'll use a placeholder - in real app, you'd pass user info as prop
|
|
const user = entry.user;
|
|
|
|
return (
|
|
<tr className={`border-b border-border ${isPending ? 'opacity-50' : ''}`}>
|
|
{/* User column */}
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<Avatar email={user.email} name={user.name} size={32} />
|
|
<span className="text-sm font-medium text-foreground">
|
|
{user.name || user.email || 'Vous'}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
{/* Performance */}
|
|
<td className="w-28 px-2 py-3">
|
|
{canEditThis ? (
|
|
<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="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-28 px-2 py-3">
|
|
{canEditThis ? (
|
|
<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="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-28 px-2 py-3">
|
|
{canEditThis ? (
|
|
<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="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-28 px-2 py-3">
|
|
{canEditThis ? (
|
|
<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="flex items-center justify-center gap-1">
|
|
<span className="text-2xl">{valueCreationEmoji || '-'}</span>
|
|
<EvolutionIndicator current={valueCreationEmoji} previous={previousEntry?.valueCreationEmoji ?? null} />
|
|
</div>
|
|
)}
|
|
</td>
|
|
|
|
{/* Notes */}
|
|
<td className="px-4 py-3 min-w-[400px]">
|
|
{canEditThis ? (
|
|
<Textarea
|
|
value={notes}
|
|
onChange={(e) => handleNotesChange(e.target.value)}
|
|
onBlur={handleNotesBlur}
|
|
placeholder="Notes globales..."
|
|
className="min-h-[120px] w-full resize-y"
|
|
rows={5}
|
|
/>
|
|
) : (
|
|
<div className="text-sm text-foreground whitespace-pre-wrap min-h-[120px]">
|
|
{notes || '-'}
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|