feat: P2.3 Folder Sharing + P2.5 Email Import + P2.6 Contact Import + P2.7 Free/Busy

- P2.3: Folder sharing system — ShareFolderDialog, sharing-store, sharing-settings
- P2.5: Email import (.eml, .tgz, .zip) with dedup and progress
- P2.6: Contact import (vCard + CSV) with auto-mapping
- P2.7: Free/Busy view grid with color-coded slots
This commit is contained in:
Bernd Rodler
2026-08-07 13:32:33 +02:00
parent 83e29b3ef1
commit e7acf56753
21 changed files with 3321 additions and 43 deletions
+41 -1
View File
@@ -4,13 +4,14 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft, Plus } from "lucide-react";
import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft, Plus, Eye, EyeOff } from "lucide-react";
import { format, parseISO, addHours, addDays, isSameDay } from "date-fns";
import type { CalendarEvent, Calendar, CalendarParticipant, CalendarEventAlert, CalendarRecurrenceRule } from "@/lib/jmap/types";
import { RecurrenceEditor, buildRecurrenceSummary, isSimpleRecurrenceRule } from "./recurrence-editor";
import { parseDuration, getEventColor } from "./event-card";
import { buildAllDayDuration, getEventDisplayEndDate, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
import { ParticipantInput, type ParticipantInputHandle } from "./participant-input";
import { FreeBusyView } from "./free-busy-view";
import {
isOrganizer,
getUserParticipantId,
@@ -351,6 +352,7 @@ export function EventModal({
.map(p => ({ name: p.name, email: p.email }));
});
const [sendInvitations, setSendInvitations] = useState(true);
const [showFreeBusy, setShowFreeBusy] = useState(false);
const participantInputRef = useRef<ParticipantInputHandle>(null);
// Plugin transform: collect conflict warnings for the current event form.
@@ -1074,6 +1076,44 @@ export function EventModal({
onAdd={handleAddAttendee}
onRemove={handleRemoveAttendee}
/>
{attendees.length > 0 && !allDay && (
<div className="mt-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowFreeBusy((prev) => !prev)}
className="text-xs"
>
{showFreeBusy ? (
<EyeOff className="w-3.5 h-3.5 me-1" />
) : (
<Eye className="w-3.5 h-3.5 me-1" />
)}
{showFreeBusy ? t("freeBusy.hide") : t("freeBusy.check")}
</Button>
{showFreeBusy && (
<div className="mt-3">
<FreeBusyView
participants={attendees}
startDate={(() => {
const d = new Date(`${startDate}T${startTime}:00`);
return isNaN(d.getTime()) ? new Date() : d;
})()}
endDate={(() => {
const d = new Date(`${endDate}T${endTime}:00`);
return isNaN(d.getTime()) ? addHours(new Date(`${startDate}T${startTime}:00`), 8) : d;
})()}
onTimeSelect={(start, end) => {
setStartDate(formatDateInput(start));
setStartTime(formatTimeInput(start));
setEndDate(formatDateInput(end));
setEndTime(formatTimeInput(end));
}}
/>
</div>
)}
</div>
)}
{isEdit && statusCounts && (existingParticipants.length > 0) && (
<p className="text-xs text-muted-foreground mt-1.5">
{t("participants.status_summary", {