diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 48e6889d..b5916819 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -272,6 +272,8 @@ export const adminApi = { checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data), getBagTracking: () => apiClient.get('/admin/bag-tracking').then(r => r.data), updateBagTracking: (enabled: boolean) => apiClient.put('/admin/bag-tracking', { enabled }).then(r => r.data), + getCollabFeatures: () => apiClient.get('/admin/collab-features').then(r => r.data), + updateCollabFeatures: (features: Record) => apiClient.put('/admin/collab-features', features).then(r => r.data), packingTemplates: () => apiClient.get('/admin/packing-templates').then(r => r.data), getPackingTemplate: (id: number) => apiClient.get(`/admin/packing-templates/${id}`).then(r => r.data), createPackingTemplate: (data: { name: string }) => apiClient.post('/admin/packing-templates', data).then(r => r.data), diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index 8a564381..c2db2218 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -4,12 +4,33 @@ import { useTranslation } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import { useAddonStore } from '../../store/addonStore' import { useToast } from '../shared/Toast' -import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen } from 'lucide-react' +import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, MessageCircle, StickyNote, BarChart3, Sparkles, Luggage } from 'lucide-react' const ICON_MAP = { ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, } +function ImmichIcon({ size = 14 }: { size?: number }) { + return ( + + + + ) +} + +function SynologyIcon({ size = 14 }: { size?: number }) { + return ( + + + + ) +} + +const PROVIDER_ICONS: Record> = { + immich: ImmichIcon, + synologyphotos: SynologyIcon, +} + interface Addon { id: string name: string @@ -38,7 +59,16 @@ function AddonIcon({ name, size = 20 }: AddonIconProps) { return } -export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }: { bagTrackingEnabled?: boolean; onToggleBagTracking?: () => void }) { +interface CollabFeatures { chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean } + +const COLLAB_SUB_FEATURES = [ + { key: 'chat', icon: MessageCircle, titleKey: 'admin.collab.chat.title', subtitleKey: 'admin.collab.chat.subtitle' }, + { key: 'notes', icon: StickyNote, titleKey: 'admin.collab.notes.title', subtitleKey: 'admin.collab.notes.subtitle' }, + { key: 'polls', icon: BarChart3, titleKey: 'admin.collab.polls.title', subtitleKey: 'admin.collab.polls.subtitle' }, + { key: 'whatsnext', icon: Sparkles, titleKey: 'admin.collab.whatsnext.title', subtitleKey: 'admin.collab.whatsnext.subtitle' }, +] as const + +export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, collabFeatures, onToggleCollabFeature }: { bagTrackingEnabled?: boolean; onToggleBagTracking?: () => void; collabFeatures?: CollabFeatures; onToggleCollabFeature?: (key: string) => void }) { const { t } = useTranslation() const dm = useSettingsStore(s => s.settings.dark_mode) const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) @@ -156,6 +186,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking } {addon.id === 'packing' && addon.enabled && onToggleBagTracking && (
+
{t('admin.bagTracking.title')}
{t('admin.bagTracking.subtitle')}
@@ -173,6 +204,36 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
)} + {addon.id === 'collab' && addon.enabled && collabFeatures && onToggleCollabFeature && ( +
+
+ {COLLAB_SUB_FEATURES.map(feat => { + const enabled = collabFeatures[feat.key] + const Icon = feat.icon + return ( +
+ +
+
{t(feat.titleKey)}
+
{t(feat.subtitleKey)}
+
+
+ + {enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} + + +
+
+ ) + })} +
+
+ )} ))} @@ -194,8 +255,11 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking } {addon.id === 'journey' && providerOptions.length > 0 && (
- {providerOptions.map(provider => ( + {providerOptions.map(provider => { + const ProviderIcon = PROVIDER_ICONS[provider.key] + return (
+ {ProviderIcon && }
{provider.label}
{provider.description}
@@ -214,7 +278,8 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
- ))} + ) + })}
)} diff --git a/client/src/components/Collab/CollabPanel.tsx b/client/src/components/Collab/CollabPanel.tsx index e67dd825..55582f82 100644 --- a/client/src/components/Collab/CollabPanel.tsx +++ b/client/src/components/Collab/CollabPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { useAuthStore } from '../../store/authStore' import { useTranslation } from '../../i18n' import { MessageCircle, StickyNote, BarChart3, Sparkles } from 'lucide-react' @@ -29,54 +29,142 @@ interface TripMember { avatar_url?: string | null } +interface CollabFeatures { + chat: boolean + notes: boolean + polls: boolean + whatsnext: boolean +} + interface CollabPanelProps { tripId: number tripMembers?: TripMember[] + collabFeatures?: CollabFeatures } -export default function CollabPanel({ tripId, tripMembers = [] }: CollabPanelProps) { +const ALL_TABS = [ + { id: 'chat', featureKey: 'chat' as const, labelKey: 'collab.tabs.chat', fallback: 'Chat', icon: MessageCircle }, + { id: 'notes', featureKey: 'notes' as const, labelKey: 'collab.tabs.notes', fallback: 'Notes', icon: StickyNote }, + { id: 'polls', featureKey: 'polls' as const, labelKey: 'collab.tabs.polls', fallback: 'Polls', icon: BarChart3 }, + { id: 'next', featureKey: 'whatsnext' as const, labelKey: 'collab.whatsNext.title', fallback: "What's Next", icon: Sparkles }, +] + +export default function CollabPanel({ tripId, tripMembers = [], collabFeatures }: CollabPanelProps) { const { user } = useAuthStore() const { t } = useTranslation() - const [mobileTab, setMobileTab] = useState('chat') const isDesktop = useIsDesktop() - const tabs = [ - { id: 'chat', label: t('collab.tabs.chat') || 'Chat', icon: MessageCircle }, - { id: 'notes', label: t('collab.tabs.notes') || 'Notes', icon: StickyNote }, - { id: 'polls', label: t('collab.tabs.polls') || 'Polls', icon: BarChart3 }, - { id: 'next', label: t('collab.whatsNext.title') || "What's Next", icon: Sparkles }, - ] + const features = collabFeatures || { chat: true, notes: true, polls: true, whatsnext: true } + + const tabs = useMemo(() => + ALL_TABS.filter(tab => features[tab.featureKey]).map(tab => ({ + ...tab, + label: t(tab.labelKey) || tab.fallback, + })), + [features, t]) + + const [mobileTab, setMobileTab] = useState(() => tabs[0]?.id || 'chat') + + // If active tab gets disabled, switch to first available + useEffect(() => { + if (tabs.length > 0 && !tabs.some(t => t.id === mobileTab)) { + setMobileTab(tabs[0].id) + } + }, [tabs, mobileTab]) + + const chatOn = features.chat + const rightPanels = [ + features.notes && 'notes', + features.polls && 'polls', + features.whatsnext && 'whatsnext', + ].filter(Boolean) as string[] + + if (tabs.length === 0) return null if (isDesktop) { + // Chat always 380px fixed when on. Right panels share remaining space. + // If chat off, all panels share full width equally. + if (chatOn && rightPanels.length === 0) { + // Only chat + return ( +
+
+ +
+
+ ) + } + + if (chatOn) { + // Chat left (380px) + right panels + return ( +
+
+ +
+
+ {rightPanels.length === 1 && ( +
+ {rightPanels[0] === 'notes' && } + {rightPanels[0] === 'polls' && } + {rightPanels[0] === 'whatsnext' && } +
+ )} + {rightPanels.length === 2 && rightPanels.map(p => ( +
+ {p === 'notes' && } + {p === 'polls' && } + {p === 'whatsnext' && } +
+ ))} + {rightPanels.length === 3 && ( + <> +
+ +
+
+
+ +
+
+ +
+
+ + )} +
+
+ ) + } + + // Chat off — remaining panels share full width + const panels = rightPanels + if (panels.length === 1) { + return ( +
+
+ {panels[0] === 'notes' && } + {panels[0] === 'polls' && } + {panels[0] === 'whatsnext' && } +
+
+ ) + } + return (
- {/* Chat — left, fixed width */} -
- -
- - {/* Right column: Notes top, Polls + What's Next bottom */} -
- {/* Notes — top */} -
- + {panels.map(p => ( +
+ {p === 'notes' && } + {p === 'polls' && } + {p === 'whatsnext' && }
- - {/* Polls + What's Next — bottom row */} -
-
- -
-
- -
-
-
+ ))}
) } - // Mobile: tab bar + single panel + // Mobile: tab bar + single panel (only enabled tabs) return (
{tabs.map(tab => { - const Icon = tab.icon const active = mobileTab === tab.id return (
- {mobileTab === 'chat' && } - {mobileTab === 'notes' && } - {mobileTab === 'polls' && } - {mobileTab === 'next' && } + {mobileTab === 'chat' && features.chat && } + {mobileTab === 'notes' && features.notes && } + {mobileTab === 'polls' && features.polls && } + {mobileTab === 'next' && features.whatsnext && }
) diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 82e28bfa..121f431f 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -584,6 +584,14 @@ const ar: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'تتبع الأمتعة', 'admin.bagTracking.subtitle': 'تفعيل الوزن وتعيين الأمتعة للعناصر', + 'admin.collab.chat.title': 'الدردشة', + 'admin.collab.chat.subtitle': 'المراسلة في الوقت الفعلي للتعاون', + 'admin.collab.notes.title': 'الملاحظات', + 'admin.collab.notes.subtitle': 'ملاحظات ومستندات مشتركة', + 'admin.collab.polls.title': 'الاستطلاعات', + 'admin.collab.polls.subtitle': 'استطلاعات وتصويت جماعي', + 'admin.collab.whatsnext.title': 'ما التالي', + 'admin.collab.whatsnext.subtitle': 'اقتراحات الأنشطة والخطوات التالية', 'admin.packingTemplates.title': 'قوالب التعبئة', 'admin.packingTemplates.subtitle': 'إنشاء قوائم تعبئة قابلة لإعادة الاستخدام', 'admin.packingTemplates.create': 'قالب جديد', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 7e914f25..6d5f2363 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -548,6 +548,14 @@ const br: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'Rastreamento de malas', 'admin.bagTracking.subtitle': 'Ativar peso e atribuição de mala para itens da lista', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Mensagens em tempo real para colaboração', + 'admin.collab.notes.title': 'Notas', + 'admin.collab.notes.subtitle': 'Notas e documentos compartilhados', + 'admin.collab.polls.title': 'Enquetes', + 'admin.collab.polls.subtitle': 'Enquetes e votações em grupo', + 'admin.collab.whatsnext.title': 'Próximos passos', + 'admin.collab.whatsnext.subtitle': 'Sugestões de atividades e próximos passos', 'admin.tabs.config': 'Personalização', 'admin.tabs.templates': 'Modelos de mala', 'admin.packingTemplates.title': 'Modelos de mala', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 97ac79ac..0b6bb15f 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -548,6 +548,14 @@ const cs: Record = { // Šablony balení (Packing Templates) 'admin.bagTracking.title': 'Sledování zavazadel', 'admin.bagTracking.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Zasílání zpráv v reálném čase', + 'admin.collab.notes.title': 'Poznámky', + 'admin.collab.notes.subtitle': 'Sdílené poznámky a dokumenty', + 'admin.collab.polls.title': 'Ankety', + 'admin.collab.polls.subtitle': 'Skupinové ankety a hlasování', + 'admin.collab.whatsnext.title': 'Co dál', + 'admin.collab.whatsnext.subtitle': 'Návrhy aktivit a další kroky', 'admin.tabs.config': 'Personalizace', 'admin.tabs.templates': 'Šablony seznamů', 'admin.packingTemplates.title': 'Šablony pro balení', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 84668eb0..3f7990be 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -552,6 +552,14 @@ const de: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'Gepäck-Tracking', 'admin.bagTracking.subtitle': 'Gewicht und Gepäckstück-Zuordnung für Packlisteneinträge aktivieren', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Echtzeit-Nachrichten für die Reiseplanung', + 'admin.collab.notes.title': 'Notizen', + 'admin.collab.notes.subtitle': 'Gemeinsame Notizen und Dokumente', + 'admin.collab.polls.title': 'Umfragen', + 'admin.collab.polls.subtitle': 'Gruppen-Umfragen und Abstimmungen', + 'admin.collab.whatsnext.title': 'Was kommt als Nächstes', + 'admin.collab.whatsnext.subtitle': 'Aktivitätsvorschläge und nächste Schritte', 'admin.tabs.config': 'Personalisierung', 'admin.tabs.templates': 'Packvorlagen', 'admin.packingTemplates.title': 'Packvorlagen', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 3dcae414..377b9d6a 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -608,6 +608,14 @@ const en: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'Bag Tracking', 'admin.bagTracking.subtitle': 'Enable weight and bag assignment for packing items', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Real-time messaging for trip collaboration', + 'admin.collab.notes.title': 'Notes', + 'admin.collab.notes.subtitle': 'Shared notes and documents', + 'admin.collab.polls.title': 'Polls', + 'admin.collab.polls.subtitle': 'Group polls and voting', + 'admin.collab.whatsnext.title': "What's Next", + 'admin.collab.whatsnext.subtitle': 'Activity suggestions and next steps', 'admin.tabs.config': 'Personalization', 'admin.tabs.templates': 'Packing Templates', 'admin.packingTemplates.title': 'Packing Templates', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index d3e193c6..12afa1c3 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -543,6 +543,14 @@ const es: Record = { 'admin.bagTracking.title': 'Seguimiento de equipaje', 'admin.bagTracking.subtitle': 'Activar peso y asignación de equipaje para artículos de la lista', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Mensajería en tiempo real para la colaboración', + 'admin.collab.notes.title': 'Notas', + 'admin.collab.notes.subtitle': 'Notas y documentos compartidos', + 'admin.collab.polls.title': 'Encuestas', + 'admin.collab.polls.subtitle': 'Encuestas y votaciones grupales', + 'admin.collab.whatsnext.title': 'Qué sigue', + 'admin.collab.whatsnext.subtitle': 'Sugerencias de actividades y próximos pasos', 'admin.tabs.config': 'Personalización', 'admin.tabs.templates': 'Plantillas de equipaje', 'admin.packingTemplates.title': 'Plantillas de equipaje', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index da78b5b2..bea2019d 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -547,6 +547,14 @@ const fr: Record = { 'admin.bagTracking.title': 'Suivi des bagages', 'admin.bagTracking.subtitle': 'Activer le poids et l\'attribution de bagages pour les articles', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Messagerie en temps réel pour la collaboration', + 'admin.collab.notes.title': 'Notes', + 'admin.collab.notes.subtitle': 'Notes et documents partagés', + 'admin.collab.polls.title': 'Sondages', + 'admin.collab.polls.subtitle': 'Sondages et votes de groupe', + 'admin.collab.whatsnext.title': 'Et ensuite', + 'admin.collab.whatsnext.subtitle': "Suggestions d'activités et prochaines étapes", 'admin.tabs.config': 'Personnalisation', 'admin.tabs.templates': 'Modèles de bagages', 'admin.packingTemplates.title': 'Modèles de bagages', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 4b03d7a7..df9930db 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -548,6 +548,14 @@ const hu: Record = { // Csomagolási sablonok és poggyászkövetés 'admin.bagTracking.title': 'Poggyászkövetés', 'admin.bagTracking.subtitle': 'Súly- és táskahozzárendelés engedélyezése csomagolási tételeknél', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Valós idejű üzenetküldés az együttműködéshez', + 'admin.collab.notes.title': 'Jegyzetek', + 'admin.collab.notes.subtitle': 'Megosztott jegyzetek és dokumentumok', + 'admin.collab.polls.title': 'Szavazások', + 'admin.collab.polls.subtitle': 'Csoportos szavazások', + 'admin.collab.whatsnext.title': 'Mi következik', + 'admin.collab.whatsnext.subtitle': 'Tevékenységjavaslatok és következő lépések', 'admin.tabs.config': 'Személyre szabás', 'admin.tabs.templates': 'Csomagolási sablonok', 'admin.packingTemplates.title': 'Csomagolási sablonok', diff --git a/client/src/i18n/translations/id.ts b/client/src/i18n/translations/id.ts index a6d4f08f..97ea0e6d 100644 --- a/client/src/i18n/translations/id.ts +++ b/client/src/i18n/translations/id.ts @@ -608,6 +608,14 @@ const id: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'Pelacak Tas', 'admin.bagTracking.subtitle': 'Aktifkan berat dan penugasan tas untuk item packing', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Pesan real-time untuk kolaborasi', + 'admin.collab.notes.title': 'Catatan', + 'admin.collab.notes.subtitle': 'Catatan dan dokumen bersama', + 'admin.collab.polls.title': 'Jajak Pendapat', + 'admin.collab.polls.subtitle': 'Jajak pendapat dan voting grup', + 'admin.collab.whatsnext.title': 'Selanjutnya', + 'admin.collab.whatsnext.subtitle': 'Saran aktivitas dan langkah selanjutnya', 'admin.tabs.config': 'Personalisasi', 'admin.tabs.templates': 'Template Packing', 'admin.packingTemplates.title': 'Template Packing', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index b1340b1a..fc0236d1 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -547,6 +547,14 @@ const it: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'Tracciamento valigia', 'admin.bagTracking.subtitle': 'Abilita il peso e l\'assegnazione della valigia per gli elementi della lista valigia', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Messaggistica in tempo reale per la collaborazione', + 'admin.collab.notes.title': 'Note', + 'admin.collab.notes.subtitle': 'Note e documenti condivisi', + 'admin.collab.polls.title': 'Sondaggi', + 'admin.collab.polls.subtitle': 'Sondaggi e votazioni di gruppo', + 'admin.collab.whatsnext.title': 'Prossimi passi', + 'admin.collab.whatsnext.subtitle': 'Suggerimenti attività e prossimi passi', 'admin.tabs.config': 'Personalizzazione', 'admin.tabs.templates': 'Modelli lista valigia', 'admin.packingTemplates.title': 'Modelli lista valigia', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index ca216397..5b601888 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -548,6 +548,14 @@ const nl: Record = { 'admin.bagTracking.title': 'Bagagetracking', 'admin.bagTracking.subtitle': 'Gewicht en bagagetoewijzing inschakelen voor paklijstitems', + 'admin.collab.chat.title': 'Chat', + 'admin.collab.chat.subtitle': 'Realtime berichten voor reissamenwerking', + 'admin.collab.notes.title': 'Notities', + 'admin.collab.notes.subtitle': 'Gedeelde notities en documenten', + 'admin.collab.polls.title': 'Peilingen', + 'admin.collab.polls.subtitle': 'Groepspeilingen en stemmen', + 'admin.collab.whatsnext.title': 'Wat nu', + 'admin.collab.whatsnext.subtitle': 'Activiteitssuggesties en volgende stappen', 'admin.tabs.config': 'Personalisatie', 'admin.tabs.templates': 'Paksjablonen', 'admin.packingTemplates.title': 'Paksjablonen', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 3f6e96ba..38a30885 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -520,6 +520,14 @@ const pl: Record = { // Packing Templates & Bag Tracking 'admin.bagTracking.title': 'Kontrola bagażu', 'admin.bagTracking.subtitle': 'Włącz wagę i przypisywanie do toreb dla przedmiotów do pakowania', + 'admin.collab.chat.title': 'Czat', + 'admin.collab.chat.subtitle': 'Wiadomości w czasie rzeczywistym', + 'admin.collab.notes.title': 'Notatki', + 'admin.collab.notes.subtitle': 'Wspólne notatki i dokumenty', + 'admin.collab.polls.title': 'Ankiety', + 'admin.collab.polls.subtitle': 'Ankiety grupowe i głosowania', + 'admin.collab.whatsnext.title': 'Co dalej', + 'admin.collab.whatsnext.subtitle': 'Sugestie aktywności i następne kroki', 'admin.tabs.config': 'Personalizacja', 'admin.tabs.templates': 'Szablony pakowania', 'admin.packingTemplates.title': 'Szablony pakowania', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 8786fc52..693081a3 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -548,6 +548,14 @@ const ru: Record = { 'admin.bagTracking.title': 'Отслеживание багажа', 'admin.bagTracking.subtitle': 'Включить вес и привязку к багажу для вещей', + 'admin.collab.chat.title': 'Чат', + 'admin.collab.chat.subtitle': 'Обмен сообщениями для совместной работы', + 'admin.collab.notes.title': 'Заметки', + 'admin.collab.notes.subtitle': 'Общие заметки и документы', + 'admin.collab.polls.title': 'Опросы', + 'admin.collab.polls.subtitle': 'Групповые опросы и голосования', + 'admin.collab.whatsnext.title': 'Что дальше', + 'admin.collab.whatsnext.subtitle': 'Предложения активностей и следующие шаги', 'admin.tabs.config': 'Персонализация', 'admin.tabs.templates': 'Шаблоны упаковки', 'admin.packingTemplates.title': 'Шаблоны упаковки', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 18012afe..2ec0d9f8 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -548,6 +548,14 @@ const zh: Record = { 'admin.bagTracking.title': '行李追踪', 'admin.bagTracking.subtitle': '为打包物品启用重量和行李分配', + 'admin.collab.chat.title': '聊天', + 'admin.collab.chat.subtitle': '实时消息协作', + 'admin.collab.notes.title': '笔记', + 'admin.collab.notes.subtitle': '共享笔记和文档', + 'admin.collab.polls.title': '投票', + 'admin.collab.polls.subtitle': '群组投票和表决', + 'admin.collab.whatsnext.title': '下一步', + 'admin.collab.whatsnext.subtitle': '活动建议和后续步骤', 'admin.tabs.config': '个性化', 'admin.tabs.templates': '打包模板', 'admin.packingTemplates.title': '打包模板', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index b54e23f6..708c1156 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -604,6 +604,14 @@ const zhTw: Record = { 'admin.bagTracking.title': '行李追蹤', 'admin.bagTracking.subtitle': '為打包物品啟用重量和行李分配', + 'admin.collab.chat.title': '聊天', + 'admin.collab.chat.subtitle': '即時訊息協作', + 'admin.collab.notes.title': '筆記', + 'admin.collab.notes.subtitle': '共享筆記和文件', + 'admin.collab.polls.title': '投票', + 'admin.collab.polls.subtitle': '群組投票和表決', + 'admin.collab.whatsnext.title': '下一步', + 'admin.collab.whatsnext.subtitle': '活動建議和後續步驟', 'admin.tabs.config': '配置', 'admin.tabs.templates': '打包模板', 'admin.packingTemplates.title': '打包模板', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 38a9728c..cf31f802 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -192,6 +192,10 @@ export default function AdminPage(): React.ReactElement { const [bagTrackingEnabled, setBagTrackingEnabled] = useState(false) useEffect(() => { adminApi.getBagTracking().then(d => setBagTrackingEnabled(d.enabled)).catch(() => {}) }, []) + // Collab features + const [collabFeatures, setCollabFeatures] = useState<{ chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean }>({ chat: true, notes: true, polls: true, whatsnext: true }) + useEffect(() => { adminApi.getCollabFeatures().then(d => setCollabFeatures(d)).catch(() => {}) }, []) + // OIDC config const [oidcConfig, setOidcConfig] = useState({ issuer: '', client_id: '', client_secret: '', client_secret_set: false, display_name: '', discovery_url: '' }) const [savingOidc, setSavingOidc] = useState(false) @@ -797,6 +801,10 @@ export default function AdminPage(): React.ReactElement { const next = !bagTrackingEnabled setBagTrackingEnabled(next) try { await adminApi.updateBagTracking(next) } catch { setBagTrackingEnabled(!next) } + }} collabFeatures={collabFeatures} onToggleCollabFeature={async (key: string) => { + const next = { ...collabFeatures, [key]: !collabFeatures[key] } + setCollabFeatures(next) + try { await adminApi.updateCollabFeatures({ [key]: next[key] }) } catch { setCollabFeatures(collabFeatures) } }} />
)} diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index 07be4f66..0f152392 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -100,6 +100,7 @@ export default function TripPlannerPage(): React.ReactElement | null { }, [undo, lastActionLabel, toast]) const [enabledAddons, setEnabledAddons] = useState>({ packing: true, budget: true, documents: true, collab: false }) + const [collabFeatures, setCollabFeatures] = useState<{ chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean }>({ chat: true, notes: true, polls: true, whatsnext: true }) const [tripAccommodations, setTripAccommodations] = useState([]) const [allowedFileTypes, setAllowedFileTypes] = useState(null) const [tripMembers, setTripMembers] = useState([]) @@ -116,6 +117,7 @@ export default function TripPlannerPage(): React.ReactElement | null { const map = {} data.addons.forEach(a => { map[a.id] = true }) setEnabledAddons({ packing: !!map.packing, budget: !!map.budget, documents: !!map.documents, collab: !!map.collab }) + if (data.collabFeatures) setCollabFeatures(data.collabFeatures) }).catch(() => {}) authApi.getAppConfig().then(config => { if (config.allowed_file_types) setAllowedFileTypes(config.allowed_file_types) @@ -956,7 +958,7 @@ export default function TripPlannerPage(): React.ReactElement | null { {activeTab === 'collab' && (
- +
)} diff --git a/server/src/app.ts b/server/src/app.ts index f4c91791..8f590b72 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -45,6 +45,7 @@ import publicConfigRoutes from './routes/publicConfig'; import { mcpHandler } from './mcp'; import { Addon } from './types'; import { getPhotoProviderConfig } from './services/memories/helpersService'; +import { getCollabFeatures } from './services/adminService'; export function createApp(): express.Application { const app = express(); @@ -236,6 +237,7 @@ export function createApp(): express.Application { } res.json({ + collabFeatures: getCollabFeatures(), addons: [ ...addons.map(a => ({ ...a, enabled: !!a.enabled })), ...providers.map(p => ({ diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 5184d4c6..67a4fa2e 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -200,6 +200,24 @@ router.put('/bag-tracking', (req: Request, res: Response) => { res.json(result); }); +// ── Collab Features ─────────────────────────────────────────────────────── + +router.get('/collab-features', (_req: Request, res: Response) => { + res.json(svc.getCollabFeatures()); +}); + +router.put('/collab-features', (req: Request, res: Response) => { + const result = svc.updateCollabFeatures(req.body); + const authReq = req as AuthRequest; + writeAudit({ + userId: authReq.user.id, + action: 'admin.collab_features', + ip: getClientIp(req), + details: result, + }); + res.json(result); +}); + // ── Packing Templates ────────────────────────────────────────────────────── router.get('/packing-templates', (_req: Request, res: Response) => { diff --git a/server/src/services/adminService.ts b/server/src/services/adminService.ts index eec43ecd..81674b54 100644 --- a/server/src/services/adminService.ts +++ b/server/src/services/adminService.ts @@ -459,6 +459,31 @@ export function updateBagTracking(enabled: boolean) { return { enabled: !!enabled }; } +// ── Collab Features ─────────────────────────────────────────────────────── + +const COLLAB_FEATURE_KEYS = ['collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled'] as const; + +export function getCollabFeatures() { + const rows = db.prepare("SELECT key, value FROM app_settings WHERE key IN ('collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled')").all() as { key: string; value: string }[]; + const map: Record = {}; + for (const r of rows) map[r.key] = r.value; + return { + chat: map['collab_chat_enabled'] !== 'false', + notes: map['collab_notes_enabled'] !== 'false', + polls: map['collab_polls_enabled'] !== 'false', + whatsnext: map['collab_whatsnext_enabled'] !== 'false', + }; +} + +export function updateCollabFeatures(features: { chat?: boolean; notes?: boolean; polls?: boolean; whatsnext?: boolean }) { + const mapping: Record = { chat: 'collab_chat_enabled', notes: 'collab_notes_enabled', polls: 'collab_polls_enabled', whatsnext: 'collab_whatsnext_enabled' }; + const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)"); + for (const [feat, key] of Object.entries(mapping)) { + if (features[feat] !== undefined) stmt.run(key, features[feat] ? 'true' : 'false'); + } + return getCollabFeatures(); +} + // ── Packing Templates ────────────────────────────────────────────────────── export function listPackingTemplates() {