Agenda Sync refactor
This commit is contained in:
parent
8fc1fa2fae
commit
3c1b20cee9
@ -26,7 +26,9 @@ import {
|
|||||||
MapPin,
|
MapPin,
|
||||||
Tag,
|
Tag,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp
|
ChevronUp,
|
||||||
|
RefreshCw,
|
||||||
|
Link as LinkIcon
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Calendar, Event } from "@prisma/client";
|
import { Calendar, Event } from "@prisma/client";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
@ -114,18 +116,40 @@ interface CalendarDialogProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (calendarData: Partial<Calendar>) => Promise<void>;
|
onSave: (calendarData: Partial<Calendar>) => Promise<void>;
|
||||||
onDelete?: (calendarId: string) => Promise<void>;
|
onDelete?: (calendarId: string) => Promise<void>;
|
||||||
|
onSyncSetup?: (calendarId: string, mailCredentialId: string, externalCalendarUrl: string) => Promise<void>;
|
||||||
initialData?: Partial<Calendar>;
|
initialData?: Partial<Calendar>;
|
||||||
|
syncConfig?: {
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
syncEnabled: boolean;
|
||||||
|
lastSyncAt: Date | null;
|
||||||
|
mailCredential?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
display_name: string | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarDialog({ open, onClose, onSave, onDelete, initialData }: CalendarDialogProps) {
|
function CalendarDialog({ open, onClose, onSave, onDelete, onSyncSetup, initialData, syncConfig }: CalendarDialogProps) {
|
||||||
const [name, setName] = useState(initialData?.name || "");
|
const [name, setName] = useState(initialData?.name || "");
|
||||||
const [color, setColor] = useState(initialData?.color || "#4f46e5");
|
const [color, setColor] = useState(initialData?.color || "#4f46e5");
|
||||||
const [description, setDescription] = useState(initialData?.description || "");
|
const [description, setDescription] = useState(initialData?.description || "");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [customColorMode, setCustomColorMode] = useState(false);
|
const [customColorMode, setCustomColorMode] = useState(false);
|
||||||
|
|
||||||
|
// Sync state
|
||||||
|
const [showSyncSection, setShowSyncSection] = useState(false);
|
||||||
|
const [availableAccounts, setAvailableAccounts] = useState<Array<{ id: string; email: string; display_name: string | null }>>([]);
|
||||||
|
const [selectedAccountId, setSelectedAccountId] = useState<string>("");
|
||||||
|
const [availableCalendars, setAvailableCalendars] = useState<Array<{ id: string; name: string; url: string }>>([]);
|
||||||
|
const [selectedCalendarUrl, setSelectedCalendarUrl] = useState<string>("");
|
||||||
|
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||||
|
const [isSettingUpSync, setIsSettingUpSync] = useState(false);
|
||||||
|
|
||||||
const isMainCalendar = initialData?.name === "Calendrier principal";
|
const isMainCalendar = initialData?.name === "Calendrier principal";
|
||||||
const isMissionOrGroupCalendar = initialData?.name?.startsWith("Mission:") || initialData?.name?.startsWith("Groupe:");
|
const isMissionOrGroupCalendar = initialData?.name?.startsWith("Mission:") || initialData?.name?.startsWith("Groupe:");
|
||||||
|
const isPrivateCalendar = !isMissionOrGroupCalendar && (initialData?.name === "Privée" || initialData?.name === "Default" || !initialData?.name);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@ -133,8 +157,81 @@ function CalendarDialog({ open, onClose, onSave, onDelete, initialData }: Calend
|
|||||||
setColor(initialData?.color || "#4f46e5");
|
setColor(initialData?.color || "#4f46e5");
|
||||||
setDescription(initialData?.description || "");
|
setDescription(initialData?.description || "");
|
||||||
setCustomColorMode(!colorPalette.includes(initialData?.color || "#4f46e5"));
|
setCustomColorMode(!colorPalette.includes(initialData?.color || "#4f46e5"));
|
||||||
|
setShowSyncSection(false);
|
||||||
|
setSelectedAccountId("");
|
||||||
|
setAvailableCalendars([]);
|
||||||
|
setSelectedCalendarUrl("");
|
||||||
|
|
||||||
|
// Load available accounts for sync
|
||||||
|
if (isPrivateCalendar && !syncConfig) {
|
||||||
|
loadAvailableAccounts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [open, initialData]);
|
}, [open, initialData, syncConfig, isPrivateCalendar]);
|
||||||
|
|
||||||
|
const loadAvailableAccounts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/courrier/account-list");
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.accounts) {
|
||||||
|
// Filter Infomaniak accounts only
|
||||||
|
const infomaniakAccounts = data.accounts.filter((acc: any) =>
|
||||||
|
acc.host && acc.host.includes('infomaniak')
|
||||||
|
);
|
||||||
|
setAvailableAccounts(infomaniakAccounts.map((acc: any) => ({
|
||||||
|
id: acc.id,
|
||||||
|
email: acc.email,
|
||||||
|
display_name: acc.display_name
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading accounts:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDiscoverCalendars = async () => {
|
||||||
|
if (!selectedAccountId) return;
|
||||||
|
|
||||||
|
setIsDiscovering(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/calendars/sync/discover", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ mailCredentialId: selectedAccountId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setAvailableCalendars(data.calendars || []);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(error.error || "Erreur lors de la découverte des calendriers");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error discovering calendars:", error);
|
||||||
|
alert("Erreur lors de la découverte des calendriers");
|
||||||
|
} finally {
|
||||||
|
setIsDiscovering(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetupSync = async () => {
|
||||||
|
if (!initialData?.id || !selectedAccountId || !selectedCalendarUrl || !onSyncSetup) return;
|
||||||
|
|
||||||
|
setIsSettingUpSync(true);
|
||||||
|
try {
|
||||||
|
await onSyncSetup(initialData.id, selectedAccountId, selectedCalendarUrl);
|
||||||
|
setShowSyncSection(false);
|
||||||
|
alert("Synchronisation configurée avec succès !");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting up sync:", error);
|
||||||
|
alert("Erreur lors de la configuration de la synchronisation");
|
||||||
|
} finally {
|
||||||
|
setIsSettingUpSync(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -284,6 +381,137 @@ function CalendarDialog({ open, onClose, onSave, onDelete, initialData }: Calend
|
|||||||
className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 bg-white text-gray-900"
|
className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 bg-white text-gray-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sync Section for Private Calendars */}
|
||||||
|
{isPrivateCalendar && initialData?.id && (
|
||||||
|
<div className="space-y-3 pt-4 border-t border-gray-200">
|
||||||
|
{syncConfig?.syncEnabled ? (
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-blue-900">Synchronisation active</p>
|
||||||
|
<p className="text-xs text-blue-700 mt-1">
|
||||||
|
{syncConfig.mailCredential?.display_name || syncConfig.mailCredential?.email || "Compte"}
|
||||||
|
</p>
|
||||||
|
{syncConfig.lastSyncAt && (
|
||||||
|
<p className="text-xs text-blue-600 mt-1">
|
||||||
|
Dernière sync: {new Date(syncConfig.lastSyncAt).toLocaleString('fr-FR')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="border-blue-400 text-blue-600">
|
||||||
|
<RefreshCw className="w-3 h-3 mr-1" />
|
||||||
|
Sync
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-gray-700">Synchronisation avec courrier</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSyncSection(!showSyncSection)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-3 h-3 mr-1" />
|
||||||
|
{showSyncSection ? "Masquer" : "Configurer"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSyncSection && (
|
||||||
|
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm text-gray-700">Compte email (Infomaniak)</Label>
|
||||||
|
<select
|
||||||
|
value={selectedAccountId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedAccountId(e.target.value);
|
||||||
|
setAvailableCalendars([]);
|
||||||
|
setSelectedCalendarUrl("");
|
||||||
|
}}
|
||||||
|
className="w-full rounded-lg border-gray-300 bg-white text-gray-900 text-sm p-2"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un compte</option>
|
||||||
|
{availableAccounts.map((account) => (
|
||||||
|
<option key={account.id} value={account.id}>
|
||||||
|
{account.display_name || account.email}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAccountId && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDiscoverCalendars}
|
||||||
|
disabled={isDiscovering}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isDiscovering ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Découverte en cours...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Découvrir les calendriers
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{availableCalendars.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm text-gray-700">Calendrier à synchroniser</Label>
|
||||||
|
<select
|
||||||
|
value={selectedCalendarUrl}
|
||||||
|
onChange={(e) => setSelectedCalendarUrl(e.target.value)}
|
||||||
|
className="w-full rounded-lg border-gray-300 bg-white text-gray-900 text-sm p-2"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un calendrier</option>
|
||||||
|
{availableCalendars.map((cal) => (
|
||||||
|
<option key={cal.id} value={cal.url}>
|
||||||
|
{cal.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCalendarUrl && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSetupSync}
|
||||||
|
disabled={isSettingUpSync}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{isSettingUpSync ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Configuration...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LinkIcon className="w-4 h-4 mr-2" />
|
||||||
|
Activer la synchronisation
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="mt-6 border-t border-gray-100 pt-4">
|
<DialogFooter className="mt-6 border-t border-gray-100 pt-4">
|
||||||
@ -1274,7 +1502,32 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
onClose={() => setIsCalendarModalOpen(false)}
|
onClose={() => setIsCalendarModalOpen(false)}
|
||||||
onSave={handleCalendarSave}
|
onSave={handleCalendarSave}
|
||||||
onDelete={handleCalendarDelete}
|
onDelete={handleCalendarDelete}
|
||||||
|
onSyncSetup={async (calendarId, mailCredentialId, externalCalendarUrl) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/calendars/sync", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
calendarId,
|
||||||
|
mailCredentialId,
|
||||||
|
externalCalendarUrl,
|
||||||
|
provider: "infomaniak",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || "Erreur lors de la configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchCalendars();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting up sync:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}}
|
||||||
initialData={selectedCalendar || undefined}
|
initialData={selectedCalendar || undefined}
|
||||||
|
syncConfig={selectedCalendar ? (calendars.find(c => c.id === selectedCalendar.id) as CalendarWithMission)?.syncConfig || null : null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Event dialog */}
|
{/* Event dialog */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user