254 lines
9.1 KiB
TypeScript
254 lines
9.1 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="text-xs text-green-500 font-bold leading-none" title="Amélioration">↑</span>;
|
|
}
|
|
if (direction === 'down') {
|
|
return <span className="text-xs text-red-500 font-bold leading-none" title="Dégradation">↓</span>;
|
|
}
|
|
return <span className="text-xs text-muted font-bold leading-none" 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(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
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-24 px-2 py-3">
|
|
{canEditThis ? (
|
|
<div className="flex flex-col items-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 mx-auto"
|
|
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
|
/>
|
|
<EvolutionIndicator current={performanceEmoji} previous={previousEntry?.performanceEmoji ?? null} />
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-2xl text-center">{performanceEmoji || '-'}</div>
|
|
<EvolutionIndicator current={performanceEmoji} previous={previousEntry?.performanceEmoji ?? null} />
|
|
</div>
|
|
)}
|
|
</td>
|
|
|
|
{/* Moral */}
|
|
<td className="w-24 px-2 py-3">
|
|
{canEditThis ? (
|
|
<div className="flex flex-col items-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 mx-auto"
|
|
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
|
/>
|
|
<EvolutionIndicator current={moralEmoji} previous={previousEntry?.moralEmoji ?? null} />
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-2xl text-center">{moralEmoji || '-'}</div>
|
|
<EvolutionIndicator current={moralEmoji} previous={previousEntry?.moralEmoji ?? null} />
|
|
</div>
|
|
)}
|
|
</td>
|
|
|
|
{/* Flux */}
|
|
<td className="w-24 px-2 py-3">
|
|
{canEditThis ? (
|
|
<div className="flex flex-col items-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 mx-auto"
|
|
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
|
/>
|
|
<EvolutionIndicator current={fluxEmoji} previous={previousEntry?.fluxEmoji ?? null} />
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-2xl text-center">{fluxEmoji || '-'}</div>
|
|
<EvolutionIndicator current={fluxEmoji} previous={previousEntry?.fluxEmoji ?? null} />
|
|
</div>
|
|
)}
|
|
</td>
|
|
|
|
{/* Création de valeur */}
|
|
<td className="w-24 px-2 py-3">
|
|
{canEditThis ? (
|
|
<div className="flex flex-col items-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 mx-auto"
|
|
className="!w-16 min-w-16 text-center text-lg py-2.5"
|
|
/>
|
|
<EvolutionIndicator current={valueCreationEmoji} previous={previousEntry?.valueCreationEmoji ?? null} />
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-2xl text-center">{valueCreationEmoji || '-'}</div>
|
|
<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>
|
|
);
|
|
}
|