308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useTransition } from 'react';
|
|
import Link from 'next/link';
|
|
import { Modal } from '@/components/ui/Modal';
|
|
import { Input } from '@/components/ui/Input';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Badge } from '@/components/ui/Badge';
|
|
import { Avatar } from '@/components/ui/Avatar';
|
|
import { shareWeatherSession, shareWeatherSessionToTeam, removeWeatherShare } from '@/actions/weather';
|
|
import type { ShareRole } from '@prisma/client';
|
|
|
|
interface ShareUser {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
}
|
|
|
|
interface Share {
|
|
id: string;
|
|
role: ShareRole;
|
|
user: ShareUser;
|
|
createdAt: Date;
|
|
}
|
|
|
|
interface Team {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
userRole: 'ADMIN' | 'MEMBER';
|
|
}
|
|
|
|
interface WeatherShareModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
sessionId: string;
|
|
sessionTitle: string;
|
|
shares: Share[];
|
|
isOwner: boolean;
|
|
userTeams?: Team[];
|
|
}
|
|
|
|
export function WeatherShareModal({
|
|
isOpen,
|
|
onClose,
|
|
sessionId,
|
|
sessionTitle,
|
|
shares,
|
|
isOwner,
|
|
userTeams = [],
|
|
}: WeatherShareModalProps) {
|
|
const [shareType, setShareType] = useState<'user' | 'team'>('user');
|
|
const [email, setEmail] = useState('');
|
|
const [teamId, setTeamId] = useState('');
|
|
const [role, setRole] = useState<ShareRole>('EDITOR');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
async function handleShare(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
|
|
startTransition(async () => {
|
|
let result;
|
|
if (shareType === 'team') {
|
|
result = await shareWeatherSessionToTeam(sessionId, teamId, role);
|
|
} else {
|
|
result = await shareWeatherSession(sessionId, email, role);
|
|
}
|
|
|
|
if (result.success) {
|
|
setEmail('');
|
|
setTeamId('');
|
|
} else {
|
|
setError(result.error || 'Erreur lors du partage');
|
|
}
|
|
});
|
|
}
|
|
|
|
async function handleRemove(userId: string) {
|
|
startTransition(async () => {
|
|
await removeWeatherShare(sessionId, userId);
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} onClose={onClose} title="Partager la météo">
|
|
<div className="space-y-6">
|
|
{/* Session info */}
|
|
<div>
|
|
<p className="text-sm text-muted">Météo personnelle</p>
|
|
<p className="font-medium text-foreground">{sessionTitle}</p>
|
|
</div>
|
|
|
|
{/* Share form (only for owner) */}
|
|
{isOwner && (
|
|
<form onSubmit={handleShare} className="space-y-4">
|
|
{/* Share type selector */}
|
|
<div className="flex gap-2 border-b border-border pb-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShareType('user');
|
|
setEmail('');
|
|
setTeamId('');
|
|
}}
|
|
className={`flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
|
shareType === 'user'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-card-hover text-muted hover:text-foreground'
|
|
}`}
|
|
>
|
|
👤 Utilisateur
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShareType('team');
|
|
setEmail('');
|
|
setTeamId('');
|
|
}}
|
|
className={`flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
|
shareType === 'team'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-card-hover text-muted hover:text-foreground'
|
|
}`}
|
|
>
|
|
👥 Équipe
|
|
</button>
|
|
</div>
|
|
|
|
{/* User share */}
|
|
{shareType === 'user' && (
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="email"
|
|
placeholder="Email de l'utilisateur"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="flex-1"
|
|
required
|
|
/>
|
|
<div className="relative">
|
|
<select
|
|
value={role}
|
|
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
className="appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-sm text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
>
|
|
<option value="EDITOR">Éditeur</option>
|
|
<option value="VIEWER">Lecteur</option>
|
|
</select>
|
|
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
|
<svg
|
|
className="h-4 w-4 text-muted"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Team share */}
|
|
{shareType === 'team' && (
|
|
<div className="space-y-2">
|
|
{userTeams.length === 0 ? (
|
|
<p className="text-sm text-muted">
|
|
Vous n'êtes membre d'aucune équipe. Créez une équipe depuis la page{' '}
|
|
<Link href="/teams" className="text-primary hover:underline">
|
|
Équipes
|
|
</Link>
|
|
.
|
|
</p>
|
|
) : (
|
|
<>
|
|
<div className="relative">
|
|
<select
|
|
value={teamId}
|
|
onChange={(e) => setTeamId(e.target.value)}
|
|
className="w-full appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-sm text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
required
|
|
>
|
|
<option value="">Sélectionner une équipe</option>
|
|
{userTeams.map((team) => (
|
|
<option key={team.id} value={team.id}>
|
|
{team.name} {team.userRole === 'ADMIN' && '(Admin)'}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
|
<svg
|
|
className="h-4 w-4 text-muted"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="relative">
|
|
<select
|
|
value={role}
|
|
onChange={(e) => setRole(e.target.value as ShareRole)}
|
|
className="w-full appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-sm text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
>
|
|
<option value="EDITOR">Éditeur</option>
|
|
<option value="VIEWER">Lecteur</option>
|
|
</select>
|
|
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
|
<svg
|
|
className="h-4 w-4 text-muted"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={isPending || (shareType === 'user' && !email) || (shareType === 'team' && !teamId)}
|
|
className="w-full"
|
|
>
|
|
{isPending ? 'Partage...' : shareType === 'team' ? "Partager à l'équipe" : 'Partager'}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
|
|
{/* Current shares */}
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
|
|
|
|
{shares.length === 0 ? (
|
|
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{shares.map((share) => (
|
|
<li
|
|
key={share.id}
|
|
className="flex items-center justify-between rounded-lg border border-border bg-card p-3"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Avatar email={share.user.email} name={share.user.name} size={32} />
|
|
<div>
|
|
<p className="text-sm font-medium text-foreground">
|
|
{share.user.name || share.user.email}
|
|
</p>
|
|
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
|
|
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
|
</Badge>
|
|
{isOwner && (
|
|
<button
|
|
onClick={() => handleRemove(share.user.id)}
|
|
disabled={isPending}
|
|
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
|
title="Retirer l'accès"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
className="h-4 w-4"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
{/* Help text */}
|
|
<div className="rounded-lg bg-primary/5 p-3">
|
|
<p className="text-xs text-muted">
|
|
<strong>Éditeur</strong> : peut modifier sa météo et voir celle des autres
|
|
<br />
|
|
<strong>Lecteur</strong> : peut uniquement consulter
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|