ultisuite-client/components/agenda/agenda-settings-fields.tsx
R3D347HR4Y 9e9fd208ad
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(admin-settings): enhance admin settings with new components and layout improvements
- Introduced new components for managing admin settings, including AdminListControls, AdminSettingsCard, and TechBrandSelectLabel.
- Implemented dynamic loading for admin settings sections to optimize performance.
- Enhanced the layout of various admin settings sections for better user experience.
- Updated the AiAssistantSection to include LLM provider management and improved model selection.
- Refactored authentication settings to streamline configuration and improve accessibility.
2026-06-15 00:22:20 +02:00

467 lines
17 KiB
TypeScript

"use client"
import { cn } from "@/lib/utils"
import { AgendaCalendarsSettingsFields } from "@/components/agenda/agenda-calendars-settings"
import { AgendaSettingsChipPicker } from "@/components/agenda/agenda-settings-chip-picker"
import { AgendaVideoProviderSelectLabel } from "@/components/agenda/agenda-video-provider-select-label"
import { ThemeModePreview } from "@/components/gmail/quick-settings/settings-preview-icons"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
AGENDA_DURATION_STEPS,
AGENDA_QUICK_DURATION_OPTIONS,
AGENDA_VIDEO_PROVIDERS,
AGENDA_WEEK_START_OPTIONS,
type AgendaDurationStep,
type AgendaTimeFormat,
type AgendaVideoProvider,
type AgendaWeekStart,
} from "@/lib/agenda/agenda-settings-types"
import {
formatDurationStepLabel,
formatQuickDurationLabel,
formatTimeFormatLabel,
formatWeekStartLabel,
videoProviderLabel,
} from "@/lib/agenda/agenda-settings-labels"
import { useAgendaSettingsStore } from "@/lib/agenda/agenda-store"
import {
useAgendaSettingsDestinationOptions,
useAgendaSettingsIdentityOptions,
} from "@/lib/agenda/use-agenda-invitation-suggestions"
import {
normalizeAutoImportInvitationSources,
normalizeInvitationImportExclusions,
} from "@/lib/agenda/agenda-destination-identities"
import { useEffectiveAgendaSettings } from "@/lib/agenda/use-effective-agenda-settings"
import { useIsDemoApp } from "@/lib/demo/use-is-demo-app"
import { useThemeModeControls } from "@/lib/demo/use-theme-mode-controls"
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
import {
MAIL_SETTINGS_PAGE_MASONRY_SECTION_CLASS,
} from "@/lib/mail-chrome-classes"
import type { MailThemeMode } from "@/lib/mail-settings/types"
const THEME_OPTIONS: { id: MailThemeMode; label: string }[] = [
{ id: "light", label: "Clair" },
{ id: "dark", label: "Sombre" },
{ id: "system", label: "Système" },
]
function SettingsSection({
title,
description,
action,
children,
variant = "panel",
}: {
title: string
description?: string
action?: React.ReactNode
children: React.ReactNode
variant?: "panel" | "page"
}) {
return (
<section
className={cn(
"space-y-2 border-b border-border px-4 py-3",
variant === "page" && MAIL_SETTINGS_PAGE_MASONRY_SECTION_CLASS,
)}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<h2 className="text-sm font-medium text-foreground">{title}</h2>
{description ? (
<p className="mt-0.5 text-[11px] leading-snug text-muted-foreground">{description}</p>
) : null}
</div>
{action}
</div>
{children}
</section>
)
}
function PickerRow({
label,
children,
}: {
label: string
children: React.ReactNode
}) {
return (
<div className="grid grid-cols-[minmax(0,0.72fr)_minmax(9.75rem,1.18fr)] items-center gap-2 py-1">
<Label className="min-w-0 text-xs font-normal text-muted-foreground">{label}</Label>
<div className="min-w-0">{children}</div>
</div>
)
}
function minutesToTimeValue(minutes: number): string {
const h = Math.floor(minutes / 60)
const m = minutes % 60
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`
}
function timeValueToMinutes(value: string): number {
const [h, m] = value.split(":").map(Number)
if (!Number.isFinite(h) || !Number.isFinite(m)) return 0
return h * 60 + m
}
export function AgendaSettingsFields({
variant = "panel",
onOpenThemeDialog,
}: {
variant?: "panel" | "page"
onOpenThemeDialog?: () => void
}) {
const effective = useEffectiveAgendaSettings()
const isDemo = useIsDemoApp()
const { themeMode, setThemeMode } = useThemeModeControls()
const defaultVideoProvider = useAgendaSettingsStore((s) => s.defaultVideoProvider)
const setDefaultVideoProvider = useAgendaSettingsStore((s) => s.setDefaultVideoProvider)
const videoProviderApiKeys = useAgendaSettingsStore((s) => s.videoProviderApiKeys)
const setVideoProviderApiKey = useAgendaSettingsStore((s) => s.setVideoProviderApiKey)
const defaultInvitationIdentityKey = useAgendaSettingsStore(
(s) => s.defaultInvitationIdentityKey,
)
const setDefaultInvitationIdentityKey = useAgendaSettingsStore(
(s) => s.setDefaultInvitationIdentityKey,
)
const autoImportInvitationSources = useAgendaSettingsStore(
(s) => s.autoImportInvitationSources,
)
const setAutoImportInvitationSources = useAgendaSettingsStore(
(s) => s.setAutoImportInvitationSources,
)
const invitationImportExclusions = useAgendaSettingsStore(
(s) => s.invitationImportExclusions,
)
const setInvitationImportExclusions = useAgendaSettingsStore(
(s) => s.setInvitationImportExclusions,
)
const weekStart = useAgendaSettingsStore((s) => s.weekStart)
const setWeekStart = useAgendaSettingsStore((s) => s.setWeekStart)
const defaultQuickDurationMinutes = useAgendaSettingsStore(
(s) => s.defaultQuickDurationMinutes,
)
const setDefaultQuickDurationMinutes = useAgendaSettingsStore(
(s) => s.setDefaultQuickDurationMinutes,
)
const visibleHoursStart = useAgendaSettingsStore((s) => s.visibleHoursStart)
const visibleHoursEnd = useAgendaSettingsStore((s) => s.visibleHoursEnd)
const setVisibleHoursStart = useAgendaSettingsStore((s) => s.setVisibleHoursStart)
const setVisibleHoursEnd = useAgendaSettingsStore((s) => s.setVisibleHoursEnd)
const timeFormat = useAgendaSettingsStore((s) => s.timeFormat)
const setTimeFormat = useAgendaSettingsStore((s) => s.setTimeFormat)
const dragSnapMinutes = useAgendaSettingsStore((s) => s.dragSnapMinutes)
const setDragSnapMinutes = useAgendaSettingsStore((s) => s.setDragSnapMinutes)
const buttonSnapMinutes = useAgendaSettingsStore((s) => s.buttonSnapMinutes)
const setButtonSnapMinutes = useAgendaSettingsStore((s) => s.setButtonSnapMinutes)
const identityOptions = useAgendaSettingsIdentityOptions()
const destinationOptions = useAgendaSettingsDestinationOptions()
const activeTheme = effective.orgEnforcesTheme ? effective.themeMode : themeMode
const activeProvider = effective.orgEnforcesVideoProvider
? effective.defaultVideoProvider
: defaultVideoProvider
const needsUserApiKey =
!effective.orgEnforcesVideoProvider &&
activeProvider !== "ultimeet" &&
activeProvider !== "none"
const autoImportItems = normalizeAutoImportInvitationSources(
autoImportInvitationSources,
destinationOptions,
)
const exclusionItems = normalizeInvitationImportExclusions(
invitationImportExclusions,
destinationOptions,
)
const isPage = variant === "page"
const fields = (
<>
{!isPage && !isDemo ? (
<SettingsSection
title="Thème"
variant={variant}
action={
onOpenThemeDialog ? (
<button
type="button"
className="shrink-0 text-xs text-[#1a73e8] hover:underline disabled:opacity-50"
disabled={effective.orgEnforcesTheme}
onClick={onOpenThemeDialog}
>
Arrière-plan
</button>
) : null
}
>
{effective.orgEnforcesTheme ? (
<p className="text-[11px] text-muted-foreground">
Thème imposé par votre organisation.
</p>
) : null}
<div className="grid grid-cols-3 gap-1.5">
{THEME_OPTIONS.map((opt) => (
<button
key={opt.id}
type="button"
disabled={effective.orgEnforcesTheme}
onClick={() => setThemeMode(opt.id)}
className={cn(
"rounded-lg border-2 p-1 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
activeTheme === opt.id
? "border-primary bg-accent/60"
: "border-border hover:border-muted-foreground/50 hover:bg-accent/40",
)}
>
<ThemeModePreview mode={opt.id} className="h-9" />
<span className="mt-0.5 block text-center text-[11px] text-foreground">
{opt.label}
</span>
</button>
))}
</div>
</SettingsSection>
) : null}
<SettingsSection title="Affichage" variant={variant}>
<PickerRow label="Premier jour">
<Select
value={String(weekStart)}
onValueChange={(v) =>
setWeekStart(
v === "auto" ? "auto" : (Number(v) as AgendaWeekStart),
)
}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue>{formatWeekStartLabel(weekStart)}</SelectValue>
</SelectTrigger>
<SelectContent>
{AGENDA_WEEK_START_OPTIONS.map((opt) => (
<SelectItem key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</PickerRow>
<PickerRow label="Format horaire">
<Select
value={timeFormat}
onValueChange={(v) => setTimeFormat(v as AgendaTimeFormat)}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue>{formatTimeFormatLabel(timeFormat)}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="24h">24 h</SelectItem>
<SelectItem value="12h">AM / PM</SelectItem>
</SelectContent>
</Select>
</PickerRow>
<PickerRow label="Heures visibles">
<div className="grid grid-cols-2 gap-1.5">
<Input
type="time"
className="h-8 text-xs [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
value={minutesToTimeValue(visibleHoursStart)}
onChange={(e) => setVisibleHoursStart(timeValueToMinutes(e.target.value))}
/>
<Input
type="time"
className="h-8 text-xs [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
value={minutesToTimeValue(visibleHoursEnd)}
onChange={(e) => setVisibleHoursEnd(timeValueToMinutes(e.target.value))}
/>
</div>
</PickerRow>
</SettingsSection>
<SettingsSection title="Création rapide" variant={variant}>
<PickerRow label="Durée par défaut">
<Select
value={String(defaultQuickDurationMinutes)}
onValueChange={(v) => setDefaultQuickDurationMinutes(Number(v))}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue>{formatQuickDurationLabel(defaultQuickDurationMinutes)}</SelectValue>
</SelectTrigger>
<SelectContent>
{AGENDA_QUICK_DURATION_OPTIONS.map((opt) => (
<SelectItem key={opt.minutes} value={String(opt.minutes)}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</PickerRow>
<PickerRow label="Arrondi glisser">
<Select
value={String(dragSnapMinutes)}
onValueChange={(v) => setDragSnapMinutes(Number(v) as AgendaDurationStep)}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue>{formatDurationStepLabel(dragSnapMinutes)}</SelectValue>
</SelectTrigger>
<SelectContent>
{AGENDA_DURATION_STEPS.map((step) => (
<SelectItem key={step} value={String(step)}>
{formatDurationStepLabel(step)}
</SelectItem>
))}
</SelectContent>
</Select>
</PickerRow>
<PickerRow label="Arrondi boutons +/-">
<Select
value={String(buttonSnapMinutes)}
onValueChange={(v) => setButtonSnapMinutes(Number(v) as AgendaDurationStep)}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue>{formatDurationStepLabel(buttonSnapMinutes)}</SelectValue>
</SelectTrigger>
<SelectContent>
{AGENDA_DURATION_STEPS.map((step) => (
<SelectItem key={step} value={String(step)}>
{formatDurationStepLabel(step)}
</SelectItem>
))}
</SelectContent>
</Select>
</PickerRow>
</SettingsSection>
<SettingsSection
title="Visioconférence"
variant={variant}
description={
effective.orgEnforcesVideoProvider
? "Fournisseur imposé par votre organisation."
: undefined
}
>
<PickerRow label="Fournisseur">
<Select
value={activeProvider}
disabled={effective.orgEnforcesVideoProvider}
onValueChange={(v) => setDefaultVideoProvider(v as AgendaVideoProvider)}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue>
<AgendaVideoProviderSelectLabel provider={activeProvider} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{AGENDA_VIDEO_PROVIDERS.map((provider) => (
<SelectItem key={provider} value={provider}>
<AgendaVideoProviderSelectLabel provider={provider} />
</SelectItem>
))}
</SelectContent>
</Select>
</PickerRow>
{needsUserApiKey ? (
<div className="pt-1">
<Label htmlFor="agenda-video-api-key" className="text-[11px] text-muted-foreground">
Clé API {videoProviderLabel(activeProvider)}
</Label>
<Input
id="agenda-video-api-key"
type="password"
autoComplete="off"
className="mt-1 h-8 text-xs"
placeholder="Optionnel si configurée par l'organisation"
value={videoProviderApiKeys[activeProvider] ?? ""}
onChange={(e) => setVideoProviderApiKey(activeProvider, e.target.value)}
/>
</div>
) : null}
</SettingsSection>
<SettingsSection
title="Invitations par mail"
variant={variant}
description="Import automatique des fichiers .ics reçus et envoi des réponses."
>
<PickerRow label="Envoi par défaut">
<Select
value={defaultInvitationIdentityKey ?? "auto"}
onValueChange={(v) =>
setDefaultInvitationIdentityKey(v === "auto" ? null : v)
}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue placeholder="Identité par défaut" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Identité mail par défaut</SelectItem>
{identityOptions.map((identity) => (
<SelectItem key={identity.key} value={identity.key}>
{identity.label}
</SelectItem>
))}
</SelectContent>
</Select>
</PickerRow>
<div className="space-y-1 pt-1">
<Label className="text-xs font-normal text-muted-foreground">
Import automatique depuis
</Label>
<AgendaSettingsChipPicker
items={autoImportItems}
allowedTypes={["identity", "contact"]}
placeholder="Mails de destination ou contacts…"
emptyHint="Boîtes qui reçoivent les .ics, ou expéditeurs à importer."
onChange={(items) =>
setAutoImportInvitationSources(
normalizeAutoImportInvitationSources(items, destinationOptions),
)
}
/>
</div>
<div className="space-y-1 pt-2">
<Label className="text-xs font-normal text-muted-foreground">
Ne pas importer automatiquement
</Label>
<AgendaSettingsChipPicker
items={exclusionItems}
placeholder="Contact, adresse, dossier, libellé…"
emptyHint="Excluez expéditeurs, dossiers ou règles contacts."
onChange={(items) =>
setInvitationImportExclusions(
normalizeInvitationImportExclusions(items, destinationOptions),
)
}
/>
</div>
</SettingsSection>
<AgendaCalendarsSettingsFields variant={variant} />
</>
)
if (isPage) {
return <AutomationTabMasonry columns={2}>{fields}</AutomationTabMasonry>
}
return fields
}