import { useEffect, useMemo, useState } from 'react'; import * as Dialog from '@radix-ui/react-dialog'; import { ArrowLeft, Bell, Pencil, Plus, Save, Trash2 } from 'lucide-react'; import { request } from '../api/client.js'; import { ConfirmDialog } from '../components/ConfirmDialog.jsx'; import { Field } from '../components/Field.jsx'; import { useToast } from '../components/Toast.jsx'; const unitToHours = { hours: 1, days: 24, weeks: 168, }; function toDisplayThreshold(thresholdHours) { if (thresholdHours % unitToHours.weeks === 0) { return { value: thresholdHours / unitToHours.weeks, unit: 'weeks' }; } if (thresholdHours % unitToHours.days === 0) { return { value: thresholdHours / unitToHours.days, unit: 'days' }; } return { value: thresholdHours, unit: 'hours' }; } function toThresholdHours(condition) { const value = String(condition.value).trim(); if (!/^\d+$/.test(value)) return Number.NaN; return Number.parseInt(value, 10) * unitToHours[condition.unit]; } function formatCertificateValue(value) { if (!value) return '未取得'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '未取得'; return date.toLocaleString(); } function formatCondition(condition) { const unitLabels = { hours: '時間前', days: '日前', weeks: '週間前', }; return `${condition.value}${unitLabels[condition.unit]}`; } export function SiteSettingsPanel({ site, onBack }) { const [alias, setAlias] = useState(site.alias); const [savedAlias, setSavedAlias] = useState(site.alias); const [conditions, setConditions] = useState([]); const [timingDialogOpen, setTimingDialogOpen] = useState(false); const [editingConditionIndex, setEditingConditionIndex] = useState(null); const [timingForm, setTimingForm] = useState({ value: '', unit: 'days' }); const [availableWebhooks, setAvailableWebhooks] = useState([]); const [webhookMethodIds, setWebhookMethodIds] = useState([]); const [pushEnabled, setPushEnabled] = useState(false); const [busy, setBusy] = useState(false); const { showToast } = useToast(); const previewText = useMemo(() => { const hours = conditions .map(toThresholdHours) .filter((value) => Number.isInteger(value) && value > 0) .sort((a, b) => a - b); if (hours.length === 0) return '通知タイミング未設定'; return hours.map((hour) => `${hour} 時間前`).join(' / '); }, [conditions]); useEffect(() => { setAlias(site.alias); setSavedAlias(site.alias); }, [site.alias, site.siteId]); useEffect(() => { async function loadSettings() { const data = await request(`/api/sites/${site.siteId}/settings`); setAvailableWebhooks(data.settings.availableWebhooks); if (data.settings.conditions.length > 0) { setConditions( data.settings.conditions.map((condition) => toDisplayThreshold(condition.thresholdHours)), ); setWebhookMethodIds(data.settings.conditions[0].webhookMethodIds); setPushEnabled(data.settings.conditions[0].pushEnabled); } else { setConditions([]); setWebhookMethodIds([]); setPushEnabled(false); } } loadSettings().catch((err) => showToast({ type: 'error', message: err.message })); }, [showToast, site.siteId]); function openAddConditionDialog() { setEditingConditionIndex(null); setTimingForm({ value: '', unit: 'days' }); setTimingDialogOpen(true); } function openEditConditionDialog(index) { setEditingConditionIndex(index); setTimingForm({ value: String(conditions[index].value), unit: conditions[index].unit, }); setTimingDialogOpen(true); } function saveTimingCondition(event) { event.preventDefault(); event.stopPropagation(); const thresholdHours = toThresholdHours(timingForm); if (!Number.isInteger(thresholdHours) || thresholdHours <= 0 || thresholdHours > 17520) { showToast({ type: 'error', message: '通知タイミングは 1 時間以上、2 年以内で指定してください', }); return; } const duplicated = conditions.some((condition, index) => { if (index === editingConditionIndex) return false; return toThresholdHours(condition) === thresholdHours; }); if (duplicated) { showToast({ type: 'error', message: '同じ通知タイミングは重複して登録できません' }); return; } setConditions((current) => { const nextCondition = { value: Number.parseInt(String(timingForm.value).trim(), 10), unit: timingForm.unit, }; if (editingConditionIndex === null) { return [...current, nextCondition]; } return current.map((condition, index) => index === editingConditionIndex ? nextCondition : condition, ); }); setTimingDialogOpen(false); } function removeCondition(index) { setConditions((current) => current.filter((_, currentIndex) => currentIndex !== index)); } function toggleWebhook(methodId) { setWebhookMethodIds((current) => current.includes(methodId) ? current.filter((id) => id !== methodId) : [...current, methodId], ); } async function saveSettings(event) { event.preventDefault(); setBusy(true); try { const nextAlias = alias.trim(); if (!nextAlias) { throw new Error('エイリアス名を入力してください'); } if (nextAlias.length > 120) { throw new Error('エイリアス名は120文字以内で入力してください'); } const thresholdHours = conditions.map(toThresholdHours); if (thresholdHours.some((value) => !Number.isInteger(value) || value <= 0 || value > 17520)) { throw new Error('通知タイミングは 1 時間以上、2 年以内で指定してください'); } if (new Set(thresholdHours).size !== thresholdHours.length) { throw new Error('同じ通知タイミングは重複して登録できません'); } if (nextAlias !== savedAlias) { await request(`/api/sites/${site.siteId}`, { method: 'PATCH', body: JSON.stringify({ alias: nextAlias }), }); setAlias(nextAlias); setSavedAlias(nextAlias); } if (thresholdHours.length === 0) { await request(`/api/sites/${site.siteId}/settings`, { method: 'DELETE' }); setWebhookMethodIds([]); setPushEnabled(false); } else { await request(`/api/sites/${site.siteId}/settings`, { method: 'PUT', body: JSON.stringify({ conditions: thresholdHours.map((value) => ({ thresholdHours: value })), webhookMethodIds, pushEnabled, }), }); } showToast({ type: 'success', message: '設定を保存しました' }); } catch (err) { showToast({ type: 'error', message: err.message }); } finally { setBusy(false); } } async function deleteSettings() { setBusy(true); try { await request(`/api/sites/${site.siteId}/settings`, { method: 'DELETE' }); setConditions([]); setWebhookMethodIds([]); setPushEnabled(false); showToast({ type: 'success', message: '設定を削除しました' }); } catch (err) { showToast({ type: 'error', message: err.message }); } finally { setBusy(false); } } return (
サイト設定

{savedAlias}

{site.url}

基本情報

setAlias(event.target.value)} maxLength="120" required />

証明書情報

発行元
{site.certificateIssuer || '未取得'}
発行日時
{formatCertificateValue(site.certificateIssuedAt)}
失効日時
{formatCertificateValue(site.certificateExpiresAt)}
{conditions.length === 0 ? (

通知タイミングはまだ設定されていません。

) : ( conditions.map((condition, index) => (
{formatCondition(condition)}
removeCondition(index)} trigger={ } />
)) )}
{editingConditionIndex === null ? '通知タイミングを追加' : '通知タイミングを編集'} 証明書の失効前に通知するタイミングを指定します。 setTimingForm({ ...timingForm, value: event.target.value }) } placeholder="例: 7" required />

通知方法

Webhook

{availableWebhooks.length === 0 ? (

登録済みのWebhookはありません。

) : ( availableWebhooks.map((webhook) => ( )) )}
設定を削除 } />
); }