feat: configurable trip reminders, admin full access, and enhanced audit logging
- Add configurable trip reminder days (1, 3, 9 or custom up to 30) settable by trip owner - Grant administrators full access to edit, archive, delete, view and list all trips - Show trip owner email in audit logs and docker logs when admin edits/deletes another user's trip - Show target user email in audit logs when admin edits or deletes a user account - Use email instead of username in all notifications (Discord/Slack/email) to avoid ambiguity - Grey out notification event toggles when no SMTP/webhook is configured - Grey out trip reminder selector when notifications are disabled - Skip local admin account creation when OIDC_ONLY=true with OIDC configured - Conditional scheduler logging: show disabled reason or active reminder count - Log per-owner reminder creation/update in docker logs - Demote 401/403 HTTP errors to DEBUG log level to reduce noise - Hide edit/archive/delete buttons for non-owner invited users on trip cards - Fix literal "0" rendering on trip cards from SQLite numeric is_owner field - Add missing translation keys across all 14 language files Made-with: Cursor
This commit is contained in:
@@ -71,18 +71,19 @@ function RootRedirect() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { loadUser, token, isAuthenticated, demoMode, setDemoMode, setHasMapsKey, setServerTimezone, setAppRequireMfa } = useAuthStore()
|
||||
const { loadUser, token, isAuthenticated, demoMode, setDemoMode, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore()
|
||||
const { loadSettings } = useSettingsStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
loadUser()
|
||||
}
|
||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean }) => {
|
||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean }) => {
|
||||
if (config?.demo_mode) setDemoMode(true)
|
||||
if (config?.has_maps_key !== undefined) setHasMapsKey(config.has_maps_key)
|
||||
if (config?.timezone) setServerTimezone(config.timezone)
|
||||
if (config?.require_mfa !== undefined) setAppRequireMfa(!!config.require_mfa)
|
||||
if (config?.trip_reminders_enabled !== undefined) setTripRemindersEnabled(config.trip_reminders_enabled)
|
||||
|
||||
if (config?.version) {
|
||||
const storedVersion = localStorage.getItem('trek_app_version')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Modal from '../shared/Modal'
|
||||
import { Calendar, Camera, X, Clipboard, UserPlus } from 'lucide-react'
|
||||
import { Calendar, Camera, X, Clipboard, UserPlus, Bell } from 'lucide-react'
|
||||
import { tripsApi, authApi } from '../../api/client'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
@@ -23,13 +23,17 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
const toast = useToast()
|
||||
const { t } = useTranslation()
|
||||
const currentUser = useAuthStore(s => s.user)
|
||||
const tripRemindersEnabled = useAuthStore(s => s.tripRemindersEnabled)
|
||||
const setTripRemindersEnabled = useAuthStore(s => s.setTripRemindersEnabled)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
reminder_days: 0 as number,
|
||||
})
|
||||
const [customReminder, setCustomReminder] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [coverPreview, setCoverPreview] = useState(null)
|
||||
@@ -41,25 +45,40 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
|
||||
useEffect(() => {
|
||||
if (trip) {
|
||||
const rd = trip.reminder_days ?? 3
|
||||
setFormData({
|
||||
title: trip.title || '',
|
||||
description: trip.description || '',
|
||||
start_date: trip.start_date || '',
|
||||
end_date: trip.end_date || '',
|
||||
reminder_days: rd,
|
||||
})
|
||||
setCustomReminder(![0, 1, 3, 9].includes(rd))
|
||||
setCoverPreview(trip.cover_image || null)
|
||||
} else {
|
||||
setFormData({ title: '', description: '', start_date: '', end_date: '' })
|
||||
setFormData({ title: '', description: '', start_date: '', end_date: '', reminder_days: tripRemindersEnabled ? 3 : 0 })
|
||||
setCustomReminder(false)
|
||||
setCoverPreview(null)
|
||||
}
|
||||
setPendingCoverFile(null)
|
||||
setSelectedMembers([])
|
||||
setError('')
|
||||
if (isOpen) {
|
||||
authApi.getAppConfig().then((c: { trip_reminders_enabled?: boolean }) => {
|
||||
if (c?.trip_reminders_enabled !== undefined) setTripRemindersEnabled(c.trip_reminders_enabled)
|
||||
}).catch(() => {})
|
||||
}
|
||||
if (!trip) {
|
||||
authApi.listUsers().then(d => setAllUsers(d.users || [])).catch(() => {})
|
||||
}
|
||||
}, [trip, isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!trip && isOpen) {
|
||||
setFormData(prev => ({ ...prev, reminder_days: tripRemindersEnabled ? 3 : 0 }))
|
||||
}
|
||||
}, [tripRemindersEnabled])
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
@@ -74,6 +93,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
description: formData.description.trim() || null,
|
||||
start_date: formData.start_date || null,
|
||||
end_date: formData.end_date || null,
|
||||
reminder_days: formData.reminder_days,
|
||||
})
|
||||
// Add selected members for newly created trips
|
||||
if (selectedMembers.length > 0 && result?.trip?.id) {
|
||||
@@ -272,6 +292,59 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reminder — only visible to owner (or when creating) */}
|
||||
{(!isEditing || trip?.user_id === currentUser?.id || currentUser?.role === 'admin') && (
|
||||
<div className={!tripRemindersEnabled ? 'opacity-50' : ''}>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">
|
||||
<Bell className="inline w-4 h-4 mr-1" />{t('trips.reminder')}
|
||||
</label>
|
||||
{!tripRemindersEnabled ? (
|
||||
<p className="text-xs text-slate-400 bg-slate-50 rounded-lg p-3">
|
||||
{t('trips.reminderDisabledHint')}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ value: 0, label: t('trips.reminderNone') },
|
||||
{ value: 1, label: `1 ${t('trips.reminderDay')}` },
|
||||
{ value: 3, label: `3 ${t('trips.reminderDays')}` },
|
||||
{ value: 9, label: `9 ${t('trips.reminderDays')}` },
|
||||
].map(opt => (
|
||||
<button key={opt.value} type="button"
|
||||
onClick={() => { update('reminder_days', opt.value); setCustomReminder(false) }}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors ${
|
||||
!customReminder && formData.reminder_days === opt.value
|
||||
? 'bg-slate-900 text-white border-slate-900'
|
||||
: 'bg-white text-slate-600 border-slate-200 hover:border-slate-300'
|
||||
}`}>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
<button type="button"
|
||||
onClick={() => { setCustomReminder(true); if ([0, 1, 3, 9].includes(formData.reminder_days)) update('reminder_days', 7) }}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors ${
|
||||
customReminder
|
||||
? 'bg-slate-900 text-white border-slate-900'
|
||||
: 'bg-white text-slate-600 border-slate-200 hover:border-slate-300'
|
||||
}`}>
|
||||
{t('trips.reminderCustom')}
|
||||
</button>
|
||||
</div>
|
||||
{customReminder && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<input type="number" min={1} max={30}
|
||||
value={formData.reminder_days}
|
||||
onChange={e => update('reminder_days', Math.max(1, Math.min(30, Number(e.target.value) || 1)))}
|
||||
className="w-20 px-3 py-1.5 border border-slate-200 rounded-lg text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-300" />
|
||||
<span className="text-xs text-slate-500">{t('trips.reminderDaysBefore')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Members — only for new trips */}
|
||||
{!isEditing && allUsers.filter(u => u.id !== currentUser?.id).length > 0 && (
|
||||
<div>
|
||||
@@ -312,11 +385,6 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!formData.start_date && !formData.end_date && (
|
||||
<p className="text-xs text-slate-400 bg-slate-50 rounded-lg p-3">
|
||||
{t('dashboard.noDateHint')}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -30,6 +30,13 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'كلمة المرور',
|
||||
'common.saving': 'جارٍ الحفظ...',
|
||||
'common.saved': 'تم الحفظ',
|
||||
'trips.reminder': 'تذكير',
|
||||
'trips.reminderNone': 'بدون',
|
||||
'trips.reminderDay': 'يوم',
|
||||
'trips.reminderDays': 'أيام',
|
||||
'trips.reminderCustom': 'مخصص',
|
||||
'trips.reminderDaysBefore': 'أيام قبل المغادرة',
|
||||
'trips.reminderDisabledHint': 'تذكيرات الرحلة معطلة. قم بتفعيلها من الإدارة > الإعدادات > الإشعارات.',
|
||||
'common.update': 'تحديث',
|
||||
'common.change': 'تغيير',
|
||||
'common.uploading': 'جارٍ الرفع...',
|
||||
@@ -165,6 +172,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'أحداث الإشعارات',
|
||||
'admin.notifications.eventsHint': 'اختر الأحداث التي تُفعّل الإشعارات لجميع المستخدمين.',
|
||||
'admin.notifications.configureFirst': 'قم بتكوين إعدادات SMTP أو Webhook أدناه أولاً، ثم قم بتفعيل الأحداث.',
|
||||
'admin.notifications.save': 'حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.saved': 'تم حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.testWebhook': 'إرسال webhook تجريبي',
|
||||
@@ -173,6 +181,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'البريد والإشعارات',
|
||||
'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.',
|
||||
'admin.smtp.testButton': 'إرسال بريد تجريبي',
|
||||
'admin.webhook.hint': 'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).',
|
||||
'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح',
|
||||
'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي',
|
||||
'dayplan.icsTooltip': 'تصدير التقويم (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Senha',
|
||||
'common.saving': 'Salvando...',
|
||||
'common.saved': 'Salvo',
|
||||
'trips.reminder': 'Lembrete',
|
||||
'trips.reminderNone': 'Nenhum',
|
||||
'trips.reminderDay': 'dia',
|
||||
'trips.reminderDays': 'dias',
|
||||
'trips.reminderCustom': 'Personalizado',
|
||||
'trips.reminderDaysBefore': 'dias antes da partida',
|
||||
'trips.reminderDisabledHint': 'Os lembretes de viagem estão desativados. Ative-os em Admin > Configurações > Notificações.',
|
||||
'common.update': 'Atualizar',
|
||||
'common.change': 'Alterar',
|
||||
'common.uploading': 'Enviando…',
|
||||
@@ -160,6 +167,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventos de notificação',
|
||||
'admin.notifications.eventsHint': 'Escolha quais eventos acionam notificações para todos os usuários.',
|
||||
'admin.notifications.configureFirst': 'Configure primeiro as configurações SMTP ou webhook abaixo, depois ative os eventos.',
|
||||
'admin.notifications.save': 'Salvar configurações de notificação',
|
||||
'admin.notifications.saved': 'Configurações de notificação salvas',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de teste',
|
||||
@@ -168,6 +176,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'E-mail e notificações',
|
||||
'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.',
|
||||
'admin.smtp.testButton': 'Enviar e-mail de teste',
|
||||
'admin.webhook.hint': 'Enviar notificações para um webhook externo (Discord, Slack, etc.).',
|
||||
'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso',
|
||||
'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste',
|
||||
'dayplan.icsTooltip': 'Exportar calendário (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Heslo',
|
||||
'common.saving': 'Ukládání...',
|
||||
'common.saved': 'Uloženo',
|
||||
'trips.reminder': 'Připomínka',
|
||||
'trips.reminderNone': 'Žádná',
|
||||
'trips.reminderDay': 'den',
|
||||
'trips.reminderDays': 'dní',
|
||||
'trips.reminderCustom': 'Vlastní',
|
||||
'trips.reminderDaysBefore': 'dní před odjezdem',
|
||||
'trips.reminderDisabledHint': 'Připomínky výletů jsou zakázány. Povolte je v Správa > Nastavení > Oznámení.',
|
||||
'common.update': 'Aktualizovat',
|
||||
'common.change': 'Změnit',
|
||||
'common.uploading': 'Nahrávání…',
|
||||
@@ -246,6 +253,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Události oznámení',
|
||||
'admin.notifications.eventsHint': 'Vyberte, které události spouštějí oznámení pro všechny uživatele.',
|
||||
'admin.notifications.configureFirst': 'Nejprve nakonfigurujte nastavení SMTP nebo webhooku níže, poté povolte události.',
|
||||
'admin.notifications.save': 'Uložit nastavení oznámení',
|
||||
'admin.notifications.saved': 'Nastavení oznámení uloženo',
|
||||
'admin.notifications.testWebhook': 'Odeslat testovací webhook',
|
||||
@@ -254,6 +262,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'E-mail a oznámení',
|
||||
'admin.smtp.hint': 'Konfigurace SMTP pro odesílání e-mailových oznámení.',
|
||||
'admin.smtp.testButton': 'Odeslat testovací e-mail',
|
||||
'admin.webhook.hint': 'Odesílat oznámení na externí webhook (Discord, Slack atd.).',
|
||||
'admin.smtp.testSuccess': 'Testovací e-mail byl úspěšně odeslán',
|
||||
'admin.smtp.testFailed': 'Odeslání testovacího e-mailu se nezdařilo',
|
||||
'dayplan.icsTooltip': 'Exportovat kalendář (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Passwort',
|
||||
'common.saving': 'Speichern...',
|
||||
'common.saved': 'Gespeichert',
|
||||
'trips.reminder': 'Erinnerung',
|
||||
'trips.reminderNone': 'Keine',
|
||||
'trips.reminderDay': 'Tag',
|
||||
'trips.reminderDays': 'Tage',
|
||||
'trips.reminderCustom': 'Benutzerdefiniert',
|
||||
'trips.reminderDaysBefore': 'Tage vor Abreise',
|
||||
'trips.reminderDisabledHint': 'Reiseerinnerungen sind deaktiviert. Aktivieren Sie sie unter Admin > Einstellungen > Benachrichtigungen.',
|
||||
'common.update': 'Aktualisieren',
|
||||
'common.change': 'Ändern',
|
||||
'common.uploading': 'Hochladen…',
|
||||
@@ -160,6 +167,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Benachrichtigungsereignisse',
|
||||
'admin.notifications.eventsHint': 'Wähle, welche Ereignisse Benachrichtigungen für alle Benutzer auslösen.',
|
||||
'admin.notifications.configureFirst': 'Konfiguriere zuerst die SMTP- oder Webhook-Einstellungen unten, dann aktiviere die Events.',
|
||||
'admin.notifications.save': 'Benachrichtigungseinstellungen speichern',
|
||||
'admin.notifications.saved': 'Benachrichtigungseinstellungen gespeichert',
|
||||
'admin.notifications.testWebhook': 'Test-Webhook senden',
|
||||
@@ -168,6 +176,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'E-Mail & Benachrichtigungen',
|
||||
'admin.smtp.hint': 'SMTP-Konfiguration zum Versenden von E-Mail-Benachrichtigungen.',
|
||||
'admin.smtp.testButton': 'Test-E-Mail senden',
|
||||
'admin.webhook.hint': 'Benachrichtigungen an einen externen Webhook senden (Discord, Slack usw.).',
|
||||
'admin.smtp.testSuccess': 'Test-E-Mail erfolgreich gesendet',
|
||||
'admin.smtp.testFailed': 'Test-E-Mail fehlgeschlagen',
|
||||
'dayplan.icsTooltip': 'Kalender exportieren (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Password',
|
||||
'common.saving': 'Saving...',
|
||||
'common.saved': 'Saved',
|
||||
'trips.reminder': 'Reminder',
|
||||
'trips.reminderNone': 'None',
|
||||
'trips.reminderDay': 'day',
|
||||
'trips.reminderDays': 'days',
|
||||
'trips.reminderCustom': 'Custom',
|
||||
'trips.reminderDaysBefore': 'days before departure',
|
||||
'trips.reminderDisabledHint': 'Trip reminders are disabled. Enable them in Admin > Settings > Notifications.',
|
||||
'common.update': 'Update',
|
||||
'common.change': 'Change',
|
||||
'common.uploading': 'Uploading…',
|
||||
@@ -157,6 +164,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Notification Events',
|
||||
'admin.notifications.eventsHint': 'Choose which events trigger notifications for all users.',
|
||||
'admin.notifications.configureFirst': 'Configure the SMTP or webhook settings below first, then enable events.',
|
||||
'admin.notifications.save': 'Save notification settings',
|
||||
'admin.notifications.saved': 'Notification settings saved',
|
||||
'admin.notifications.testWebhook': 'Send test webhook',
|
||||
@@ -165,6 +173,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'Email & Notifications',
|
||||
'admin.smtp.hint': 'SMTP configuration for sending email notifications.',
|
||||
'admin.smtp.testButton': 'Send test email',
|
||||
'admin.webhook.hint': 'Send notifications to an external webhook (Discord, Slack, etc.).',
|
||||
'admin.smtp.testSuccess': 'Test email sent successfully',
|
||||
'admin.smtp.testFailed': 'Test email failed',
|
||||
'settings.notificationsDisabled': 'Notifications are not configured. Ask an admin to enable email or webhook notifications.',
|
||||
|
||||
@@ -26,6 +26,13 @@ const es: Record<string, string> = {
|
||||
'common.password': 'Contraseña',
|
||||
'common.saving': 'Guardando...',
|
||||
'common.saved': 'Guardado',
|
||||
'trips.reminder': 'Recordatorio',
|
||||
'trips.reminderNone': 'Ninguno',
|
||||
'trips.reminderDay': 'día',
|
||||
'trips.reminderDays': 'días',
|
||||
'trips.reminderCustom': 'Personalizado',
|
||||
'trips.reminderDaysBefore': 'días antes de la salida',
|
||||
'trips.reminderDisabledHint': 'Los recordatorios de viaje están desactivados. Actívalos en Admin > Configuración > Notificaciones.',
|
||||
'common.update': 'Actualizar',
|
||||
'common.change': 'Cambiar',
|
||||
'common.uploading': 'Subiendo…',
|
||||
@@ -161,6 +168,7 @@ const es: Record<string, string> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventos de notificación',
|
||||
'admin.notifications.eventsHint': 'Elige qué eventos activan notificaciones para todos los usuarios.',
|
||||
'admin.notifications.configureFirst': 'Configura primero los ajustes SMTP o webhook a continuación, luego activa los eventos.',
|
||||
'admin.notifications.save': 'Guardar configuración de notificaciones',
|
||||
'admin.notifications.saved': 'Configuración de notificaciones guardada',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de prueba',
|
||||
@@ -169,6 +177,7 @@ const es: Record<string, string> = {
|
||||
'admin.smtp.title': 'Correo y notificaciones',
|
||||
'admin.smtp.hint': 'Configuración SMTP para el envío de notificaciones por correo.',
|
||||
'admin.smtp.testButton': 'Enviar correo de prueba',
|
||||
'admin.webhook.hint': 'Enviar notificaciones a un webhook externo (Discord, Slack, etc.).',
|
||||
'admin.smtp.testSuccess': 'Correo de prueba enviado correctamente',
|
||||
'admin.smtp.testFailed': 'Error al enviar correo de prueba',
|
||||
'dayplan.icsTooltip': 'Exportar calendario (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const fr: Record<string, string> = {
|
||||
'common.password': 'Mot de passe',
|
||||
'common.saving': 'Enregistrement…',
|
||||
'common.saved': 'Enregistré',
|
||||
'trips.reminder': 'Rappel',
|
||||
'trips.reminderNone': 'Aucun',
|
||||
'trips.reminderDay': 'jour',
|
||||
'trips.reminderDays': 'jours',
|
||||
'trips.reminderCustom': 'Personnalisé',
|
||||
'trips.reminderDaysBefore': 'jours avant le départ',
|
||||
'trips.reminderDisabledHint': 'Les rappels de voyage sont désactivés. Activez-les dans Admin > Paramètres > Notifications.',
|
||||
'common.update': 'Mettre à jour',
|
||||
'common.change': 'Modifier',
|
||||
'common.uploading': 'Import en cours…',
|
||||
@@ -160,6 +167,7 @@ const fr: Record<string, string> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Événements de notification',
|
||||
'admin.notifications.eventsHint': 'Choisissez quels événements déclenchent des notifications pour tous les utilisateurs.',
|
||||
'admin.notifications.configureFirst': 'Configurez d\'abord les paramètres SMTP ou webhook ci-dessous, puis activez les événements.',
|
||||
'admin.notifications.save': 'Enregistrer les paramètres de notification',
|
||||
'admin.notifications.saved': 'Paramètres de notification enregistrés',
|
||||
'admin.notifications.testWebhook': 'Envoyer un webhook de test',
|
||||
@@ -168,6 +176,7 @@ const fr: Record<string, string> = {
|
||||
'admin.smtp.title': 'E-mail et notifications',
|
||||
'admin.smtp.hint': 'Configuration SMTP pour l\'envoi des notifications par e-mail.',
|
||||
'admin.smtp.testButton': 'Envoyer un e-mail de test',
|
||||
'admin.webhook.hint': 'Envoyer des notifications vers un webhook externe (Discord, Slack, etc.).',
|
||||
'admin.smtp.testSuccess': 'E-mail de test envoyé avec succès',
|
||||
'admin.smtp.testFailed': 'Échec de l\'e-mail de test',
|
||||
'dayplan.icsTooltip': 'Exporter le calendrier (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Jelszó',
|
||||
'common.saving': 'Mentés...',
|
||||
'common.saved': 'Mentve',
|
||||
'trips.reminder': 'Emlékeztető',
|
||||
'trips.reminderNone': 'Nincs',
|
||||
'trips.reminderDay': 'nap',
|
||||
'trips.reminderDays': 'nap',
|
||||
'trips.reminderCustom': 'Egyéni',
|
||||
'trips.reminderDaysBefore': 'nappal indulás előtt',
|
||||
'trips.reminderDisabledHint': 'Az utazási emlékeztetők ki vannak kapcsolva. Kapcsold be az Admin > Beállítások > Értesítések menüben.',
|
||||
'common.update': 'Frissítés',
|
||||
'common.change': 'Módosítás',
|
||||
'common.uploading': 'Feltöltés…',
|
||||
@@ -245,6 +252,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Értesítési események',
|
||||
'admin.notifications.eventsHint': 'Válaszd ki, mely események indítsanak értesítéseket minden felhasználó számára.',
|
||||
'admin.notifications.configureFirst': 'Először konfiguráld az SMTP vagy webhook beállításokat lent, majd engedélyezd az eseményeket.',
|
||||
'admin.notifications.save': 'Értesítési beállítások mentése',
|
||||
'admin.notifications.saved': 'Értesítési beállítások mentve',
|
||||
'admin.notifications.testWebhook': 'Teszt webhook küldése',
|
||||
@@ -253,6 +261,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'E-mail és értesítések',
|
||||
'admin.smtp.hint': 'SMTP konfiguráció e-mail értesítések küldéséhez.',
|
||||
'admin.smtp.testButton': 'Teszt e-mail küldése',
|
||||
'admin.webhook.hint': 'Értesítések küldése külső webhookra (Discord, Slack stb.).',
|
||||
'admin.smtp.testSuccess': 'Teszt e-mail sikeresen elküldve',
|
||||
'admin.smtp.testFailed': 'Teszt e-mail küldése sikertelen',
|
||||
'dayplan.icsTooltip': 'Naptár exportálása (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.password': 'Password',
|
||||
'common.saving': 'Salvataggio...',
|
||||
'common.saved': 'Salvato',
|
||||
'trips.reminder': 'Promemoria',
|
||||
'trips.reminderNone': 'Nessuno',
|
||||
'trips.reminderDay': 'giorno',
|
||||
'trips.reminderDays': 'giorni',
|
||||
'trips.reminderCustom': 'Personalizzato',
|
||||
'trips.reminderDaysBefore': 'giorni prima della partenza',
|
||||
'trips.reminderDisabledHint': 'I promemoria dei viaggi sono disabilitati. Abilitali in Admin > Impostazioni > Notifiche.',
|
||||
'common.update': 'Aggiorna',
|
||||
'common.change': 'Cambia',
|
||||
'common.uploading': 'Caricamento…',
|
||||
@@ -245,6 +252,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventi di notifica',
|
||||
'admin.notifications.eventsHint': 'Scegli quali eventi attivano le notifiche per tutti gli utenti.',
|
||||
'admin.notifications.configureFirst': 'Configura prima le impostazioni SMTP o webhook qui sotto, poi abilita gli eventi.',
|
||||
'admin.notifications.save': 'Salva impostazioni notifiche',
|
||||
'admin.notifications.saved': 'Impostazioni notifiche salvate',
|
||||
'admin.notifications.testWebhook': 'Invia webhook di test',
|
||||
@@ -253,6 +261,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.smtp.title': 'Email e notifiche',
|
||||
'admin.smtp.hint': 'Configurazione SMTP per l\'invio delle notifiche via e-mail.',
|
||||
'admin.smtp.testButton': 'Invia email di prova',
|
||||
'admin.webhook.hint': 'Invia notifiche a un webhook esterno (Discord, Slack, ecc.).',
|
||||
'admin.smtp.testSuccess': 'Email di prova inviata con successo',
|
||||
'admin.smtp.testFailed': 'Invio email di prova fallito',
|
||||
'dayplan.icsTooltip': 'Esporta calendario (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const nl: Record<string, string> = {
|
||||
'common.password': 'Wachtwoord',
|
||||
'common.saving': 'Opslaan...',
|
||||
'common.saved': 'Opgeslagen',
|
||||
'trips.reminder': 'Herinnering',
|
||||
'trips.reminderNone': 'Geen',
|
||||
'trips.reminderDay': 'dag',
|
||||
'trips.reminderDays': 'dagen',
|
||||
'trips.reminderCustom': 'Aangepast',
|
||||
'trips.reminderDaysBefore': 'dagen voor vertrek',
|
||||
'trips.reminderDisabledHint': 'Reisherinneringen zijn uitgeschakeld. Schakel ze in via Admin > Instellingen > Meldingen.',
|
||||
'common.update': 'Bijwerken',
|
||||
'common.change': 'Wijzigen',
|
||||
'common.uploading': 'Uploaden…',
|
||||
@@ -160,6 +167,7 @@ const nl: Record<string, string> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Meldingsgebeurtenissen',
|
||||
'admin.notifications.eventsHint': 'Kies welke gebeurtenissen meldingen activeren voor alle gebruikers.',
|
||||
'admin.notifications.configureFirst': 'Configureer eerst de SMTP- of webhook-instellingen hieronder en schakel dan de events in.',
|
||||
'admin.notifications.save': 'Meldingsinstellingen opslaan',
|
||||
'admin.notifications.saved': 'Meldingsinstellingen opgeslagen',
|
||||
'admin.notifications.testWebhook': 'Testwebhook verzenden',
|
||||
@@ -168,6 +176,7 @@ const nl: Record<string, string> = {
|
||||
'admin.smtp.title': 'E-mail en meldingen',
|
||||
'admin.smtp.hint': 'SMTP-configuratie voor het verzenden van e-mailmeldingen.',
|
||||
'admin.smtp.testButton': 'Test-e-mail verzenden',
|
||||
'admin.webhook.hint': 'Meldingen verzenden naar een externe webhook (Discord, Slack, enz.).',
|
||||
'admin.smtp.testSuccess': 'Test-e-mail succesvol verzonden',
|
||||
'admin.smtp.testFailed': 'Test-e-mail mislukt',
|
||||
'dayplan.icsTooltip': 'Kalender exporteren (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const ru: Record<string, string> = {
|
||||
'common.password': 'Пароль',
|
||||
'common.saving': 'Сохранение...',
|
||||
'common.saved': 'Сохранено',
|
||||
'trips.reminder': 'Напоминание',
|
||||
'trips.reminderNone': 'Нет',
|
||||
'trips.reminderDay': 'день',
|
||||
'trips.reminderDays': 'дней',
|
||||
'trips.reminderCustom': 'Другое',
|
||||
'trips.reminderDaysBefore': 'дней до отъезда',
|
||||
'trips.reminderDisabledHint': 'Напоминания о поездках отключены. Включите их в Админ > Настройки > Уведомления.',
|
||||
'common.update': 'Обновить',
|
||||
'common.change': 'Изменить',
|
||||
'common.uploading': 'Загрузка…',
|
||||
@@ -160,6 +167,7 @@ const ru: Record<string, string> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'События уведомлений',
|
||||
'admin.notifications.eventsHint': 'Выберите, какие события вызывают уведомления для всех пользователей.',
|
||||
'admin.notifications.configureFirst': 'Сначала настройте SMTP или webhook ниже, затем включите события.',
|
||||
'admin.notifications.save': 'Сохранить настройки уведомлений',
|
||||
'admin.notifications.saved': 'Настройки уведомлений сохранены',
|
||||
'admin.notifications.testWebhook': 'Отправить тестовый вебхук',
|
||||
@@ -168,6 +176,7 @@ const ru: Record<string, string> = {
|
||||
'admin.smtp.title': 'Почта и уведомления',
|
||||
'admin.smtp.hint': 'Конфигурация SMTP для отправки уведомлений по электронной почте.',
|
||||
'admin.smtp.testButton': 'Отправить тестовое письмо',
|
||||
'admin.webhook.hint': 'Отправлять уведомления через внешний webhook (Discord, Slack и т.д.).',
|
||||
'admin.smtp.testSuccess': 'Тестовое письмо успешно отправлено',
|
||||
'admin.smtp.testFailed': 'Ошибка отправки тестового письма',
|
||||
'dayplan.icsTooltip': 'Экспорт календаря (ICS)',
|
||||
|
||||
@@ -26,6 +26,13 @@ const zh: Record<string, string> = {
|
||||
'common.password': '密码',
|
||||
'common.saving': '保存中...',
|
||||
'common.saved': '已保存',
|
||||
'trips.reminder': '提醒',
|
||||
'trips.reminderNone': '无',
|
||||
'trips.reminderDay': '天',
|
||||
'trips.reminderDays': '天',
|
||||
'trips.reminderCustom': '自定义',
|
||||
'trips.reminderDaysBefore': '天前提醒',
|
||||
'trips.reminderDisabledHint': '旅行提醒已禁用。请在管理 > 设置 > 通知中启用。',
|
||||
'common.update': '更新',
|
||||
'common.change': '修改',
|
||||
'common.uploading': '上传中…',
|
||||
@@ -160,6 +167,7 @@ const zh: Record<string, string> = {
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': '通知事件',
|
||||
'admin.notifications.eventsHint': '选择哪些事件为所有用户触发通知。',
|
||||
'admin.notifications.configureFirst': '请先在下方配置 SMTP 或 Webhook,然后启用事件。',
|
||||
'admin.notifications.save': '保存通知设置',
|
||||
'admin.notifications.saved': '通知设置已保存',
|
||||
'admin.notifications.testWebhook': '发送测试 Webhook',
|
||||
@@ -168,6 +176,7 @@ const zh: Record<string, string> = {
|
||||
'admin.smtp.title': '邮件与通知',
|
||||
'admin.smtp.hint': '用于发送电子邮件通知的 SMTP 配置。',
|
||||
'admin.smtp.testButton': '发送测试邮件',
|
||||
'admin.webhook.hint': '向外部 Webhook 发送通知(Discord、Slack 等)。',
|
||||
'admin.smtp.testSuccess': '测试邮件发送成功',
|
||||
'admin.smtp.testFailed': '测试邮件发送失败',
|
||||
'dayplan.icsTooltip': '导出日历 (ICS)',
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function AdminPage(): React.ReactElement {
|
||||
const [updating, setUpdating] = useState<boolean>(false)
|
||||
const [updateResult, setUpdateResult] = useState<'success' | 'error' | null>(null)
|
||||
|
||||
const { user: currentUser, updateApiKeys, setAppRequireMfa } = useAuthStore()
|
||||
const { user: currentUser, updateApiKeys, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore()
|
||||
const navigate = useNavigate()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -999,9 +999,15 @@ export default function AdminPage(): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{/* Notification event toggles — shown when any channel is active */}
|
||||
{(smtpValues.notification_channel || 'none') !== 'none' && (
|
||||
<div className="space-y-2 pt-2 border-t border-slate-100">
|
||||
{(smtpValues.notification_channel || 'none') !== 'none' && (() => {
|
||||
const ch = smtpValues.notification_channel || 'none'
|
||||
const configValid = ch === 'email' ? !!(smtpValues.smtp_host?.trim()) : ch === 'webhook' ? !!(smtpValues.notification_webhook_url?.trim()) : false
|
||||
return (
|
||||
<div className={`space-y-2 pt-2 border-t border-slate-100 ${!configValid ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
<p className="text-xs font-medium text-slate-500 uppercase tracking-wider mb-2">{t('admin.notifications.events')}</p>
|
||||
{!configValid && (
|
||||
<p className="text-[10px] text-amber-600 mb-3">{t('admin.notifications.configureFirst')}</p>
|
||||
)}
|
||||
<p className="text-[10px] text-slate-400 mb-3">{t('admin.notifications.eventsHint')}</p>
|
||||
{[
|
||||
{ key: 'notify_trip_invite', label: t('settings.notifyTripInvite') },
|
||||
@@ -1029,7 +1035,8 @@ export default function AdminPage(): React.ReactElement {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Email (SMTP) settings — shown when email channel is active */}
|
||||
{(smtpValues.notification_channel || 'none') === 'email' && (
|
||||
@@ -1072,7 +1079,7 @@ export default function AdminPage(): React.ReactElement {
|
||||
{/* Webhook settings — shown when webhook channel is active */}
|
||||
{(smtpValues.notification_channel || 'none') === 'webhook' && (
|
||||
<div className="space-y-3 pt-2 border-t border-slate-100">
|
||||
<p className="text-xs text-slate-400">{t('admin.webhook.hint') || 'Send notifications to an external webhook (Discord, Slack, etc.).'}</p>
|
||||
<p className="text-xs text-slate-400">{t('admin.webhook.hint')}</p>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">Webhook URL</label>
|
||||
<input
|
||||
@@ -1097,6 +1104,9 @@ export default function AdminPage(): React.ReactElement {
|
||||
try {
|
||||
await authApi.updateAppSettings(payload)
|
||||
toast.success(t('admin.notifications.saved'))
|
||||
authApi.getAppConfig().then((c: { trip_reminders_enabled?: boolean }) => {
|
||||
if (c?.trip_reminders_enabled !== undefined) setTripRemindersEnabled(c.trip_reminders_enabled)
|
||||
}).catch(() => {})
|
||||
} catch { toast.error(t('common.error')) }
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium hover:bg-slate-800 transition-colors"
|
||||
|
||||
@@ -145,9 +145,10 @@ interface TripCardProps {
|
||||
t: (key: string, params?: Record<string, string | number | null>) => string
|
||||
locale: string
|
||||
dark?: boolean
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
function SpotlightCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale, dark }: TripCardProps): React.ReactElement {
|
||||
function SpotlightCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale, dark, isAdmin }: TripCardProps): React.ReactElement {
|
||||
const status = getTripStatus(trip)
|
||||
|
||||
const coverBg = trip.cover_image
|
||||
@@ -186,12 +187,14 @@ function SpotlightCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale,
|
||||
</div>
|
||||
|
||||
{/* Top-right actions */}
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
<div style={{ position: 'absolute', top: 16, right: 16, display: 'flex', gap: 6 }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconBtn onClick={() => onEdit(trip)} title={t('common.edit')}><Edit2 size={14} /></IconBtn>
|
||||
<IconBtn onClick={() => onArchive(trip.id)} title={t('dashboard.archive')}><Archive size={14} /></IconBtn>
|
||||
<IconBtn onClick={() => onDelete(trip)} title={t('common.delete')} danger><Trash2 size={14} /></IconBtn>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom content */}
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '20px 24px' }}>
|
||||
@@ -228,7 +231,7 @@ function SpotlightCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale,
|
||||
}
|
||||
|
||||
// ── Regular Trip Card ────────────────────────────────────────────────────────
|
||||
function TripCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale }: Omit<TripCardProps, 'dark'>): React.ReactElement {
|
||||
function TripCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale, isAdmin }: Omit<TripCardProps, 'dark'>): React.ReactElement {
|
||||
const status = getTripStatus(trip)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
@@ -305,19 +308,21 @@ function TripCard({ trip, onEdit, onDelete, onArchive, onClick, t, locale }: Omi
|
||||
<Stat label={t('dashboard.places')} value={trip.place_count || 0} />
|
||||
</div>
|
||||
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
<div style={{ display: 'flex', gap: 6, borderTop: '1px solid #f3f4f6', paddingTop: 10 }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label={t('common.edit')} />
|
||||
<CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label={t('dashboard.archive')} />
|
||||
<CardAction onClick={() => onDelete(trip)} icon={<Trash2 size={12} />} label={t('common.delete')} danger />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── List View Item ──────────────────────────────────────────────────────────
|
||||
function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale }: Omit<TripCardProps, 'dark'>): React.ReactElement {
|
||||
function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale, isAdmin }: Omit<TripCardProps, 'dark'>): React.ReactElement {
|
||||
const status = getTripStatus(trip)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
@@ -403,11 +408,13 @@ function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale }:
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
<CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label="" />
|
||||
<CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label="" />
|
||||
<CardAction onClick={() => onDelete(trip)} icon={<Trash2 size={12} />} label="" danger />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -421,9 +428,10 @@ interface ArchivedRowProps {
|
||||
onClick: (trip: DashboardTrip) => void
|
||||
t: (key: string, params?: Record<string, string | number | null>) => string
|
||||
locale: string
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
function ArchivedRow({ trip, onEdit, onUnarchive, onDelete, onClick, t, locale }: ArchivedRowProps): React.ReactElement {
|
||||
function ArchivedRow({ trip, onEdit, onUnarchive, onDelete, onClick, t, locale, isAdmin }: ArchivedRowProps): React.ReactElement {
|
||||
return (
|
||||
<div onClick={() => onClick(trip)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px',
|
||||
@@ -449,6 +457,7 @@ function ArchivedRow({ trip, onEdit, onUnarchive, onDelete, onClick, t, locale }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(!!trip.is_owner || isAdmin) && (
|
||||
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
<button onClick={() => onUnarchive(trip.id)} title={t('dashboard.restore')} style={{ padding: '4px 8px', borderRadius: 8, border: '1px solid #e5e7eb', background: 'white', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: '#6b7280' }}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--text-faint)'; e.currentTarget.style.color = 'var(--text-primary)' }}
|
||||
@@ -461,6 +470,7 @@ function ArchivedRow({ trip, onEdit, onUnarchive, onDelete, onClick, t, locale }
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -539,7 +549,8 @@ export default function DashboardPage(): React.ReactElement {
|
||||
const navigate = useNavigate()
|
||||
const toast = useToast()
|
||||
const { t, locale } = useTranslation()
|
||||
const { demoMode } = useAuthStore()
|
||||
const { demoMode, user } = useAuthStore()
|
||||
const isAdmin = user?.role === 'admin'
|
||||
const { settings, updateSetting } = useSettingsStore()
|
||||
const dm = settings.dark_mode
|
||||
const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
@@ -781,7 +792,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
{!isLoading && spotlight && viewMode === 'grid' && (
|
||||
<SpotlightCard
|
||||
trip={spotlight}
|
||||
t={t} locale={locale} dark={dark}
|
||||
t={t} locale={locale} dark={dark} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onDelete={handleDelete}
|
||||
onArchive={handleArchive}
|
||||
@@ -797,7 +808,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
<TripCard
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
t={t} locale={locale}
|
||||
t={t} locale={locale} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onDelete={handleDelete}
|
||||
onArchive={handleArchive}
|
||||
@@ -811,7 +822,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
<TripListItem
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
t={t} locale={locale}
|
||||
t={t} locale={locale} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onDelete={handleDelete}
|
||||
onArchive={handleArchive}
|
||||
@@ -841,7 +852,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
<ArchivedRow
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
t={t} locale={locale}
|
||||
t={t} locale={locale} isAdmin={isAdmin}
|
||||
onEdit={tr => { setEditingTrip(tr); setShowForm(true) }}
|
||||
onUnarchive={handleUnarchive}
|
||||
onDelete={handleDelete}
|
||||
|
||||
@@ -26,6 +26,7 @@ interface AuthState {
|
||||
serverTimezone: string
|
||||
/** Server policy: all users must enable MFA */
|
||||
appRequireMfa: boolean
|
||||
tripRemindersEnabled: boolean
|
||||
|
||||
login: (email: string, password: string) => Promise<LoginResult>
|
||||
completeMfaLogin: (mfaToken: string, code: string) => Promise<AuthResponse>
|
||||
@@ -42,6 +43,7 @@ interface AuthState {
|
||||
setHasMapsKey: (val: boolean) => void
|
||||
setServerTimezone: (tz: string) => void
|
||||
setAppRequireMfa: (val: boolean) => void
|
||||
setTripRemindersEnabled: (val: boolean) => void
|
||||
demoLogin: () => Promise<AuthResponse>
|
||||
}
|
||||
|
||||
@@ -55,6 +57,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
hasMapsKey: false,
|
||||
serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
appRequireMfa: false,
|
||||
tripRemindersEnabled: false,
|
||||
|
||||
login: async (email: string, password: string) => {
|
||||
set({ isLoading: true, error: null })
|
||||
@@ -224,6 +227,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
setHasMapsKey: (val: boolean) => set({ hasMapsKey: val }),
|
||||
setServerTimezone: (tz: string) => set({ serverTimezone: tz }),
|
||||
setAppRequireMfa: (val: boolean) => set({ appRequireMfa: val }),
|
||||
setTripRemindersEnabled: (val: boolean) => set({ tripRemindersEnabled: val }),
|
||||
|
||||
demoLogin: async () => {
|
||||
set({ isLoading: true, error: null })
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface Trip {
|
||||
end_date: string
|
||||
cover_url: string | null
|
||||
is_archived: boolean
|
||||
reminder_days: number
|
||||
owner_id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
||||
Reference in New Issue
Block a user