diff --git a/public/logos/spitch-logo.svg b/public/logos/spitch-logo.svg new file mode 100644 index 0000000..ea0f59c --- /dev/null +++ b/public/logos/spitch-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 33fe9e1..c32997a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -74,10 +74,17 @@ api.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { - // Clear invalid token - localStorage.removeItem('auth_data'); - // Redirect to login - window.location.href = '/login'; + // Don't redirect to login if the request was to a login endpoint + const isLoginEndpoint = error.config?.url?.includes('/login') || + error.config?.url?.includes('/api/local/login') || + error.config?.url?.includes('/api/msft/login'); + + if (!isLoginEndpoint) { + // Clear invalid token + localStorage.removeItem('auth_data'); + // Redirect to login + window.location.href = '/login'; + } } return Promise.reject(error); } diff --git a/src/components/Connections/connectionsLogic.tsx b/src/components/Connections/connectionsLogic.tsx index 66d6288..aaecf24 100644 --- a/src/components/Connections/connectionsLogic.tsx +++ b/src/components/Connections/connectionsLogic.tsx @@ -94,7 +94,19 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { if (!value) return t('connections.not_available', 'N/A'); try { // Convert from seconds to milliseconds for Date constructor - return new Date(value * 1000).toLocaleString(); + const date = new Date(value * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; } catch { return t('connections.invalid_date', 'Invalid Date'); } @@ -109,7 +121,19 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { if (!value) return t('connections.not_available', 'N/A'); try { // Convert from seconds to milliseconds for Date constructor - return new Date(value * 1000).toLocaleString(); + const date = new Date(value * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; } catch { return t('connections.invalid_date', 'Invalid Date'); } @@ -174,7 +198,28 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { width: 150, sortable: true, filterable: false, - searchable: true + searchable: true, + formatter: (value: number) => { + if (!value) return t('connections.not_available', 'N/A'); + try { + // Convert from seconds to milliseconds for Date constructor + const date = new Date(value * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; + } catch { + return t('connections.invalid_date', 'Invalid Date'); + } + } }, { key: 'lastChecked', @@ -183,7 +228,28 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { width: 150, sortable: true, filterable: true, - searchable: true + searchable: true, + formatter: (value: number) => { + if (!value) return t('connections.not_available', 'N/A'); + try { + // Convert from seconds to milliseconds for Date constructor + const date = new Date(value * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; + } catch { + return t('connections.invalid_date', 'Invalid Date'); + } + } }, { key: 'expiresAt', @@ -191,7 +257,28 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { type: 'date', width: 150, sortable: true, - filterable: true + filterable: true, + formatter: (value: number) => { + if (!value) return t('connections.not_available', 'N/A'); + try { + // Convert from seconds to milliseconds for Date constructor + const date = new Date(value * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; + } catch { + return t('connections.invalid_date', 'Invalid Date'); + } + } } ]; diff --git a/src/components/Dateien/DateienTable.tsx b/src/components/Dateien/DateienTable.tsx index af00ffe..6815ef4 100644 --- a/src/components/Dateien/DateienTable.tsx +++ b/src/components/Dateien/DateienTable.tsx @@ -14,6 +14,7 @@ export function DateienTable({ className = '' }: DateienTableProps) { files, loading, error, + refetch, columns, actions, editModalOpen, @@ -57,6 +58,7 @@ export function DateienTable({ className = '' }: DateienTableProps) { onRowClick={undefined} onDelete={handleDelete} onDeleteMultiple={handleDeleteMultiple} + onRefresh={refetch} actions={actions} className={styles.dateienFormGenerator} /> diff --git a/src/components/Dateien/dateienLogic.tsx b/src/components/Dateien/dateienLogic.tsx index f050222..789b70e 100644 --- a/src/components/Dateien/dateienLogic.tsx +++ b/src/components/Dateien/dateienLogic.tsx @@ -139,7 +139,12 @@ export function useDateienLogic(): DateienLogicReturn { const hh = pad(date.getHours()); const mi = pad(date.getMinutes()); const ss = pad(date.getSeconds()); - return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`; + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss} ${timezone}`; } catch { return '-'; } diff --git a/src/components/FormGenerator/FormGenerator.module.css b/src/components/FormGenerator/FormGenerator.module.css index bb16f9f..2e138c5 100644 --- a/src/components/FormGenerator/FormGenerator.module.css +++ b/src/components/FormGenerator/FormGenerator.module.css @@ -92,6 +92,41 @@ flex-shrink: 0; } +.refreshButton { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 1px solid var(--color-primary); + border-radius: 50%; + background: var(--color-bg); + color: var(--color-text); + cursor: pointer; + transition: all 0.2s ease; + font-size: 16px; + font-family: var(--font-family); + flex-shrink: 0; +} + +.refreshButton:hover:not(:disabled) { + background: var(--color-secondary); + color: white; + border-color: var(--color-secondary); +} + +.refreshButton:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.refreshIcon { + font-size: 18px; + font-weight: bold; + transition: transform 0.2s ease; +} + .floatingLabelInput { position: relative; width: 250px; diff --git a/src/components/FormGenerator/FormGenerator.tsx b/src/components/FormGenerator/FormGenerator.tsx index a0b46d3..daf9d40 100644 --- a/src/components/FormGenerator/FormGenerator.tsx +++ b/src/components/FormGenerator/FormGenerator.tsx @@ -2,6 +2,8 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; import { useLanguage } from '../../contexts/LanguageContext'; import styles from './FormGenerator.module.css'; +import { IoIosRefresh } from "react-icons/io"; + // Types for the FormGenerator export interface ColumnConfig { key: string; @@ -41,6 +43,7 @@ export interface FormGeneratorProps { }[]; onDelete?: (row: T) => void; onDeleteMultiple?: (rows: T[]) => void; + onRefresh?: () => void; className?: string; } @@ -63,6 +66,7 @@ export function FormGenerator>({ actions = [], onDelete, onDeleteMultiple, + onRefresh, className = '' }: FormGeneratorProps) { const { t } = useLanguage(); @@ -370,7 +374,24 @@ export function FormGenerator>({ switch (column.type) { case 'date': - return new Date(value).toLocaleDateString(); + try { + const date = new Date(value); + if (isNaN(date.getTime())) return '-'; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; + } catch { + return '-'; + } case 'boolean': return value ? '✓' : '✗'; case 'number': @@ -431,6 +452,16 @@ export function FormGenerator>({ /> + {onRefresh && ( + + )} )} diff --git a/src/components/Mitglieder/MitgliederTable.tsx b/src/components/Mitglieder/MitgliederTable.tsx index 9b3d7fa..fde97f9 100644 --- a/src/components/Mitglieder/MitgliederTable.tsx +++ b/src/components/Mitglieder/MitgliederTable.tsx @@ -41,6 +41,7 @@ function MitgliederTable({ className = '' }: MitgliederTableProps) { selectable={false} loading={loading} actions={actions} + onRefresh={refetch} className={styles.mitgliederFormGenerator} /> diff --git a/src/components/PageManager/PageManager.tsx b/src/components/PageManager/PageManager.tsx index c47ea84..3107d82 100644 --- a/src/components/PageManager/PageManager.tsx +++ b/src/components/PageManager/PageManager.tsx @@ -28,10 +28,34 @@ const PageManager: React.FC = ({ loadingComponent: LoadingComp const currentPath = getCurrentPath(); + // Check if user has access to speech-related pages + const checkSpeechAccess = (path: string) => { + if (path.startsWith('speech/transcripts')) { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const timestamp = localStorage.getItem('speechSignUpTimestamp'); + + if (savedData && timestamp) { + const savedTime = parseInt(timestamp); + const now = Date.now(); + const twentyFourHours = 24 * 60 * 60 * 1000; + + // Check if data is still valid (within 24 hours) + return (now - savedTime) < twentyFourHours; + } + return false; + } catch (error) { + console.error('Error checking speech access:', error); + return false; + } + } + return true; // Allow access to non-speech pages + }; + useEffect(() => { const config = pageConfigs.find(c => c.path === currentPath); - if (!config || !config.moduleEnabled) { + if (!config || !config.moduleEnabled || !checkSpeechAccess(currentPath)) { return; } @@ -110,7 +134,7 @@ const PageManager: React.FC = ({ loadingComponent: LoadingComp const config = pageConfigs.find(c => c.path === currentPath); - if (!config || !config.moduleEnabled) { + if (!config || !config.moduleEnabled || !checkSpeechAccess(currentPath)) { return ; } diff --git a/src/components/PageManager/pageConfigInterface.ts b/src/components/PageManager/pageConfigInterface.ts index d602611..aeccaf7 100644 --- a/src/components/PageManager/pageConfigInterface.ts +++ b/src/components/PageManager/pageConfigInterface.ts @@ -22,6 +22,9 @@ export interface PageConfig { order?: number; // For sidebar ordering showInSidebar?: boolean; // Whether to show in sidebar (default: true) + // Subpages support + subpages?: PageConfig[]; + // Lifecycle hooks onActivate?: () => void | Promise; onDeactivate?: () => void | Promise; @@ -37,4 +40,5 @@ export interface SidebarItem { icon: IconType; moduleEnabled: boolean; order: number; + submenu?: SidebarSubmenuItemData[]; } diff --git a/src/components/PageManager/pageConfigs.ts b/src/components/PageManager/pageConfigs.ts index 37f1289..2a57368 100644 --- a/src/components/PageManager/pageConfigs.ts +++ b/src/components/PageManager/pageConfigs.ts @@ -1,4 +1,4 @@ -import { PageConfig } from './pageConfigInterface'; +import { PageConfig, SidebarItem } from './pageConfigInterface'; import { lazy } from 'react'; // Import icons for sidebar @@ -7,6 +7,8 @@ import { LuWorkflow, LuTicket } from "react-icons/lu"; import { GoGear } from "react-icons/go"; import { FaPlug, FaRegFileAlt } from "react-icons/fa"; import { LuMessageSquareText } from "react-icons/lu"; +import { IoIosDocument } from "react-icons/io"; +import Speech from '../../pages/Home/Speech'; // Lazy load components for better performance const Dashboard = lazy(() => import('../../pages/Home/Dashboard')); @@ -15,8 +17,8 @@ const TeamBereich = lazy(() => import('../../pages/Home/TeamBereich')); const Connections = lazy(() => import('../../pages/Home/Connections')); const Workflows = lazy(() => import('../../pages/Home/Workflows')); const Einstellungen = lazy(() => import('../../pages/Home/Einstellungen')); - const Prompts = lazy(() => import('../../pages/Home/Prompts')); +const SpeechTranscripts = lazy(() => import('../../pages/Home/SpeechTranscripts')); // Page configuration with caching and lifecycle settings export const pageConfigs: PageConfig[] = [ @@ -160,6 +162,38 @@ export const pageConfigs: PageConfig[] = [ onActivate: async () => { if (import.meta.env.DEV) console.log('Einstellungen activated'); } + }, + { + path: 'speech', + component: Speech, + persistent: false, + preload: true, + moduleEnabled: true, + // Sidebar properties + id: '8', + name: 'Speech', + icon: FaRegFileAlt, + order: 7, + showInSidebar: true, + onActivate: async () => { + if (import.meta.env.DEV) console.log('Speech activated'); + } + }, + { + path: 'speech/transcripts', + component: SpeechTranscripts, + persistent: false, + preload: false, + moduleEnabled: true, + // Sidebar properties + id: '8-1', + name: 'Transkriptverwaltung', + icon: IoIosDocument, + order: 8, + showInSidebar: false, // Will be shown as subpage under Speech + onActivate: async () => { + if (import.meta.env.DEV) console.log('Speech Transcripts activated'); + } } ]; @@ -197,17 +231,104 @@ export const getPageConfig = (path: string): PageConfig | undefined => { // Get sidebar items from page configs export const getSidebarItems = () => { - return pageConfigs + const items: SidebarItem[] = []; + + pageConfigs .filter(config => config.showInSidebar !== false) .sort((a, b) => (a.order || 0) - (b.order || 0)) - .map(config => ({ - id: config.id, - name: config.name, - link: `/${config.path}`, - icon: config.icon, - moduleEnabled: config.moduleEnabled ?? true, - order: config.order || 0 - })); + .forEach(config => { + // Check if this is the Speech item and if user has signed up + if (config.path === 'speech') { + const hasSignedUp = checkSpeechSignUpStatus(); + + if (hasSignedUp) { + // Find the transcript subpage + const transcriptConfig = pageConfigs.find(c => c.path === 'speech/transcripts'); + + if (transcriptConfig) { + // Create expandable Speech item with submenu + items.push({ + id: config.id, + name: config.name, + link: `/${config.path}`, + icon: config.icon, + moduleEnabled: config.moduleEnabled ?? true, + order: config.order || 0, + submenu: [ + { + id: transcriptConfig.id, + name: transcriptConfig.name, + link: `/${transcriptConfig.path}` + } + ] + }); + } else { + // Fallback to regular item if transcript config not found + items.push({ + id: config.id, + name: config.name, + link: `/${config.path}`, + icon: config.icon, + moduleEnabled: config.moduleEnabled ?? true, + order: config.order || 0 + }); + } + } else { + // User hasn't signed up, show regular non-expandable item + items.push({ + id: config.id, + name: config.name, + link: `/${config.path}`, + icon: config.icon, + moduleEnabled: config.moduleEnabled ?? true, + order: config.order || 0 + }); + } + } else { + // Regular non-Speech items + items.push({ + id: config.id, + name: config.name, + link: `/${config.path}`, + icon: config.icon, + moduleEnabled: config.moduleEnabled ?? true, + order: config.order || 0 + }); + } + }); + + return items; +}; + +// Helper function to check speech sign-up status +const checkSpeechSignUpStatus = (): boolean => { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const timestamp = localStorage.getItem('speechSignUpTimestamp'); + + console.log('🔍 Checking speech sign-up status:', { savedData: !!savedData, timestamp }); + + if (savedData && timestamp) { + const signUpTime = parseInt(timestamp); + const now = Date.now(); + const hoursDiff = (now - signUpTime) / (1000 * 60 * 60); + + console.log('📊 Speech sign-up validation:', { + signUpTime, + now, + hoursDiff, + isValid: hoursDiff < 24 + }); + + // Data is valid for 24 hours + return hoursDiff < 24; + } + console.log('❌ No speech sign-up data found'); + return false; + } catch (error) { + console.error('Error checking speech sign-up status:', error); + return false; + } }; export default pageConfigs; diff --git a/src/components/PageManager/pages.module.css b/src/components/PageManager/pages.module.css index a370a27..1f67d77 100644 --- a/src/components/PageManager/pages.module.css +++ b/src/components/PageManager/pages.module.css @@ -23,6 +23,7 @@ background: var(--color-bg); gap: 20px; min-height: 100%; + overflow: hidden; /* Prevent card from expanding beyond viewport */ } /* Page headers with consistent spacing */ @@ -45,11 +46,24 @@ } +.pageSubtitle { + font-size: 1.3rem; + font-weight: 600; + color: var(--color-secondary); + margin: 0; + font-family: var(--font-family); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + letter-spacing: 0.5px; + display: flex; + align-items: center; + gap: 0.5rem; +} + /* Common button styles */ .primaryButton { border-radius: 30px; background: var(--color-secondary); - color: var(--color-bg); + color: white; border: none; outline: none; padding: 10px 20px; @@ -109,7 +123,8 @@ flex-direction: column; gap: 10px; flex: 1; - + overflow-y: auto; + max-height: calc(100vh - 200px); /* Account for header and padding */ } .scrollableContent { diff --git a/src/components/Prompts/PromptsTable.tsx b/src/components/Prompts/PromptsTable.tsx index 70d4395..2d318e9 100644 --- a/src/components/Prompts/PromptsTable.tsx +++ b/src/components/Prompts/PromptsTable.tsx @@ -76,6 +76,7 @@ function PromptsTable({ className = '' }: PromptsTableProps) { actions={actions} onDelete={handleDeleteSingle} onDeleteMultiple={handleDeleteMultiple} + onRefresh={refetch} className={styles.promptsFormGenerator} /> diff --git a/src/components/Sidebar/SidebarItem.tsx b/src/components/Sidebar/SidebarItem.tsx index a95b165..592c0cf 100644 --- a/src/components/Sidebar/SidebarItem.tsx +++ b/src/components/Sidebar/SidebarItem.tsx @@ -23,10 +23,9 @@ const SidebarItem: React.FC = ({ e.preventDefault(); return; } - if (hasSubItems) { - e.preventDefault(); - onToggle(); - } + e.preventDefault(); + e.stopPropagation(); + onToggle(); }; const handleLinkClick = (e: React.MouseEvent) => { @@ -34,40 +33,42 @@ const SidebarItem: React.FC = ({ e.preventDefault(); return; } + // Allow normal navigation for the main link }; return (
  • - {/* Icon is always present */} + {/* Icon - always visible */} {Icon && } - {/* Text content - always present but hidden when minimized */} - {hasSubItems ? ( - - - {item.name} - - - - ) : ( - - - {item.name} - - + {/* Text and arrow - hidden when minimized */} + {!isMinimized && ( + <> + + + {item.name} + + + + {/* Arrow button separate from link */} + {hasSubItems && ( + + )} + )} diff --git a/src/components/Sidebar/SidebarStyles/SidebarItem.module.css b/src/components/Sidebar/SidebarStyles/SidebarItem.module.css index 81aca46..51cbf3e 100644 --- a/src/components/Sidebar/SidebarStyles/SidebarItem.module.css +++ b/src/components/Sidebar/SidebarStyles/SidebarItem.module.css @@ -17,50 +17,72 @@ color: var(--color-text); list-style: none; position: relative; - + gap: 8px; } .menu li:hover, .menu li.active { background: var(--color-secondary); - color: var(--color-bg); + color: white; border-top-right-radius: 25px; border-bottom-right-radius: 25px; } -.menu li:hover a, .menu li.active a { - color: var(--color-bg); +.menu li:hover a , .menu li.active a { + color: white; text-decoration: none; } -.menu li a { + +.menuTextLink { + flex: 1; text-decoration: none; + color: inherit; + display: flex; + align-items: center; + padding: 0; + margin: 0; + background: none; + border: none; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + gap: 5px; font-family: var(--font-family); font-size: 0.9rem; font-style: normal; font-weight: 500; line-height: normal; - color: inherit; - - display: flex; - align-items: center; - justify-content: left; - position: absolute; - - height: 100%; - opacity: 1; - - white-space: nowrap; - overflow: hidden; - transition: opacity 0.3s ease-in-out; } -.menuLink { +.arrowButton { + background: none; + border: none; + padding: 4px; + margin-right: 8px; + cursor: pointer; display: flex; align-items: center; - justify-content: space-between; - width: 100%; - padding-right: 15px; - padding-left: 35px; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s ease; + flex-shrink: 0; + width: 24px; + height: 24px; + color:var(--color-text); +} + +.menu li:hover .arrowButton { + color: white; +} + + + + +.arrowButton:disabled { + cursor: not-allowed; + opacity: 0.6; } .icon { @@ -71,17 +93,13 @@ justify-content: center; align-items: center; flex-shrink: 0; - flex-grow: 0; /* Prevent growth during transitions */ - position: absolute; - top: 50%; - transform: translateY(-50%); /* Center vertically */ + flex-grow: 0; } .hassubmenu { width: 20px; height: 20px; opacity: 1; - margin-left: auto; /* Push to right side, replacing gap spacing */ transition: opacity 0.3s ease-in-out; } @@ -90,10 +108,7 @@ } .menuText { - white-space: nowrap; - overflow: hidden; opacity: 1; - margin-left: 5px; /* Replace gap with margin to prevent layout shifts */ transition: opacity 0.3s ease-in-out; } @@ -109,8 +124,8 @@ /* Minimized Menu Styles */ .menu.minimized li { width: 46px; - padding: 0 3px 0 11px; - justify-content: flex-start; + padding: 0; + justify-content: center; position: relative; } .menu.minimized li a{ diff --git a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css index 17b783f..af64e69 100644 --- a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css +++ b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css @@ -1,54 +1,65 @@ .submenu { position: relative; - background-color: var(--color-bg); + background: var(--color-primary); + border: none; + border-top-right-radius: 25px; + border-bottom-right-radius: 25px; + z-index: 1000; + margin: 0; overflow: hidden; + width: 220px; } -.open { - opacity: 1; - visibility: visible; - height: auto; -} .submenuLineContainer { display: flex; align-items: stretch; gap: 0; - padding: 5px 0; + padding: 0; + margin: 0; } - - .submenuList { - margin: 5px 0; + margin: 0; flex-grow: 1; + list-style: none; + padding: 0; } .submenuList li { - width: 153px; - height: 20px; - color: var(--color-text); - margin: 4px 0; + width: 100%; + height: 44px; + color: #181818; + margin: 0; position: relative; overflow: hidden; } .submenuList li a { - padding: 4px 0; - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: normal; - color: var(--color-text); - text-decoration: none; - display: block; - overflow: hidden; + width: 100%; + height: 100%; + padding: 0 3px 0 27px; + background: none; + border: none; + text-align: left; + cursor: pointer; font-family: var(--font-family); + font-size: 0.9rem; + color: #181818; + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; + transition: background-color 0.2s ease; +} + +.submenuList li a:hover { + background-color: var(--color-hover, rgba(0, 0, 0, 0.05)); } .textContainer { position: relative; - width: 153px; + width: 100%; overflow: hidden; white-space: nowrap; } diff --git a/src/components/Speech/SpeechConfirmation.module.css b/src/components/Speech/SpeechConfirmation.module.css new file mode 100644 index 0000000..fbdcd84 --- /dev/null +++ b/src/components/Speech/SpeechConfirmation.module.css @@ -0,0 +1,331 @@ +.container { + max-width: 100%; + margin: 0; + padding: 1.5rem; + text-align: center; + height: calc(100vh - 120px); + overflow-y: auto; +} + +.content { + background: var(--color-bg); + padding: 2rem; + border-radius: 25px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; +} + +.iconContainer { + margin-bottom: 2rem; +} + +.successIcon { + font-size: 4rem; + color: var(--color-secondary); +} + +.title { + font-size: 1.8rem; + font-weight: bold; + margin: 0 0 1rem 0; + color: var(--color-text); +} + +.message { + font-size: 1rem; + color: var(--color-text); + line-height: 1.6; + margin-bottom: 2rem; +} + +.nextSteps { + text-align: left; + margin-bottom: 2rem; +} + +.nextSteps h2 { + font-size: 1.2rem; + font-weight: bold; + margin: 0 0 1.5rem 0; + color: var(--color-secondary); + text-align: center; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--color-secondary); +} + +.step { + display: flex; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.5rem; + padding: 1.25rem; + background: var(--color-bg); + border-radius: 20px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.stepIcon { + flex-shrink: 0; + width: 40px; + height: 40px; + background: var(--color-secondary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.stepIconInner { + color: white; + font-size: 1.2rem; +} + +.stepContent h3 { + font-size: 1rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + color: var(--color-text); +} + +.stepContent p { + color: var(--color-text); + margin: 0; + line-height: 1.5; +} + +.contactInfo { + background: var(--color-bg); + padding: 1.5rem; + border-radius: 20px; + border: 1px solid var(--color-secondary); + text-align: left; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.contactInfo h3 { + font-size: 1.1rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + color: var(--color-secondary); + text-align: center; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--color-secondary); +} + +.contactInfo p { + color: var(--color-text); + margin: 0 0 1rem 0; + line-height: 1.5; +} + +.contactMethods { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.contactMethod { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--color-text); +} + +.contactIcon { + color: var(--color-secondary); + font-size: 1rem; +} + +.submittedData { + background: var(--color-bg); + padding: 1.5rem; + border-radius: 20px; + border: 1px solid var(--color-secondary); + margin-bottom: 2rem; + text-align: left; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.submittedData h3 { + font-size: 1.1rem; + font-weight: bold; + margin: 0 0 1rem 0; + color: var(--color-secondary); + text-align: center; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--color-secondary); +} + +.dataGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; +} + +.dataItem { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.dataItem strong { + color: var(--color-text); + font-size: 0.9rem; + font-weight: 600; +} + +.dataItem span { + color: var(--color-text); + font-size: 0.9rem; + word-break: break-word; +} + +.actions { + display: flex; + justify-content: center; + gap: 1rem; + flex-wrap: wrap; +} + +.backButton { + padding: 8px 16px; + border: 1px solid var(--color-primary); + background-color: var(--color-bg); + color: var(--color-text); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.backButton:hover { + background-color: var(--color-primary-hover); + border-color: var(--color-primary); + color: #181818; +} + +.resetButton { + padding: 8px 16px; + border: none; + background-color: var(--color-secondary); + color: var(--color-bg); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.resetButton:hover { + background-color: var(--color-secondary-hover); +} + +.resetIcon { + font-size: 1rem; +} + +.additionalActions { + display: flex; + justify-content: center; + gap: 1rem; + flex-wrap: wrap; + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--color-secondary); +} + +.transcriptButton { + padding: 8px 16px; + border: none; + background-color: var(--color-secondary); + color: var(--color-bg); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.transcriptButton:hover { + background-color: var(--color-secondary-hover); +} + +.settingsButton { + padding: 8px 16px; + border: 1px solid var(--color-primary); + background-color: var(--color-bg); + color: var(--color-text); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.settingsButton:hover { + background-color: var(--color-primary-hover); + border-color: var(--color-primary); + color: #181818; +} + +@media (max-width: 640px) { + .container { + padding: 1rem; + height: calc(100vh - 100px); + } + + .content { + padding: 1.5rem 1rem; + } + + .title { + font-size: 1.5rem; + } + + .step { + flex-direction: column; + text-align: center; + padding: 1rem; + } + + .stepIcon { + align-self: center; + } + + .contactMethods { + align-items: center; + } + + .dataGrid { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .actions { + flex-direction: column-reverse; + } + + .backButton, + .resetButton { + width: 100%; + padding: 12px; + } + + .additionalActions { + flex-direction: column; + align-items: center; + } + + .transcriptButton, + .settingsButton { + width: 100%; + max-width: 200px; + padding: 12px; + } +} diff --git a/src/components/Speech/SpeechConfirmation.tsx b/src/components/Speech/SpeechConfirmation.tsx new file mode 100644 index 0000000..20ecc94 --- /dev/null +++ b/src/components/Speech/SpeechConfirmation.tsx @@ -0,0 +1,170 @@ +import { IoIosCheckmarkCircle, IoIosMail, IoIosCall, IoIosTime, IoIosRefresh } from 'react-icons/io'; +import { useNavigate } from 'react-router-dom'; +import sharedStyles from '../PageManager/pages.module.css'; +import styles from './SpeechConfirmation.module.css'; +import { useLanguage } from '../../contexts/LanguageContext'; + +interface MandateData { + id: string; + mandate_general: { + company_name: string; + industry: string; + contact_info: { + email: string; + phone: string; + street: string; + postal_code: string; + city: string; + country: string; + }; + business_hours: string; + timezone: string; + }; + setup_contacts: boolean; +} + +interface SpeechConfirmationProps { + onBackToInfo: () => void; + submittedData?: MandateData | null; + onReset?: () => void; +} + +function SpeechConfirmation({ onBackToInfo, submittedData, onReset }: SpeechConfirmationProps) { + const { t } = useLanguage(); + const navigate = useNavigate(); + return ( +
    +
    +
    + +
    + +

    {t('speech.confirmation.title')}

    + +

    + {t('speech.confirmation.message')} +

    + + {submittedData && ( +
    +

    {t('speech.confirmation.submitted_data')}

    +
    +
    + {t('speech.confirmation.company')}: + {submittedData.mandate_general.company_name} +
    +
    + {t('speech.confirmation.industry')}: + {submittedData.mandate_general.industry} +
    +
    + {t('speech.confirmation.email')}: + {submittedData.mandate_general.contact_info.email} +
    +
    + {t('speech.confirmation.phone')}: + {submittedData.mandate_general.contact_info.phone} +
    +
    + {t('speech.confirmation.address')}: + + {submittedData.mandate_general.contact_info.street}, {submittedData.mandate_general.contact_info.postal_code} {submittedData.mandate_general.contact_info.city}, {submittedData.mandate_general.contact_info.country} + +
    +
    + {t('speech.confirmation.timezone')}: + {submittedData.mandate_general.timezone} +
    +
    +
    + )} + +
    +

    {t('speech.confirmation.next_steps')}

    + +
    +
    + +
    +
    +

    {t('speech.confirmation.email_confirmation')}

    +

    {t('speech.confirmation.email_confirmation_desc')}

    +
    +
    + +
    +
    + +
    +
    +

    {t('speech.confirmation.review_process')}

    +

    {t('speech.confirmation.review_process_desc')}

    +
    +
    + +
    +
    + +
    +
    +

    {t('speech.confirmation.setup_call')}

    +

    {t('speech.confirmation.setup_call_desc')}

    +
    +
    +
    + +
    +

    {t('speech.confirmation.questions')}

    +

    + {t('speech.confirmation.questions_desc')} +

    +
    +
    + + support@spitch.ai +
    +
    + + +1 (555) 123-4567 +
    +
    +
    +
    + +
    + + {onReset && ( + + )} +
    + +
    + + +
    +
    + ); +} + +export default SpeechConfirmation; diff --git a/src/components/Speech/SpeechInfo.module.css b/src/components/Speech/SpeechInfo.module.css new file mode 100644 index 0000000..e710a5e --- /dev/null +++ b/src/components/Speech/SpeechInfo.module.css @@ -0,0 +1,283 @@ +.container { + margin: 0 auto; + text-align: center; + width: 100%; + margin: 0; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + + +.content { + flex: 1; + width: 100%; + margin: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 0; +} + +.features { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1rem; + flex: 1; + min-height: 0; +} + +.feature { + padding: 1rem; + border-radius: 20px; + background: var(--color-bg); + border: 1px solid var(--color-secondary); + transition: all 0.3s ease; + color: var(--color-text); + position: relative; + overflow: hidden; + min-height: 160px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.feature::before { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 60px; + height: 60px; + background: linear-gradient(135deg, var(--color-secondary), transparent 50%); + opacity: 0.1; + border-radius: 50% 0 0 0; +} + +.feature:hover { + transform: translateY(-8px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15); + background: var(--color-secondary); + color: white; +} + +.feature:hover::before { + opacity: 0.2; +} + + +.featureIconContainer { + width: 50px; + height: 50px; + border-radius: 50%; + background: var(--feature-color, #ff4757); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.75rem; + transition: all 0.3s ease; +} + +.feature:hover .featureIconContainer { + background: white; + transform: scale(1.1); +} + +.featureAcronym { + font-size: 1.2rem; + font-weight: bold; + color: white; + letter-spacing: 1px; +} + +.feature:hover .featureAcronym { + color: var(--feature-color, #ff4757); +} + +.featureTitle { + font-size: 1rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + color: var(--color-text); + transition: color 0.3s ease; +} + +.feature:hover .featureTitle { + color: white; +} + +.featureDescription { + color: var(--color-text-secondary); + margin: 0; + line-height: 1.3; + font-size: 0.8rem; + transition: color 0.3s ease; + flex-grow: 1; + display: flex; + align-items: center; +} + +.feature:hover .featureDescription { + color: white; +} + +.partnerInfo { + background: var(--color-bg); + padding: 1rem; + border-radius: 20px; + border: 1px solid var(--color-secondary); + text-align: left; + margin-bottom: 0.5rem; + flex-shrink: 0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.partnerInfo h2 { + font-size: 1.1rem; + font-weight: bold; + color: var(--color-secondary); + margin: 0 0 0.75rem 0; + text-align: center; +} + +.introText { + color: var(--color-text); + line-height: 1.4; + margin-bottom: 1rem; + font-size: 0.85rem; + text-align: center; + font-style: italic; +} + +.featureSection { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.featureItem { + background: var(--color-secondary); + padding: 0.75rem; + border-radius: 10px; + color: white; +} + +.featureItem h3 { + font-size: 0.8rem; + font-weight: bold; + margin: 0 0 0.4rem 0; + color: white; +} + +.featureItem p { + font-size: 0.7rem; + line-height: 1.3; + margin: 0; + color: rgba(255, 255, 255, 0.9); +} + +.partnerLink { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.linkIcon { + color: var(--color-secondary); +} + +.partnerLink a { + color: var(--color-secondary); + text-decoration: none; + font-weight: 500; + transition: color 0.2s ease; +} + +.partnerLink a:hover { + color: var(--primary-hover, #0056b3); + text-decoration: underline; +} + + +@media (max-width: 1024px) { + .features { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + + .feature { + min-height: 140px; + padding: 0.75rem; + } +} + +@media (max-width: 768px) { + .container { + padding: 0.5rem; + } + + + .features { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .feature { + padding: 0.75rem; + min-height: 120px; + } + + .featureIconContainer { + width: 40px; + height: 40px; + } + + .featureAcronym { + font-size: 1rem; + } + + .featureTitle { + font-size: 0.9rem; + } + + .featureDescription { + font-size: 0.75rem; + } + + .partnerInfo { + padding: 0.75rem; + margin-bottom: 0.5rem; + } + + .partnerInfo h2 { + font-size: 1rem; + } + + .introText { + font-size: 0.8rem; + margin-bottom: 0.75rem; + } + + .featureSection { + grid-template-columns: 1fr; + gap: 0.5rem; + margin-bottom: 0.75rem; + } + + .featureItem { + padding: 0.5rem; + } + + .featureItem h3 { + font-size: 0.75rem; + } + + .featureItem p { + font-size: 0.65rem; + } + +} diff --git a/src/components/Speech/SpeechInfo.tsx b/src/components/Speech/SpeechInfo.tsx new file mode 100644 index 0000000..41ff633 --- /dev/null +++ b/src/components/Speech/SpeechInfo.tsx @@ -0,0 +1,112 @@ +import { IoIosLink, IoIosCall, IoIosAnalytics, IoIosFingerPrint, IoIosBook, IoIosChatbubbles, IoIosDesktop } from 'react-icons/io'; +import styles from './SpeechInfo.module.css'; +import { useLanguage } from '../../contexts/LanguageContext'; + +function SpeechInfo() { + const { t } = useLanguage(); + + const features = [ + { + key: 'va', + icon: IoIosCall, + title: t('speech.info.va'), + description: t('speech.info.va_description'), + color: 'var(--color-secondary)' + }, + { + key: 'sa', + icon: IoIosAnalytics, + title: t('speech.info.sa'), + description: t('speech.info.sa_description'), + color: 'var(--color-secondary)' + }, + { + key: 'vb', + icon: IoIosFingerPrint, + title: t('speech.info.vb'), + description: t('speech.info.vb_description'), + color: 'var(--color-secondary)' + }, + { + key: 'ka', + icon: IoIosBook, + title: t('speech.info.ka'), + description: t('speech.info.ka_description'), + color: 'var(--color-secondary)' + }, + { + key: 'cp', + icon: IoIosChatbubbles, + title: t('speech.info.cp'), + description: t('speech.info.cp_description'), + color: 'var(--color-secondary)' + }, + { + key: 'aa', + icon: IoIosDesktop, + title: t('speech.info.aa'), + description: t('speech.info.aa_description'), + color: 'var(--color-secondary)' + } + ]; + + return ( +
    +
    +
    + {features.map((feature) => ( +
    +
    + {feature.key.toUpperCase()} +
    +

    {feature.title}

    +

    {feature.description}

    +
    + ))} +
    + +
    +

    {t('speech.info.about')}

    +

    + {t('speech.info.about_intro')} +

    + +
    +
    +

    🎯 {t('speech.info.workflow_title')}

    +

    {t('speech.info.workflow_description')}

    +
    + +
    +

    🤖 {t('speech.info.ai_title')}

    +

    {t('speech.info.ai_description')}

    +
    + +
    +

    🔄 {t('speech.info.sync_title')}

    +

    {t('speech.info.sync_description')}

    +
    + +
    +

    💰 {t('speech.info.cost_title')}

    +

    {t('speech.info.cost_description')}

    +
    +
    + + +
    +
    +
    + ); +} + +export default SpeechInfo; diff --git a/src/components/Speech/SpeechSettings.module.css b/src/components/Speech/SpeechSettings.module.css new file mode 100644 index 0000000..5b07f15 --- /dev/null +++ b/src/components/Speech/SpeechSettings.module.css @@ -0,0 +1,225 @@ +.container { + width: 100%; + max-width: 100%; + margin: 0; + padding: 0; +} + +.header { + margin-bottom: 2rem; +} + +.title { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text); + margin: 0 0 0.5rem 0; +} + +.description { + font-size: 1rem; + color: var(--color-text-secondary); + margin: 0; + line-height: 1.5; +} + +.loading { + text-align: center; + padding: 2rem; + color: var(--color-text); + font-size: 1rem; +} + +.noData { + text-align: center; + padding: 2rem; + background: var(--color-bg); + border-radius: 20px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.noData p { + margin: 0 0 1.5rem 0; + color: var(--color-text); + font-size: 1rem; +} + +.message { + padding: 1rem; + border-radius: 8px; + margin-bottom: 1.5rem; + font-size: 0.9rem; + font-weight: 500; +} + +.successMessage { + background-color: rgba(34, 197, 94, 0.1); + color: #16a34a; + border: 1px solid rgba(34, 197, 94, 0.2); +} + +.errorMessage { + background-color: rgba(239, 68, 68, 0.1); + color: #dc2626; + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.form { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.section { + background: var(--color-bg); + border-radius: 20px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + padding: 1.5rem; +} + +.sectionHeader { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid var(--color-secondary); +} + +.sectionIcon { + font-size: 1.25rem; + color: var(--color-secondary); +} + +.sectionTitle { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text); + margin: 0; +} + +.formRow { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.formRow:last-child { + margin-bottom: 0; +} + +.formField { + display: flex; + flex-direction: column; +} + +.inputContainer { + position: relative; + width: 100%; +} + +.formInput, +.formSelect { + width: 100%; + padding: 1rem 0.75rem 0.5rem 0.75rem; + border: 2px solid var(--color-border); + border-radius: 8px; + font-size: 1rem; + font-family: var(--font-family); + background: var(--color-bg); + color: var(--color-text); + transition: all 0.2s ease; + outline: none; +} + +.formInput:focus, +.formSelect:focus { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.formInput:invalid:not(:focus) { + border-color: #ef4444; +} + +.formSelect { + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; +} + +.floatingLabel { + position: absolute; + left: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: var(--color-bg); + padding: 0 0.25rem; + color: var(--color-text-secondary); + font-size: 1rem; + font-family: var(--font-family); + pointer-events: none; + transition: all 0.2s ease; + z-index: 1; +} + +.floatingLabelActive { + top: 0.25rem; + transform: translateY(0); + font-size: 0.75rem; + color: var(--color-secondary); + font-weight: 500; +} + +.actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + align-items: center; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} + +.resetIcon { + margin-right: 0.5rem; + font-size: 1rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .formRow { + grid-template-columns: 1fr; + gap: 1rem; + } + + .actions { + flex-direction: column; + align-items: stretch; + } + + .actions button { + width: 100%; + } +} + +@media (max-width: 640px) { + .section { + padding: 1rem; + } + + .sectionHeader { + margin-bottom: 1rem; + padding-bottom: 0.5rem; + } + + .formRow { + gap: 0.75rem; + margin-bottom: 1rem; + } +} diff --git a/src/components/Speech/SpeechSettings.tsx b/src/components/Speech/SpeechSettings.tsx new file mode 100644 index 0000000..3946c51 --- /dev/null +++ b/src/components/Speech/SpeechSettings.tsx @@ -0,0 +1,482 @@ +import React, { useState, useEffect } from 'react'; +import { IoIosBusiness, IoIosContact, IoIosTime, IoIosRefresh } from 'react-icons/io'; +import sharedStyles from '../PageManager/pages.module.css'; +import styles from './SpeechSettings.module.css'; +import { useLanguage } from '../../contexts/LanguageContext'; + +interface MandateData { + id: string; + mandate_general: { + company_name: string; + industry: string; + contact_info: { + email: string; + phone: string; + street: string; + postal_code: string; + city: string; + country: string; + }; + business_hours: string; + timezone: string; + }; + setup_contacts: boolean; +} + +interface SpeechSettingsProps { + onDataUpdate?: (data: MandateData) => void; +} + +function SpeechSettings({ onDataUpdate }: SpeechSettingsProps) { + const { t } = useLanguage(); + const [formData, setFormData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); + const [focusedFields, setFocusedFields] = useState>(new Set()); + + // Load data from localStorage on component mount + useEffect(() => { + const loadSpeechData = () => { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const timestamp = localStorage.getItem('speechSignUpTimestamp'); + + if (savedData && timestamp) { + const parsedData = JSON.parse(savedData); + const savedTime = parseInt(timestamp); + const now = Date.now(); + const twentyFourHours = 24 * 60 * 60 * 1000; + + // Check if data is still valid (within 24 hours) + if (now - savedTime < twentyFourHours) { + setFormData(parsedData); + } else { + // Data expired, clear it + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + } + } + } catch (error) { + console.error('Error loading speech data:', error); + } finally { + setIsLoading(false); + } + }; + + loadSpeechData(); + }, []); + + const handleInputChange = (field: string, value: string) => { + if (!formData) return; + + const newData = { ...formData }; + const fieldParts = field.split('.'); + + if (fieldParts.length === 2) { + // Handle nested fields like mandate_general.company_name + const [parent, child] = fieldParts; + if (parent === 'mandate_general' && child in newData.mandate_general) { + (newData.mandate_general as any)[child] = value; + } + } else if (fieldParts.length === 3) { + // Handle deeply nested fields like mandate_general.contact_info.email + const [parent, child, grandchild] = fieldParts; + if (parent === 'mandate_general' && child === 'contact_info' && grandchild in newData.mandate_general.contact_info) { + (newData.mandate_general.contact_info as any)[grandchild] = value; + } + } else if (field === 'setup_contacts') { + newData.setup_contacts = value === 'true'; + } + + setFormData(newData); + setSaveMessage(null); + }; + + const handleFocus = (field: string) => { + setFocusedFields(prev => new Set(prev).add(field)); + }; + + const handleBlur = (field: string) => { + setFocusedFields(prev => { + const newSet = new Set(prev); + newSet.delete(field); + return newSet; + }); + }; + + const handleSave = async () => { + if (!formData) return; + + setIsSaving(true); + try { + // Save to localStorage + localStorage.setItem('speechSignUpData', JSON.stringify(formData)); + localStorage.setItem('speechSignUpTimestamp', Date.now().toString()); + + // Dispatch event to notify other components + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + + setSaveMessage({ type: 'success', text: t('speech.settings.save_success') }); + + // Notify parent component if callback provided + if (onDataUpdate) { + onDataUpdate(formData); + } + + // Clear message after 3 seconds + setTimeout(() => setSaveMessage(null), 3000); + } catch (error) { + console.error('Error saving speech settings:', error); + setSaveMessage({ type: 'error', text: t('speech.settings.save_error') }); + } finally { + setIsSaving(false); + } + }; + + const handleReset = () => { + if (window.confirm(t('speech.settings.reset_confirm'))) { + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + setFormData(null); + setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') }); + setTimeout(() => setSaveMessage(null), 3000); + } + }; + + if (isLoading) { + return ( +
    +
    + {t('common.loading')} +
    +
    + ); + } + + if (!formData) { + return ( +
    +
    +

    {t('speech.settings.no_data')}

    + +
    +
    + ); + } + + return ( +
    +
    +

    {t('speech.settings.title')}

    +

    {t('speech.settings.description')}

    +
    + + {saveMessage && ( +
    + {saveMessage.text} +
    + )} + +
    + {/* Company Information Section */} +
    +
    + +

    {t('speech.settings.company_info')}

    +
    + +
    +
    +
    + handleInputChange('mandate_general.company_name', e.target.value)} + onFocus={() => handleFocus('company_name')} + onBlur={() => handleBlur('company_name')} + required + /> + +
    +
    + +
    +
    + handleInputChange('mandate_general.industry', e.target.value)} + onFocus={() => handleFocus('industry')} + onBlur={() => handleBlur('industry')} + required + /> + +
    +
    +
    +
    + + {/* Contact Information Section */} +
    +
    + +

    {t('speech.settings.contact_info')}

    +
    + +
    +
    +
    + handleInputChange('mandate_general.contact_info.email', e.target.value)} + onFocus={() => handleFocus('email')} + onBlur={() => handleBlur('email')} + required + /> + +
    +
    + +
    +
    + handleInputChange('mandate_general.contact_info.phone', e.target.value)} + onFocus={() => handleFocus('phone')} + onBlur={() => handleBlur('phone')} + required + /> + +
    +
    +
    + +
    +
    +
    + handleInputChange('mandate_general.contact_info.street', e.target.value)} + onFocus={() => handleFocus('street')} + onBlur={() => handleBlur('street')} + required + /> + +
    +
    + +
    +
    + handleInputChange('mandate_general.contact_info.postal_code', e.target.value)} + onFocus={() => handleFocus('postal_code')} + onBlur={() => handleBlur('postal_code')} + required + /> + +
    +
    +
    + +
    +
    +
    + handleInputChange('mandate_general.contact_info.city', e.target.value)} + onFocus={() => handleFocus('city')} + onBlur={() => handleBlur('city')} + required + /> + +
    +
    + +
    +
    + handleInputChange('mandate_general.contact_info.country', e.target.value)} + onFocus={() => handleFocus('country')} + onBlur={() => handleBlur('country')} + required + /> + +
    +
    +
    +
    + + {/* Business Hours Section */} +
    +
    + +

    {t('speech.settings.business_hours')}

    +
    + +
    +
    +
    + handleInputChange('mandate_general.business_hours', e.target.value)} + onFocus={() => handleFocus('business_hours')} + onBlur={() => handleBlur('business_hours')} + required + /> + +
    +
    + +
    +
    + + +
    +
    +
    +
    + + {/* Actions */} +
    + + + +
    +
    +
    + ); +} + +export default SpeechSettings; diff --git a/src/components/Speech/SpeechSignUp.module.css b/src/components/Speech/SpeechSignUp.module.css new file mode 100644 index 0000000..62e2216 --- /dev/null +++ b/src/components/Speech/SpeechSignUp.module.css @@ -0,0 +1,384 @@ +.container { + max-width: 100%; + margin: 0; + height: calc(100vh - 120px); + overflow-y: auto; +} + +.header { + margin-bottom: 1.5rem; + position: relative; +} + +.backButton { + display: flex; + align-items: center; + gap: 0.5rem; + background: none; + border: none; + color: var(--color-text); + cursor: pointer; + font-size: 1rem; + padding: 0.5rem 0; + transition: color 0.2s ease; + margin-bottom: 1rem; +} + +.backButton:hover { + color: var(--primary-color, #007bff); +} + +.backIcon { + font-size: 1.2rem; +} + +.title { + font-size: 2rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + color: var(--color-text); +} + +.subtitle { + color: var(--color-text); + margin: 0; + line-height: 1.5; +} + +.form { + display: flex; + flex-direction: column; + gap: 1.25rem; + height: 100%; +} + +.section { + background: var(--color-bg); + border: 1px solid var(--color-secondary); + border-radius: 25px; + padding: 1.25rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.sectionTitle { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 1.2rem; + font-weight: bold; + color: var(--color-secondary); + margin: 0 0 1rem 0; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--color-secondary); +} + +.sectionIcon { + font-size: 1.3rem; +} + +.formGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.inputGroupFull { + grid-column: 1 / -1; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 20px; +} + +.label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + color: var(--color-text); + font-size: 14px; + margin-bottom: 6px; +} + +.labelIcon { + color: var(--color-primary); + font-size: 1rem; +} + +/* Floating label container */ +.floatingLabelInput { + position: relative; + margin-bottom: 20px; +} + +.input { + width: 100%; + padding: 12px 12px 8px 12px; + border: 1px solid var(--color-primary); + border-radius: 25px; + font-size: 14px; + transition: all 0.2s ease; + background-color: var(--color-bg); + box-sizing: border-box; + color: var(--color-text); +} + +.input:focus { + outline: none; + border-color: var(--color-secondary); +} + +.inputError { + border-color: #ef4444; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +.inputError:focus { + border-color: #ef4444; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +/* Select styling to match input fields */ +select.input { + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 16px; + padding-right: 40px; + cursor: pointer; +} + +select.input:focus { + outline: none; + border-color: var(--color-secondary); +} + +select.input:hover { + border-color: var(--color-secondary); +} + +/* Floating label styles */ +.floatingLabel { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--color-primary); + opacity: 0.7; + font-size: 14px; + transition: all 0.2s ease; + pointer-events: none; + z-index: 1; +} + +.focusedLabel { + position: absolute; + left: 12px; + top: -8px; + transform: translateY(0); + color: var(--color-primary); + opacity: 1; + font-size: 12px; + font-weight: 500; + background-color: var(--color-bg); + padding: 0 4px; + transition: all 0.2s ease; + pointer-events: none; + z-index: 2; +} + +.activeFocusedLabel { + position: absolute; + left: 12px; + top: -8px; + transform: translateY(0); + color: var(--color-secondary); + opacity: 1; + font-size: 12px; + font-weight: 500; + background-color: var(--color-bg); + padding: 0 4px; + transition: all 0.2s ease; + pointer-events: none; + z-index: 2; +} + +.textarea { + width: 100%; + padding: 12px 12px 8px 12px; + border: 1px solid var(--color-primary); + border-radius: 25px; + font-size: 14px; + transition: all 0.2s ease; + background-color: var(--color-bg); + box-sizing: border-box; + color: var(--color-text); + font-family: inherit; + line-height: 1.5; + overflow-y: auto; + resize: vertical; + min-height: 4em; + max-height: 8em; +} + +.textarea:focus { + outline: none; + border-color: var(--color-secondary); +} + +.textarea.inputError { + border-color: #ef4444; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +.errorText { + color: #ef4444; + font-size: 12px; + margin-top: 4px; + display: block; +} + +.contactsSection { + text-align: center; + padding: 1rem 0; +} + +.contactsDescription { + color: var(--color-text); + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.contactsActions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.skipButton { + padding: 8px 16px; + border: 1px solid var(--color-primary); + background-color: var(--color-bg); + color: var(--color-text); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.skipButton:hover { + background-color: var(--color-primary-hover); + border-color: var(--color-primary); + color: #181818; +} + +.setupButton { + padding: 8px 16px; + border: none; + background-color: var(--color-secondary); + color: var(--color-bg); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.setupButton:hover { + background-color: var(--color-secondary-hover); +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--color-primary); +} + +.cancelButton { + padding: 8px 16px; + border: 1px solid var(--color-primary); + background-color: var(--color-bg); + color: var(--color-text); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.cancelButton:hover { + background-color: var(--color-primary-hover); + border-color: var(--color-primary); + color: #181818; +} + +.submitButton { + padding: 8px 16px; + border: none; + background-color: var(--color-secondary); + color: var(--color-bg); + border-radius: 25px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.submitButton:hover { + background-color: var(--color-secondary-hover); +} + +.submitButton:disabled { + background-color: #9ca3af; + cursor: not-allowed; +} + +@media (max-width: 640px) { + .container { + padding: 1rem; + height: calc(100vh - 100px); + } + + .formGrid { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .section { + padding: 1rem; + } + + .sectionTitle { + font-size: 1.1rem; + } + + .contactsActions { + flex-direction: column; + align-items: center; + } + + .skipButton, + .setupButton { + width: 100%; + max-width: 200px; + } + + .actions { + flex-direction: column-reverse; + } + + .cancelButton, + .submitButton { + width: 100%; + padding: 12px; + } +} diff --git a/src/components/Speech/SpeechSignUp.tsx b/src/components/Speech/SpeechSignUp.tsx new file mode 100644 index 0000000..4c23653 --- /dev/null +++ b/src/components/Speech/SpeechSignUp.tsx @@ -0,0 +1,342 @@ +import { useState } from 'react'; +import { IoIosArrowBack, IoIosBusiness, IoIosPeople } from 'react-icons/io'; +import sharedStyles from '../PageManager/pages.module.css'; +import styles from './SpeechSignUp.module.css'; +import { useLanguage } from '../../contexts/LanguageContext'; + +interface SpeechSignUpProps { + onBack: () => void; + onSubmit: (data: MandateData) => void; +} + +interface MandateData { + id: string; + mandate_general: { + company_name: string; + industry: string; + contact_info: { + email: string; + phone: string; + street: string; + postal_code: string; + city: string; + country: string; + }; + business_hours: string; + timezone: string; + }; + setup_contacts: boolean; +} + +function SpeechSignUp({ onBack, onSubmit }: SpeechSignUpProps) { + const { t } = useLanguage(); + + const [formData, setFormData] = useState({ + id: crypto.randomUUID(), + mandate_general: { + company_name: '', + industry: '', + contact_info: { + email: '', + phone: '', + street: '', + postal_code: '', + city: '', + country: '' + }, + business_hours: '9:00-17:00', + timezone: 'Europe/Zurich' + }, + setup_contacts: false + }); + + const [errors, setErrors] = useState>({}); + const [focusedFields, setFocusedFields] = useState>(new Set()); + + const handleInputChange = (field: string, value: string) => { + if (field.startsWith('contact_info.')) { + const contactField = field.split('.')[1] as keyof typeof formData.mandate_general.contact_info; + setFormData(prev => ({ + ...prev, + mandate_general: { + ...prev.mandate_general, + contact_info: { + ...prev.mandate_general.contact_info, + [contactField]: value + } + } + })); + } else if (field.startsWith('mandate_general.')) { + const generalField = field.split('.')[1] as keyof typeof formData.mandate_general; + setFormData(prev => ({ + ...prev, + mandate_general: { + ...prev.mandate_general, + [generalField]: value + } + })); + } else { + setFormData(prev => ({ ...prev, [field]: value })); + } + + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })); + } + }; + + const handleFocus = (field: string) => { + setFocusedFields(prev => new Set(prev).add(field)); + }; + + const handleBlur = (field: string) => { + setFocusedFields(prev => { + const newSet = new Set(prev); + newSet.delete(field); + return newSet; + }); + }; + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!formData.mandate_general.company_name.trim()) newErrors['mandate_general.company_name'] = t('speech.signup.company_required'); + if (!formData.mandate_general.industry.trim()) newErrors['mandate_general.industry'] = t('speech.signup.industry_required'); + if (!formData.mandate_general.contact_info.email.trim()) { + newErrors['contact_info.email'] = t('speech.signup.email_required'); + } else if (!/\S+@\S+\.\S+/.test(formData.mandate_general.contact_info.email)) { + newErrors['contact_info.email'] = t('speech.signup.email_invalid'); + } + if (!formData.mandate_general.contact_info.phone.trim()) newErrors['contact_info.phone'] = t('speech.signup.phone_required'); + if (!formData.mandate_general.contact_info.street.trim()) newErrors['contact_info.street'] = t('speech.signup.street_required'); + if (!formData.mandate_general.contact_info.postal_code.trim()) newErrors['contact_info.postal_code'] = t('speech.signup.postal_code_required'); + if (!formData.mandate_general.contact_info.city.trim()) newErrors['contact_info.city'] = t('speech.signup.city_required'); + if (!formData.mandate_general.contact_info.country.trim()) newErrors['contact_info.country'] = t('speech.signup.country_required'); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (validateForm()) { + // Save to localStorage for session persistence + try { + localStorage.setItem('speechSignUpData', JSON.stringify(formData)); + localStorage.setItem('speechSignUpTimestamp', Date.now().toString()); + console.log('Sign-up data saved to localStorage:', formData); + + // Dispatch custom event to refresh sidebar + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + } catch (error) { + console.error('Failed to save to localStorage:', error); + } + + onSubmit(formData); + } + }; + + const renderFloatingInput = ( + field: string, + value: string, + placeholder: string, + type: string = 'text', + icon?: React.ReactNode + ) => { + const isFocused = focusedFields.has(field); + const hasValue = value.trim() !== ''; + const hasError = errors[field]; + + return ( +
    + handleInputChange(field, e.target.value)} + onFocus={() => handleFocus(field)} + onBlur={() => handleBlur(field)} + className={`${styles.input} ${hasError ? styles.inputError : ''}`} + placeholder="" + /> + + {hasError && {hasError}} +
    + ); + }; + + return ( +
    +
    + +

    {t('speech.signup.title')}

    +

    + {t('speech.signup.subtitle')} +

    +
    + +
    +
    +

    + + {t('speech.signup.company_info')} +

    + +
    + {renderFloatingInput( + 'mandate_general.company_name', + formData.mandate_general.company_name, + `${t('speech.signup.company_name')} *` + )} + + {renderFloatingInput( + 'mandate_general.industry', + formData.mandate_general.industry, + `${t('speech.signup.industry')} *` + )} + + {renderFloatingInput( + 'mandate_general.business_hours', + formData.mandate_general.business_hours, + `${t('speech.signup.business_hours')}`, + 'text', + + )} + +
    + + +
    +
    +
    + +
    +

    + + {t('speech.signup.contact_info')} +

    + +
    + {renderFloatingInput( + 'contact_info.email', + formData.mandate_general.contact_info.email, + `${t('speech.signup.email')} *`, + 'email' + )} + + {renderFloatingInput( + 'contact_info.phone', + formData.mandate_general.contact_info.phone, + `${t('speech.signup.phone')} *`, + 'tel', + + )} + + {renderFloatingInput( + 'contact_info.street', + formData.mandate_general.contact_info.street, + `${t('speech.signup.street')} *` + )} + + {renderFloatingInput( + 'contact_info.postal_code', + formData.mandate_general.contact_info.postal_code, + `${t('speech.signup.postal_code')} *` + )} + + {renderFloatingInput( + 'contact_info.city', + formData.mandate_general.contact_info.city, + `${t('speech.signup.city')} *` + )} + + {renderFloatingInput( + 'contact_info.country', + formData.mandate_general.contact_info.country, + `${t('speech.signup.country')} *` + )} +
    +
    + +
    +

    + + {t('speech.signup.contacts_setup')} +

    +
    +

    + {t('speech.signup.contacts_description')} +

    +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + ); +} + +export default SpeechSignUp; diff --git a/src/components/Speech/index.ts b/src/components/Speech/index.ts new file mode 100644 index 0000000..c9d68ac --- /dev/null +++ b/src/components/Speech/index.ts @@ -0,0 +1,4 @@ +export { default as SpeechInfo } from './SpeechInfo'; +export { default as SpeechSignUp } from './SpeechSignUp'; +export { default as SpeechConfirmation } from './SpeechConfirmation'; +export { default as SpeechSettings } from './SpeechSettings'; diff --git a/src/components/Workflows/WorkflowsTable.tsx b/src/components/Workflows/WorkflowsTable.tsx index af4a34c..6cc55fb 100644 --- a/src/components/Workflows/WorkflowsTable.tsx +++ b/src/components/Workflows/WorkflowsTable.tsx @@ -38,6 +38,7 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) { actions={logic.actions} onDelete={logic.handleDeleteSingle} onDeleteMultiple={logic.handleDeleteMultiple} + onRefresh={logic.refetch} className={styles.workflowsFormGenerator} onRowClick={(workflow: any) => { // TODO: Navigate to workflow detail view diff --git a/src/components/Workflows/workflowsLogic.tsx b/src/components/Workflows/workflowsLogic.tsx index e798fd4..ae7bfb9 100644 --- a/src/components/Workflows/workflowsLogic.tsx +++ b/src/components/Workflows/workflowsLogic.tsx @@ -193,7 +193,18 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn { console.warn('Invalid startedAt date:', value); return '-'; } - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; } catch (error) { console.warn('Error parsing startedAt date:', value, error); return '-'; @@ -220,7 +231,18 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn { console.warn('Invalid lastActivity date:', value); return '-'; } - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const timezoneOffset = date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); + const offsetMinutes = Math.abs(timezoneOffset) % 60; + const offsetSign = timezoneOffset <= 0 ? '+' : '-'; + const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; } catch (error) { console.warn('Error parsing lastActivity date:', value, error); return '-'; diff --git a/src/components/settings/settingsSpeech.module.css b/src/components/settings/settingsSpeech.module.css new file mode 100644 index 0000000..8cd4dcf --- /dev/null +++ b/src/components/settings/settingsSpeech.module.css @@ -0,0 +1,290 @@ +/* Speech Settings Form - matches userInfoForm styling */ +.speechSettingsForm { + display: flex; + flex-direction: column; + gap: 20px; + padding: 20px; + background: var(--color-bg); + border-radius: 25px; + border: 1px solid var(--color-primary); + margin-bottom: 20px; +} + +.settingLabel { + font-size: 1rem; + font-weight: 500; + color: var(--color-text); + font-family: var(--font-family); +} + +.settingDescription { + font-size: 0.875rem; + color: var(--color-primary); + font-family: var(--font-family); +} + +.loading { + text-align: center; + padding: 2rem; + color: var(--color-text); + font-size: 1rem; +} + +.noData { + text-align: center; + padding: 2rem; + background: var(--color-bg); + border-radius: 20px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.noData p { + margin: 0 0 1.5rem 0; + color: var(--color-text); + font-size: 1rem; +} + +.signUpButton { + padding: 12px 24px; + border-radius: 25px; + border: none; + background: var(--color-secondary); + color: white; + cursor: pointer; + transition: all 0.3s ease; + font-family: var(--font-family); + font-size: 0.875rem; + font-weight: 500; + min-width: 120px; +} + +.signUpButton:hover:not(:disabled) { + background: var(--color-secondary); + border-color: var(--color-secondary); + box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3); + transform: translateY(-2px); +} + +.updateMessage { + padding: 12px 16px; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 10px; +} + +.successMessage { + background-color: rgba(34, 197, 94, 0.1); + color: #16a34a; + border: 1px solid rgba(34, 197, 94, 0.2); +} + +.errorMessage { + background-color: rgba(239, 68, 68, 0.1); + color: #dc2626; + border: 1px solid rgba(239, 68, 68, 0.2); +} + +/* Section styling */ +.section { + display: flex; + flex-direction: column; + gap: 15px; + padding: 15px; + background: var(--color-bg); + border-radius: 15px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.sectionHeader { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-secondary); +} + +.sectionIcon { + font-size: 1.1rem; + color: var(--color-secondary); +} + +.sectionTitle { + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + margin: 0; + font-family: var(--font-family); +} + +/* Form styling - matches userInfoForm */ +.formRow { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.formField { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-width: 250px; +} + +.fieldLabel { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text); + font-family: var(--font-family); +} + +.formInput, +.formSelect { + padding: 12px 16px; + border-radius: 25px; + border: 1px solid var(--color-primary); + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-family); + font-size: 0.875rem; + transition: all 0.3s ease; + outline: none; +} + +.formInput:focus, +.formSelect:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.1); +} + +.formInput:hover, +.formSelect:hover { + border-color: var(--color-secondary); +} + +.formSelect { + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; +} + +.formSelect option { + background: var(--color-bg); + color: var(--color-text); + padding: 10px; +} + +/* Actions styling - matches userInfoForm */ +.formActions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 10px; +} + +.saveButton { + padding: 12px 24px; + border-radius: 25px; + border: none; + background: var(--color-secondary); + color: white; + cursor: pointer; + transition: all 0.3s ease; + font-family: var(--font-family); + font-size: 0.875rem; + font-weight: 500; + min-width: 120px; +} + +.saveButton:hover:not(:disabled) { + background: var(--color-secondary); + border-color: var(--color-secondary); + box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3); + transform: translateY(-2px); +} + +.saveButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.resetButton { + padding: 12px 24px; + border-radius: 25px; + border: 1px solid var(--color-primary); + background: var(--color-bg); + color: var(--color-text); + cursor: pointer; + transition: all 0.3s ease; + font-family: var(--font-family); + font-size: 0.875rem; + font-weight: 500; + min-width: 120px; + display: flex; + align-items: center; + gap: 8px; +} + +.resetButton:hover:not(:disabled) { + background: var(--color-primary); + color: white; + transform: translateY(-2px); +} + +.resetButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.resetIcon { + font-size: 1rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .formRow { + flex-direction: column; + gap: 15px; + } + + .formField { + min-width: unset; + } + + .formActions { + justify-content: center; + flex-direction: column; + } + + .saveButton, + .resetButton { + width: 100%; + } +} + +@media (max-width: 640px) { + .speechSettingsForm { + padding: 15px; + } + + .section { + padding: 12px; + } + + .sectionHeader { + gap: 6px; + padding-bottom: 6px; + } + + .formRow { + gap: 12px; + } +} diff --git a/src/components/settings/settingsSpeech.tsx b/src/components/settings/settingsSpeech.tsx new file mode 100644 index 0000000..e2a965e --- /dev/null +++ b/src/components/settings/settingsSpeech.tsx @@ -0,0 +1,419 @@ +import React, { useState, useEffect } from 'react'; +import { IoIosBusiness, IoIosContact, IoIosTime, IoIosRefresh } from 'react-icons/io'; +import { useNavigate } from 'react-router-dom'; +import styles from './settingsSpeech.module.css'; +import { useLanguage } from '../../contexts/LanguageContext'; + +interface MandateData { + id: string; + mandate_general: { + company_name: string; + industry: string; + contact_info: { + email: string; + phone: string; + street: string; + postal_code: string; + city: string; + country: string; + }; + business_hours: string; + timezone: string; + }; + setup_contacts: boolean; +} + +interface SettingsSpeechProps { + onDataUpdate?: (data: MandateData) => void; +} + +function SettingsSpeech({ onDataUpdate }: SettingsSpeechProps) { + const { t } = useLanguage(); + const navigate = useNavigate(); + const [formData, setFormData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); + const [focusedFields, setFocusedFields] = useState>(new Set()); + + // Load data from localStorage on component mount + useEffect(() => { + const loadSpeechData = () => { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const timestamp = localStorage.getItem('speechSignUpTimestamp'); + + if (savedData && timestamp) { + const parsedData = JSON.parse(savedData); + const savedTime = parseInt(timestamp); + const now = Date.now(); + const twentyFourHours = 24 * 60 * 60 * 1000; + + // Check if data is still valid (within 24 hours) + if (now - savedTime < twentyFourHours) { + setFormData(parsedData); + } else { + // Data expired, clear it + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + } + } + } catch (error) { + console.error('Error loading speech data:', error); + } finally { + setIsLoading(false); + } + }; + + loadSpeechData(); + }, []); + + const handleInputChange = (field: string, value: string) => { + if (!formData) return; + + const newData = { ...formData }; + const fieldParts = field.split('.'); + + if (fieldParts.length === 2) { + // Handle nested fields like mandate_general.company_name + const [parent, child] = fieldParts; + if (parent === 'mandate_general' && child in newData.mandate_general) { + (newData.mandate_general as any)[child] = value; + } + } else if (fieldParts.length === 3) { + // Handle deeply nested fields like mandate_general.contact_info.email + const [parent, child, grandchild] = fieldParts; + if (parent === 'mandate_general' && child === 'contact_info' && grandchild in newData.mandate_general.contact_info) { + (newData.mandate_general.contact_info as any)[grandchild] = value; + } + } else if (field === 'setup_contacts') { + newData.setup_contacts = value === 'true'; + } + + setFormData(newData); + setSaveMessage(null); + }; + + const handleFocus = (field: string) => { + setFocusedFields(prev => new Set(prev).add(field)); + }; + + const handleBlur = (field: string) => { + setFocusedFields(prev => { + const newSet = new Set(prev); + newSet.delete(field); + return newSet; + }); + }; + + const handleSave = async () => { + if (!formData) return; + + setIsSaving(true); + try { + // Save to localStorage + localStorage.setItem('speechSignUpData', JSON.stringify(formData)); + localStorage.setItem('speechSignUpTimestamp', Date.now().toString()); + + // Dispatch event to notify other components + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + + setSaveMessage({ type: 'success', text: t('speech.settings.save_success') }); + + // Notify parent component if callback provided + if (onDataUpdate) { + onDataUpdate(formData); + } + + // Clear message after 3 seconds + setTimeout(() => setSaveMessage(null), 3000); + } catch (error) { + console.error('Error saving speech settings:', error); + setSaveMessage({ type: 'error', text: t('speech.settings.save_error') }); + } finally { + setIsSaving(false); + } + }; + + const handleReset = () => { + if (window.confirm(t('speech.settings.reset_confirm'))) { + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + setFormData(null); + setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') }); + setTimeout(() => setSaveMessage(null), 3000); + } + }; + + if (isLoading) { + return ( +
    +
    + {t('common.loading')} +
    +
    + ); + } + + if (!formData) { + return ( +
    +
    +

    {t('speech.settings.no_data')}

    + +
    +
    + ); + } + + return ( +
    + {t('speech.settings.title')} + {t('speech.settings.description')} + + {saveMessage && ( +
    + {saveMessage.text} +
    + )} + + {/* Company Information Section */} +
    +
    + +

    {t('speech.settings.company_info')}

    +
    + +
    +
    + + handleInputChange('mandate_general.company_name', e.target.value)} + onFocus={() => handleFocus('company_name')} + onBlur={() => handleBlur('company_name')} + required + /> +
    + +
    + + handleInputChange('mandate_general.industry', e.target.value)} + onFocus={() => handleFocus('industry')} + onBlur={() => handleBlur('industry')} + required + /> +
    +
    +
    + + {/* Contact Information Section */} +
    +
    + +

    {t('speech.settings.contact_info')}

    +
    + +
    +
    + + handleInputChange('mandate_general.contact_info.email', e.target.value)} + onFocus={() => handleFocus('email')} + onBlur={() => handleBlur('email')} + required + /> +
    + +
    + + handleInputChange('mandate_general.contact_info.phone', e.target.value)} + onFocus={() => handleFocus('phone')} + onBlur={() => handleBlur('phone')} + required + /> +
    +
    + +
    +
    + + handleInputChange('mandate_general.contact_info.street', e.target.value)} + onFocus={() => handleFocus('street')} + onBlur={() => handleBlur('street')} + required + /> +
    + +
    + + handleInputChange('mandate_general.contact_info.postal_code', e.target.value)} + onFocus={() => handleFocus('postal_code')} + onBlur={() => handleBlur('postal_code')} + required + /> +
    +
    + +
    +
    + + handleInputChange('mandate_general.contact_info.city', e.target.value)} + onFocus={() => handleFocus('city')} + onBlur={() => handleBlur('city')} + required + /> +
    + +
    + + handleInputChange('mandate_general.contact_info.country', e.target.value)} + onFocus={() => handleFocus('country')} + onBlur={() => handleBlur('country')} + required + /> +
    +
    +
    + + {/* Business Hours Section */} +
    +
    + +

    {t('speech.settings.business_hours')}

    +
    + +
    +
    + + handleInputChange('mandate_general.business_hours', e.target.value)} + onFocus={() => handleFocus('business_hours')} + onBlur={() => handleBlur('business_hours')} + required + /> +
    + +
    + + +
    +
    +
    + + {/* Actions */} +
    + + + +
    +
    + ); +} + +export default SettingsSpeech; diff --git a/src/hooks/useAuthentication.ts b/src/hooks/useAuthentication.ts index 1d5bf12..c89f64b 100644 --- a/src/hooks/useAuthentication.ts +++ b/src/hooks/useAuthentication.ts @@ -3,6 +3,7 @@ import { useState } from 'react'; import { useMsal } from '@azure/msal-react'; import api from '../api'; import { useApiRequest } from './useApi'; +import { getApiBaseUrl } from '../../config/config'; // Regular authentication interface LoginResponse { @@ -114,7 +115,7 @@ export function useMsalAuth() { try { return new Promise((resolve, reject) => { - const backendUrl = import.meta.env.VITE_API_BASE_URL; + const backendUrl = getApiBaseUrl(); const loginUrl = `${backendUrl}/api/msft/login?state=login`; console.log('🔐 Starting MSAL authentication...'); @@ -227,7 +228,7 @@ export function useMsalAuth() { setMsalError('Authentication timeout - please check your internet connection and try again'); reject(new Error('Authentication timeout')); } - }, 10000); + }, 60000); // Override popup.close to mark as manually closed const originalClose = popup.close; diff --git a/src/hooks/useConnections.ts b/src/hooks/useConnections.ts index d1f7b0f..8c13a20 100644 --- a/src/hooks/useConnections.ts +++ b/src/hooks/useConnections.ts @@ -1,5 +1,8 @@ import { useState } from 'react'; import { useApiRequest } from './useApi'; +import { getApiBaseUrl } from '../../config/config'; + + // Connection interfaces - exactly matching backend UserConnection model export interface Connection { @@ -194,7 +197,7 @@ export function useOAuthConnect() { // Convert relative URL to absolute URL if needed let authUrl = response.authUrl; if (authUrl.startsWith('/')) { - authUrl = `${import.meta.env.VITE_API_BASE_URL}${authUrl}`; + authUrl = `${getApiBaseUrl()}${authUrl}`; } // Open popup using the same pattern as useAuthentication.ts @@ -228,7 +231,7 @@ export function useOAuthConnect() { // Listen for messages from the popup (similar to useMsalAuth) const messageListener = (event: MessageEvent) => { // Verify origin for security - const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL); + const apiUrl = new URL(getApiBaseUrl()); if (event.origin !== apiUrl.origin) { return; } diff --git a/src/hooks/useSidebar.ts b/src/hooks/useSidebar.ts index cd67cd0..81251f6 100644 --- a/src/hooks/useSidebar.ts +++ b/src/hooks/useSidebar.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { getSidebarItems } from '../components/PageManager/pageConfigs'; import { SidebarItem } from '../components/PageManager/pageConfigInterface'; import { useLanguage } from '../contexts/LanguageContext'; @@ -6,9 +6,40 @@ import { useLanguage } from '../contexts/LanguageContext'; // Hook to get sidebar items from page configurations export const useSidebarFromPageConfigs = (): SidebarItem[] => { const { t } = useLanguage(); + const [refreshTrigger, setRefreshTrigger] = useState(0); + + // Listen for localStorage changes to refresh sidebar when sign-up status changes + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'speechSignUpData' || e.key === 'speechSignUpTimestamp') { + setRefreshTrigger(prev => prev + 1); + } + }; + + window.addEventListener('storage', handleStorageChange); + + // Also listen for changes in the same tab + const handleCustomStorageChange = () => { + setRefreshTrigger(prev => prev + 1); + }; + + // Custom event for same-tab changes + window.addEventListener('speechSignUpChanged', handleCustomStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('speechSignUpChanged', handleCustomStorageChange); + }; + }, []); return useMemo(() => { + console.log('🔄 Sidebar refreshing, trigger:', refreshTrigger); const sidebarItems = getSidebarItems(); + console.log('📋 Sidebar items:', sidebarItems.map(item => ({ + name: item.name, + hasSubmenu: !!item.submenu, + submenuCount: item.submenu?.length || 0 + }))); // Map the items with translations return sidebarItems.map(item => ({ @@ -17,7 +48,7 @@ export const useSidebarFromPageConfigs = (): SidebarItem[] => { // For now, we'll use the names from pageConfigs directly name: getTranslatedName(item.name, t) })); - }, [t]); + }, [t, refreshTrigger]); }; // Helper function to get translated names @@ -30,7 +61,9 @@ const getTranslatedName = (name: string, t: (key: string) => string): string => 'Connections': t('nav.connections'), 'Team Bereich': t('nav.team'), 'Einstellungen': t('nav.settings'), - 'Test Sharepoint': t('nav.testSharepoint') + 'Test Sharepoint': t('nav.testSharepoint'), + 'Speech': t('nav.speech'), + 'Transkriptverwaltung': t('nav.transcript_management') }; return translationMap[name] || name; diff --git a/src/locales/de.ts b/src/locales/de.ts index 7f961f5..111a378 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -7,6 +7,8 @@ export default { 'nav.workflows': 'Workflows', 'nav.settings': 'Einstellungen', 'nav.testSharepoint': 'SharePoint Test', + 'nav.speech': 'Sprache', + 'nav.transcript_management': 'Transkriptverwaltung', // Settings page 'settings.title': 'Einstellungen', @@ -414,6 +416,7 @@ export default { // FormGenerator 'formgen.search.placeholder': 'Suchen...', + 'formgen.refresh.tooltip': 'Daten aktualisieren', 'formgen.filter.yes': 'Ja', 'formgen.filter.no': 'Nein', 'formgen.filter.clear': 'Filter löschen', @@ -498,4 +501,141 @@ export default { 'sharepoint.sites.retryConnection': 'Versuchen Sie, Ihr Microsoft-Konto auf der Verbindungsseite erneut zu verbinden.', 'sharepoint.form.siteUrl': 'SharePoint Site URL', 'sharepoint.form.folderPaths': 'Ordnerpfade', + + // Speech + 'speech.title': 'Sprach Integration', + 'speech.subtitle': 'Unterstützt von', + 'speech.signup.title': 'Sprach Integration', + 'speech.signup.subtitle': 'Unterstützt von', + + 'speech.info.va': 'Virtual Assistant (VA)', + 'speech.info.va_description': 'Geben Sie Kunden einen schnellen und effizienten Selbstservice für Sprach- und Textanfragen, der 24/7 verfügbar ist.', + 'speech.info.sa': 'Speech Analytics (SA)', + 'speech.info.sa_description': 'Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten.', + 'speech.info.vb': 'Voice Biometrics (VB)', + 'speech.info.vb_description': 'Identifizieren und authentifizieren Sie Anrufer in Sekunden mit kontinuierlicher Verifizierung und Sicherheit.', + 'speech.info.ka': 'Knowledge Agent (KA)', + 'speech.info.ka_description': 'Vereinheitlichen und liefern Sie Informationen an Ihre Kunden und Mitarbeiter, wann und wo sie sie benötigen.', + 'speech.info.cp': 'Chat Platform (CP)', + 'speech.info.cp_description': 'Bieten Sie Unterstützung im Live-Chat und setzen Sie intelligente Chatbots in allen Kanälen ein.', + 'speech.info.aa': 'Agent Assist (AA)', + 'speech.info.aa_description': 'Stellen Sie alles, was Ihre Agenten benötigen, in ihren Händen bereit, mit einem einheitlichen Agent-Desktop.', + + 'speech.info.about': 'Revolutionäre Telefonie-Integration mit Spitch.ai', + 'speech.info.about_intro': 'Erleben Sie die Zukunft der Mandantenkommunikation durch unsere strategische Partnerschaft mit Spitch.ai. Diese bahnbrechende Integration verwandelt Ihre PowerOn-Plattform in ein intelligentes Telefonie-System, das externe Mandanten nahtlos mit Unternehmen verbindet.', + 'speech.info.workflow_title': 'Nahtloser Mandanten-Workflow:', + 'speech.info.workflow_description': 'Von der Registrierung bis zur technischen Einrichtung - Ihr Mandant registriert sich bei PowerOn für Telefonie-Services, lädt Dokumente hoch und erhält automatisch eine technische SIP-Nummer von Spitch. Die Call-Weiterleitung kann jederzeit aktiviert oder deaktiviert werden, was maximale Flexibilität und BCM-Sicherheit gewährleistet.', + 'speech.info.ai_title': 'KI-gestützte Dokumentengenerierung:', + 'speech.info.ai_description': 'Unsere bereits aktive Dokumenten-Extraktions-Engine generiert automatisch personalisierte Dokumente für Spitch, basierend auf Mandantenspezifischen Daten. Die KI nutzt FAQ-Datenbanken, Mitarbeiterinformationen und Service-Details, um jeden Anruf kontextuell und hochpersonalisiert zu gestalten.', + 'speech.info.sync_title': 'Echtzeit-Datensynchronisation:', + 'speech.info.sync_description': 'Spitch prüft vor jedem Anruf die Mandantenberechtigung bei PowerOn, während alle Datenänderungen zentral von PowerOn initiiert werden. Call-Transkripte werden in Echtzeit in Ihrer PowerOn-Datenbank gespeichert, mit vollständiger Mandantenisolation und Sicherheit. Bei Ausfällen werden Anrufe automatisch blockiert, um die Integrität zu gewährleisten.', + 'speech.info.cost_title': 'Kosteneinsparungen & Effizienz:', + 'speech.info.cost_description': 'Mandanten können jederzeit auf die technische SIP-Nummer umstellen und dabei erhebliche Telefoniekosten sparen. Die Integration funktioniert wie ein weiterer Connector (Outlook, SharePoint) und wird nahtlos in Ihren bestehenden Workflow integriert.', + 'speech.info.about_link': 'Mehr erfahren', + + 'speech.signup.button': 'Verbinden', + 'speech.signup.title': 'Mandat für Sprach Integration erstellen', + 'speech.signup.subtitle': 'Erstellen Sie Ihr Mandat für die Spitch.ai Integration', + 'speech.signup.back': 'Zurück zur Sprach Integration', + 'speech.signup.submit': 'Mandat erstellen', + 'speech.signup.cancel': 'Abbrechen', + + 'speech.signup.company_info': 'Unternehmensinformationen', + 'speech.signup.company_name': 'Firmenname', + 'speech.signup.company_name_placeholder': 'Geben Sie Ihren Firmennamen ein', + 'speech.signup.industry': 'Branche', + 'speech.signup.industry_placeholder': 'z.B. Finanzdienstleistungen, Technologie, etc.', + 'speech.signup.business_hours': 'Geschäftszeiten', + 'speech.signup.timezone': 'Zeitzone', + + 'speech.signup.contact_info': 'Kontaktinformationen', + 'speech.signup.email': 'E-Mail-Adresse', + 'speech.signup.email_placeholder': 'kontakt@firma.com', + 'speech.signup.phone': 'Telefonnummer', + 'speech.signup.phone_placeholder': '+41 123 456 789', + 'speech.signup.street': 'Straße', + 'speech.signup.postal_code': 'Postleitzahl', + 'speech.signup.city': 'Stadt', + 'speech.signup.country': 'Land', + + 'speech.signup.contacts_setup': 'Kontakte einrichten', + 'speech.signup.contacts_description': 'Möchten Sie jetzt Kontakte für Ihr Mandat einrichten? Sie können dies auch später in den Einstellungen tun.', + 'speech.signup.setup_contacts': 'Kontakte einrichten', + 'speech.signup.skip_for_now': 'Jetzt überspringen', + + 'speech.signup.company_required': 'Firmenname ist erforderlich', + 'speech.signup.industry_required': 'Branche ist erforderlich', + 'speech.signup.email_required': 'E-Mail-Adresse ist erforderlich', + 'speech.signup.email_invalid': 'Bitte geben Sie eine gültige E-Mail-Adresse ein', + 'speech.signup.phone_required': 'Telefonnummer ist erforderlich', + 'speech.signup.street_required': 'Straße ist erforderlich', + 'speech.signup.postal_code_required': 'Postleitzahl ist erforderlich', + 'speech.signup.city_required': 'Stadt ist erforderlich', + 'speech.signup.country_required': 'Land ist erforderlich', + + 'speech.status.submitted': '✓ Mandat eingereicht', + 'speech.status.reset': 'Neu starten', + + 'speech.confirmation.title': 'Mandat erfolgreich eingereicht!', + 'speech.confirmation.message': 'Vielen Dank für Ihr Interesse an unserer Sprach Integration powered by Spitch.ai. Wir haben Ihr Mandat erhalten und werden es in Kürze überprüfen.', + 'speech.confirmation.submitted_data': 'Eingereichte Daten:', + 'speech.confirmation.company': 'Firma', + 'speech.confirmation.industry': 'Branche', + 'speech.confirmation.email': 'E-Mail', + 'speech.confirmation.phone': 'Telefon', + 'speech.confirmation.address': 'Adresse', + 'speech.confirmation.timezone': 'Zeitzone', + 'speech.confirmation.back': 'Zurück zur Sprach Integration', + 'speech.confirmation.reset': 'Neu starten', + 'speech.confirmation.next_steps': 'Was passiert als nächstes?', + 'speech.confirmation.email_confirmation': 'E-Mail-Bestätigung', + 'speech.confirmation.email_confirmation_desc': 'Sie erhalten in den nächsten Minuten eine Bestätigungs-E-Mail.', + 'speech.confirmation.review_process': 'Überprüfungsprozess', + 'speech.confirmation.review_process_desc': 'Unser Team wird Ihr Mandat innerhalb von 1-2 Werktagen überprüfen.', + 'speech.confirmation.setup_call': 'Einrichtungsanruf', + 'speech.confirmation.setup_call_desc': 'Bei Genehmigung planen wir einen Einrichtungsanruf zur Konfiguration Ihrer Integration.', + 'speech.confirmation.questions': 'Fragen?', + 'speech.confirmation.questions_desc': 'Falls Sie Fragen zu Ihrem Mandat oder dem Integrationsprozess haben, zögern Sie nicht, unser Support-Team zu kontaktieren.', + 'speech.confirmation.transcript_management': 'Transkriptverwaltung', + 'speech.confirmation.speech_settings': 'Sprach-Einstellungen', + + 'speech.transcripts.title': 'Transkriptverwaltung', + 'speech.transcripts.new_transcript': 'Neues Transkript', + 'speech.transcripts.recent_transcripts': 'Aktuelle Transkripte', + 'speech.transcripts.no_transcripts': 'Keine Transkripte vorhanden', + 'speech.transcripts.date': 'Datum', + 'speech.transcripts.duration': 'Dauer', + 'speech.transcripts.status': 'Status', + 'speech.transcripts.transcript': 'Transkript', + 'speech.transcripts.processing': 'Transkript wird verarbeitet...', + 'speech.transcripts.status.completed': 'Abgeschlossen', + 'speech.transcripts.status.processing': 'Verarbeitung', + 'speech.transcripts.status.failed': 'Fehlgeschlagen', + 'speech.transcripts.access_denied_title': 'Zugriff verweigert', + 'speech.transcripts.access_denied_message': 'Sie müssen sich zuerst für die Sprach-Integration anmelden, um auf die Transkriptverwaltung zuzugreifen.', + 'speech.transcripts.sign_up_now': 'Jetzt anmelden', + 'speech.transcripts.subject': 'Betreff', + 'speech.transcripts.start_time': 'Startzeit', + 'speech.transcripts.end_time': 'Endzeit', + 'speech.transcripts.caller': 'Anrufer', + 'speech.transcripts.recipient': 'Empfänger', + 'speech.transcripts.tags': 'Tags', + 'speech.transcripts.created': 'Erstellt', + 'speech.transcripts.view': 'Anzeigen', + 'speech.transcripts.download': 'Herunterladen', + + 'speech.settings.title': 'Sprach-Integration Einstellungen', + 'speech.settings.description': 'Verwalten Sie Ihre Sprach-Integrations-Konfiguration und Einstellungen.', + 'speech.settings.company_info': 'Unternehmensinformationen', + 'speech.settings.contact_info': 'Kontaktinformationen', + 'speech.settings.business_hours': 'Geschäftszeiten & Zeitzone', + 'speech.settings.save': 'Änderungen speichern', + 'speech.settings.saving': 'Speichern...', + 'speech.settings.save_success': 'Einstellungen erfolgreich gespeichert!', + 'speech.settings.save_error': 'Fehler beim Speichern der Einstellungen. Bitte versuchen Sie es erneut.', + 'speech.settings.reset': 'Auf Standard zurücksetzen', + 'speech.settings.reset_confirm': 'Sind Sie sicher, dass Sie alle Sprach-Integrations-Einstellungen zurücksetzen möchten? Diese Aktion kann nicht rückgängig gemacht werden.', + 'speech.settings.reset_success': 'Einstellungen wurden erfolgreich zurückgesetzt.', + 'speech.settings.no_data': 'Keine Sprach-Integrations-Daten gefunden. Bitte melden Sie sich zuerst an, um auf die Einstellungen zuzugreifen.', + 'speech.settings.sign_up_now': 'Jetzt anmelden', }; \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index c84d51f..45f15bc 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -7,6 +7,8 @@ export default { 'nav.connections': 'Connections', 'nav.settings': 'Settings', 'nav.testSharepoint': 'Test SharePoint', + 'nav.speech': 'Speech', + 'nav.transcript_management': 'Transcript Management', // Settings page 'settings.title': 'Settings', @@ -417,6 +419,7 @@ export default { // FormGenerator 'formgen.search.placeholder': 'Search...', + 'formgen.refresh.tooltip': 'Refresh data', 'formgen.filter.yes': 'Yes', 'formgen.filter.no': 'No', 'formgen.filter.clear': 'Clear filter', @@ -498,4 +501,141 @@ export default { 'sharepoint.sites.retryConnection': 'Try reconnecting your Microsoft account in the Connections page.', 'sharepoint.form.siteUrl': 'SharePoint Site URL', 'sharepoint.form.folderPaths': 'Folder Paths', + + // Speech + 'speech.title': 'Speech Integration', + 'speech.subtitle': 'Powered by', + 'speech.signup.title': 'Speech Integration', + 'speech.signup.subtitle': 'Powered by', + + 'speech.info.va': 'Virtual Assistant (VA)', + 'speech.info.va_description': 'Give customers a fast and efficient self-service for voice and text queries that\'s available 24/7.', + 'speech.info.sa': 'Speech Analytics (SA)', + 'speech.info.sa_description': 'Automatically monitor 100% of conversations to get valuable insights for your business.', + 'speech.info.vb': 'Voice Biometrics (VB)', + 'speech.info.vb_description': 'Identify and authenticate callers in seconds with continuous verification and security.', + 'speech.info.ka': 'Knowledge Agent (KA)', + 'speech.info.ka_description': 'Unify and deliver info to your customers and staff wherever and whenever they need it.', + 'speech.info.cp': 'Chat Platform (CP)', + 'speech.info.cp_description': 'Deliver assistance in live chat and deploy intelligent chatbots in all channels.', + 'speech.info.aa': 'Agent Assist (AA)', + 'speech.info.aa_description': 'Put everything your agents need at their fingertips, with a unified agent desktop.', + + 'speech.info.about': 'Revolutionary Telephony Integration with Spitch.ai', + 'speech.info.about_intro': 'Experience the future of client communication through our strategic partnership with Spitch.ai. This groundbreaking integration transforms your PowerOn platform into an intelligent telephony system that seamlessly connects external clients with companies.', + 'speech.info.workflow_title': 'Seamless Client Workflow:', + 'speech.info.workflow_description': 'From registration to technical setup - your client registers with PowerOn for telephony services, uploads documents, and automatically receives a technical SIP number from Spitch. Call forwarding can be activated or deactivated at any time, ensuring maximum flexibility and BCM safety.', + 'speech.info.ai_title': 'AI-Powered Document Generation:', + 'speech.info.ai_description': 'Our already active document extraction engine automatically generates personalized documents for Spitch based on client-specific data. The AI uses FAQ databases, employee information, and service details to make every call contextual and highly personalized.', + 'speech.info.sync_title': 'Real-time Data Synchronization:', + 'speech.info.sync_description': 'Spitch checks client authorization with PowerOn before each call, while all data changes are centrally initiated by PowerOn. Call transcripts are stored in real-time in your PowerOn database with complete client isolation and security. In case of failures, calls are automatically blocked to ensure integrity.', + 'speech.info.cost_title': 'Cost Savings & Efficiency:', + 'speech.info.cost_description': 'Clients can switch to the technical SIP number at any time and save significant telephony costs. The integration works like another connector (Outlook, SharePoint) and is seamlessly integrated into your existing workflow.', + 'speech.info.about_link': 'Learn more', + + 'speech.signup.button': 'Connect', + 'speech.signup.title': 'Create Mandate for Speech Integration', + 'speech.signup.subtitle': 'Create your mandate for Spitch.ai integration', + 'speech.signup.back': 'Back to Speech Integration', + 'speech.signup.submit': 'Create Mandate', + 'speech.signup.cancel': 'Cancel', + + 'speech.signup.company_info': 'Company Information', + 'speech.signup.company_name': 'Company Name', + 'speech.signup.company_name_placeholder': 'Enter your company name', + 'speech.signup.industry': 'Industry', + 'speech.signup.industry_placeholder': 'e.g. Financial Services, Technology, etc.', + 'speech.signup.business_hours': 'Business Hours', + 'speech.signup.timezone': 'Timezone', + + 'speech.signup.contact_info': 'Contact Information', + 'speech.signup.email': 'Email Address', + 'speech.signup.email_placeholder': 'contact@company.com', + 'speech.signup.phone': 'Phone Number', + 'speech.signup.phone_placeholder': '+41 123 456 789', + 'speech.signup.street': 'Street', + 'speech.signup.postal_code': 'Postal Code', + 'speech.signup.city': 'City', + 'speech.signup.country': 'Country', + + 'speech.signup.contacts_setup': 'Setup Contacts', + 'speech.signup.contacts_description': 'Would you like to setup contacts for your mandate now? You can also do this later in settings.', + 'speech.signup.setup_contacts': 'Setup Contacts', + 'speech.signup.skip_for_now': 'Skip for Now', + + 'speech.signup.company_required': 'Company name is required', + 'speech.signup.industry_required': 'Industry is required', + 'speech.signup.email_required': 'Email address is required', + 'speech.signup.email_invalid': 'Please enter a valid email address', + 'speech.signup.phone_required': 'Phone number is required', + 'speech.signup.street_required': 'Street is required', + 'speech.signup.postal_code_required': 'Postal code is required', + 'speech.signup.city_required': 'City is required', + 'speech.signup.country_required': 'Country is required', + + 'speech.status.submitted': '✓ Mandate Submitted', + 'speech.status.reset': 'Start Over', + + 'speech.confirmation.title': 'Mandate Submitted Successfully!', + 'speech.confirmation.message': 'Thank you for your interest in our Speech Integration powered by Spitch.ai. We have received your mandate and will review it shortly.', + 'speech.confirmation.submitted_data': 'Submitted Data:', + 'speech.confirmation.company': 'Company', + 'speech.confirmation.industry': 'Industry', + 'speech.confirmation.email': 'Email', + 'speech.confirmation.phone': 'Phone', + 'speech.confirmation.address': 'Address', + 'speech.confirmation.timezone': 'Timezone', + 'speech.confirmation.back': 'Back to Speech Integration', + 'speech.confirmation.reset': 'Start Over', + 'speech.confirmation.next_steps': 'What happens next?', + 'speech.confirmation.email_confirmation': 'Email Confirmation', + 'speech.confirmation.email_confirmation_desc': 'You will receive a confirmation email within the next few minutes.', + 'speech.confirmation.review_process': 'Review Process', + 'speech.confirmation.review_process_desc': 'Our team will review your mandate within 1-2 business days.', + 'speech.confirmation.setup_call': 'Setup Call', + 'speech.confirmation.setup_call_desc': 'If approved, we\'ll schedule a setup call to configure your integration.', + 'speech.confirmation.questions': 'Questions?', + 'speech.confirmation.questions_desc': 'If you have any questions about your mandate or the integration process, please don\'t hesitate to contact our support team.', + 'speech.confirmation.transcript_management': 'Transcript Management', + 'speech.confirmation.speech_settings': 'Speech Settings', + + 'speech.transcripts.title': 'Transcript Management', + 'speech.transcripts.new_transcript': 'New Transcript', + 'speech.transcripts.recent_transcripts': 'Recent Transcripts', + 'speech.transcripts.no_transcripts': 'No transcripts available', + 'speech.transcripts.date': 'Date', + 'speech.transcripts.duration': 'Duration', + 'speech.transcripts.status': 'Status', + 'speech.transcripts.transcript': 'Transcript', + 'speech.transcripts.processing': 'Processing transcript...', + 'speech.transcripts.status.completed': 'Completed', + 'speech.transcripts.status.processing': 'Processing', + 'speech.transcripts.status.failed': 'Failed', + 'speech.transcripts.access_denied_title': 'Access Denied', + 'speech.transcripts.access_denied_message': 'You must first sign up for speech integration to access transcript management.', + 'speech.transcripts.sign_up_now': 'Sign Up Now', + 'speech.transcripts.subject': 'Subject', + 'speech.transcripts.start_time': 'Start Time', + 'speech.transcripts.end_time': 'End Time', + 'speech.transcripts.caller': 'Caller', + 'speech.transcripts.recipient': 'Recipient', + 'speech.transcripts.tags': 'Tags', + 'speech.transcripts.created': 'Created', + 'speech.transcripts.view': 'View', + 'speech.transcripts.download': 'Download', + + 'speech.settings.title': 'Speech Integration Settings', + 'speech.settings.description': 'Manage your speech integration configuration and preferences.', + 'speech.settings.company_info': 'Company Information', + 'speech.settings.contact_info': 'Contact Information', + 'speech.settings.business_hours': 'Business Hours & Timezone', + 'speech.settings.save': 'Save Changes', + 'speech.settings.saving': 'Saving...', + 'speech.settings.save_success': 'Settings saved successfully!', + 'speech.settings.save_error': 'Failed to save settings. Please try again.', + 'speech.settings.reset': 'Reset to Default', + 'speech.settings.reset_confirm': 'Are you sure you want to reset all speech integration settings? This action cannot be undone.', + 'speech.settings.reset_success': 'Settings have been reset successfully.', + 'speech.settings.no_data': 'No speech integration data found. Please sign up first to access settings.', + 'speech.settings.sign_up_now': 'Sign Up Now', }; \ No newline at end of file diff --git a/src/locales/fr.ts b/src/locales/fr.ts index d5d665c..1da63a0 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -7,6 +7,8 @@ export default { 'nav.connections': 'Connections', 'nav.settings': 'Paramètres', 'nav.testSharepoint': 'Test SharePoint', + 'nav.speech': 'Parole', + 'nav.transcript_management': 'Gestion des Transcriptions', // Settings page 'settings.title': 'Paramètres', @@ -417,6 +419,7 @@ export default { // FormGenerator 'formgen.search.placeholder': 'Rechercher...', + 'formgen.refresh.tooltip': 'Actualiser les données', 'formgen.filter.yes': 'Oui', 'formgen.filter.no': 'Non', 'formgen.filter.clear': 'Effacer le filtre', @@ -498,4 +501,141 @@ export default { 'sharepoint.sites.retryConnection': 'Essayez de reconnecter votre compte Microsoft dans la page Connexions.', 'sharepoint.form.siteUrl': 'URL du site SharePoint', 'sharepoint.form.folderPaths': 'Chemins des dossiers', + + // Speech + 'speech.title': 'Intégration Vocale', + 'speech.subtitle': 'Alimenté par ', + 'speech.signup.title': 'Intégration Vocale', + 'speech.signup.subtitle': 'Alimenté par', + + 'speech.info.va': 'Assistant Virtuel (VA)', + 'speech.info.va_description': 'Offrez aux clients un libre-service rapide et efficace pour les requêtes vocales et textuelles disponible 24h/24.', + 'speech.info.sa': 'Analyse Vocale (SA)', + 'speech.info.sa_description': 'Surveillez automatiquement 100% des conversations pour obtenir des insights précieux pour votre entreprise.', + 'speech.info.vb': 'Biométrie Vocale (VB)', + 'speech.info.vb_description': 'Identifiez et authentifiez les appelants en quelques secondes avec une vérification et sécurité continues.', + 'speech.info.ka': 'Agent de Connaissance (KA)', + 'speech.info.ka_description': 'Unifiez et livrez des informations à vos clients et employés où et quand ils en ont besoin.', + 'speech.info.cp': 'Plateforme de Chat (CP)', + 'speech.info.cp_description': 'Offrez une assistance en chat en direct et déployez des chatbots intelligents sur tous les canaux.', + 'speech.info.aa': 'Assistance Agent (AA)', + 'speech.info.aa_description': 'Mettez tout ce dont vos agents ont besoin à portée de main, avec un bureau d\'agent unifié.', + + 'speech.info.about': 'Intégration Téléphonique Révolutionnaire avec Spitch.ai', + 'speech.info.about_intro': 'Découvrez l\'avenir de la communication client grâce à notre partenariat stratégique avec Spitch.ai. Cette intégration révolutionnaire transforme votre plateforme PowerOn en un système téléphonique intelligent qui connecte de manière transparente les clients externes avec les entreprises.', + 'speech.info.workflow_title': 'Workflow Client Transparent:', + 'speech.info.workflow_description': 'De l\'inscription à la configuration technique - votre client s\'inscrit auprès de PowerOn pour les services téléphoniques, télécharge des documents et reçoit automatiquement un numéro SIP technique de Spitch. Le transfert d\'appel peut être activé ou désactivé à tout moment, garantissant une flexibilité maximale et la sécurité BCM.', + 'speech.info.ai_title': 'Génération de Documents alimentée par l\'IA:', + 'speech.info.ai_description': 'Notre moteur d\'extraction de documents déjà actif génère automatiquement des documents personnalisés pour Spitch basés sur les données spécifiques au client. L\'IA utilise les bases de données FAQ, les informations employés et les détails de service pour rendre chaque appel contextuel et hautement personnalisé.', + 'speech.info.sync_title': 'Synchronisation de Données en Temps Réel:', + 'speech.info.sync_description': 'Spitch vérifie l\'autorisation client avec PowerOn avant chaque appel, tandis que tous les changements de données sont initiés centralement par PowerOn. Les transcriptions d\'appels sont stockées en temps réel dans votre base de données PowerOn avec une isolation complète du client et la sécurité. En cas de panne, les appels sont automatiquement bloqués pour assurer l\'intégrité.', + 'speech.info.cost_title': 'Économies de Coûts & Efficacité:', + 'speech.info.cost_description': 'Les clients peuvent basculer sur le numéro SIP technique à tout moment et économiser des coûts téléphoniques significatifs. L\'intégration fonctionne comme un autre connecteur (Outlook, SharePoint) et est intégrée de manière transparente dans votre workflow existant.', + 'speech.info.about_link': 'En savoir plus', + + 'speech.signup.button': 'Connecter', + 'speech.signup.title': 'Créer un Mandat pour l\'Intégration Vocale', + 'speech.signup.subtitle': 'Créez votre mandat pour l\'intégration Spitch.ai', + 'speech.signup.back': 'Retour à l\'Intégration Vocale', + 'speech.signup.submit': 'Créer le Mandat', + 'speech.signup.cancel': 'Annuler', + + 'speech.signup.company_info': 'Informations de l\'Entreprise', + 'speech.signup.company_name': 'Nom de l\'Entreprise', + 'speech.signup.company_name_placeholder': 'Entrez le nom de votre entreprise', + 'speech.signup.industry': 'Secteur d\'Activité', + 'speech.signup.industry_placeholder': 'ex. Services Financiers, Technologie, etc.', + 'speech.signup.business_hours': 'Heures d\'Ouverture', + 'speech.signup.timezone': 'Fuseau Horaire', + + 'speech.signup.contact_info': 'Informations de Contact', + 'speech.signup.email': 'Adresse Email', + 'speech.signup.email_placeholder': 'contact@entreprise.com', + 'speech.signup.phone': 'Numéro de Téléphone', + 'speech.signup.phone_placeholder': '+41 123 456 789', + 'speech.signup.street': 'Rue', + 'speech.signup.postal_code': 'Code Postal', + 'speech.signup.city': 'Ville', + 'speech.signup.country': 'Pays', + + 'speech.signup.contacts_setup': 'Configurer les Contacts', + 'speech.signup.contacts_description': 'Souhaitez-vous configurer les contacts pour votre mandat maintenant ? Vous pouvez également le faire plus tard dans les paramètres.', + 'speech.signup.setup_contacts': 'Configurer les Contacts', + 'speech.signup.skip_for_now': 'Ignorer pour l\'Instant', + + 'speech.signup.company_required': 'Le nom de l\'entreprise est requis', + 'speech.signup.industry_required': 'Le secteur d\'activité est requis', + 'speech.signup.email_required': 'L\'adresse email est requise', + 'speech.signup.email_invalid': 'Veuillez entrer une adresse email valide', + 'speech.signup.phone_required': 'Le numéro de téléphone est requis', + 'speech.signup.street_required': 'La rue est requise', + 'speech.signup.postal_code_required': 'Le code postal est requis', + 'speech.signup.city_required': 'La ville est requise', + 'speech.signup.country_required': 'Le pays est requis', + + 'speech.status.submitted': '✓ Mandat Soumis', + 'speech.status.reset': 'Recommencer', + + 'speech.confirmation.title': 'Mandat Soumis avec Succès !', + 'speech.confirmation.message': 'Merci pour votre intérêt pour notre Intégration Vocale powered by Spitch.ai. Nous avons reçu votre mandat et l\'examinerons sous peu.', + 'speech.confirmation.submitted_data': 'Données Soumises :', + 'speech.confirmation.company': 'Entreprise', + 'speech.confirmation.industry': 'Secteur', + 'speech.confirmation.email': 'Email', + 'speech.confirmation.phone': 'Téléphone', + 'speech.confirmation.address': 'Adresse', + 'speech.confirmation.timezone': 'Fuseau Horaire', + 'speech.confirmation.back': 'Retour à l\'Intégration Vocale', + 'speech.confirmation.reset': 'Recommencer', + 'speech.confirmation.next_steps': 'Que se passe-t-il ensuite ?', + 'speech.confirmation.email_confirmation': 'Confirmation par Email', + 'speech.confirmation.email_confirmation_desc': 'Vous recevrez un email de confirmation dans les prochaines minutes.', + 'speech.confirmation.review_process': 'Processus de Révision', + 'speech.confirmation.review_process_desc': 'Notre équipe examinera votre mandat dans les 1-2 jours ouvrables.', + 'speech.confirmation.setup_call': 'Appel de Configuration', + 'speech.confirmation.setup_call_desc': 'Si approuvé, nous planifierons un appel de configuration pour configurer votre intégration.', + 'speech.confirmation.questions': 'Questions ?', + 'speech.confirmation.questions_desc': 'Si vous avez des questions sur votre mandat ou le processus d\'intégration, n\'hésitez pas à contacter notre équipe de support.', + 'speech.confirmation.transcript_management': 'Gestion des Transcriptions', + 'speech.confirmation.speech_settings': 'Paramètres Vocaux', + + 'speech.transcripts.title': 'Gestion des Transcriptions', + 'speech.transcripts.new_transcript': 'Nouvelle Transcription', + 'speech.transcripts.recent_transcripts': 'Transcriptions Récentes', + 'speech.transcripts.no_transcripts': 'Aucune transcription disponible', + 'speech.transcripts.date': 'Date', + 'speech.transcripts.duration': 'Durée', + 'speech.transcripts.status': 'Statut', + 'speech.transcripts.transcript': 'Transcription', + 'speech.transcripts.processing': 'Traitement de la transcription...', + 'speech.transcripts.status.completed': 'Terminé', + 'speech.transcripts.status.processing': 'En cours', + 'speech.transcripts.status.failed': 'Échoué', + 'speech.transcripts.access_denied_title': 'Accès Refusé', + 'speech.transcripts.access_denied_message': 'Vous devez d\'abord vous inscrire à l\'intégration vocale pour accéder à la gestion des transcriptions.', + 'speech.transcripts.sign_up_now': 'S\'inscrire Maintenant', + 'speech.transcripts.subject': 'Sujet', + 'speech.transcripts.start_time': 'Heure de Début', + 'speech.transcripts.end_time': 'Heure de Fin', + 'speech.transcripts.caller': 'Appelant', + 'speech.transcripts.recipient': 'Destinataire', + 'speech.transcripts.tags': 'Étiquettes', + 'speech.transcripts.created': 'Créé', + 'speech.transcripts.view': 'Voir', + 'speech.transcripts.download': 'Télécharger', + + 'speech.settings.title': 'Paramètres d\'Intégration Vocale', + 'speech.settings.description': 'Gérez votre configuration et vos préférences d\'intégration vocale.', + 'speech.settings.company_info': 'Informations de l\'Entreprise', + 'speech.settings.contact_info': 'Informations de Contact', + 'speech.settings.business_hours': 'Heures d\'Ouverture et Fuseau Horaire', + 'speech.settings.save': 'Sauvegarder les Modifications', + 'speech.settings.saving': 'Sauvegarde...', + 'speech.settings.save_success': 'Paramètres sauvegardés avec succès !', + 'speech.settings.save_error': 'Échec de la sauvegarde des paramètres. Veuillez réessayer.', + 'speech.settings.reset': 'Réinitialiser par Défaut', + 'speech.settings.reset_confirm': 'Êtes-vous sûr de vouloir réinitialiser tous les paramètres d\'intégration vocale ? Cette action ne peut pas être annulée.', + 'speech.settings.reset_success': 'Les paramètres ont été réinitialisés avec succès.', + 'speech.settings.no_data': 'Aucune donnée d\'intégration vocale trouvée. Veuillez d\'abord vous inscrire pour accéder aux paramètres.', + 'speech.settings.sign_up_now': 'S\'inscrire Maintenant', }; \ No newline at end of file diff --git a/src/pages/Home/Einstellungen.tsx b/src/pages/Home/Einstellungen.tsx index 36be041..8101b23 100644 --- a/src/pages/Home/Einstellungen.tsx +++ b/src/pages/Home/Einstellungen.tsx @@ -1,14 +1,18 @@ import { useState, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; import styles from './HomeStyles/Einstellungen.module.css'; import sharedStyles from '../../components/PageManager/pages.module.css'; import { useLanguage, Language } from '../../contexts/LanguageContext'; import { useCurrentUser, useUser, User } from '../../hooks/useUsers'; +import SettingsSpeech from '../../components/settings/settingsSpeech'; function Einstellungen() { + console.log('🏠 Einstellungen component loaded'); const [isDarkMode, setIsDarkMode] = useState(false); const { currentLanguage, setLanguage, t, isLoading } = useLanguage(); const { user: currentUser, isLoading: currentUserLoading } = useCurrentUser(); const { getUser, updateUser, isLoading: updateLoading } = useUser(); + const navigate = useNavigate(); // Local state for user data fetched directly via API const [user, setUser] = useState(null); @@ -28,6 +32,9 @@ function Einstellungen() { const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update const hasLoadedUser = useRef(false); + // Speech integration state + const [hasSpeechIntegration, setHasSpeechIntegration] = useState(false); + // Fetch user data directly using the /api/users/{userId} endpoint const fetchUserData = async () => { if (!currentUser?.id || hasLoadedUser.current) return; @@ -48,11 +55,43 @@ function Einstellungen() { } }; + // Check for speech integration data + const checkSpeechIntegration = () => { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const timestamp = localStorage.getItem('speechSignUpTimestamp'); + + if (savedData && timestamp) { + const savedTime = parseInt(timestamp); + const now = Date.now(); + const twentyFourHours = 24 * 60 * 60 * 1000; + + // Check if data is still valid (within 24 hours) + if (now - savedTime < twentyFourHours) { + setHasSpeechIntegration(true); + } else { + // Data expired, clear it + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + setHasSpeechIntegration(false); + } + } else { + setHasSpeechIntegration(false); + } + } catch (error) { + console.error('Error checking speech integration data:', error); + setHasSpeechIntegration(false); + } + }; + // Sync component state with current theme on mount useEffect(() => { const savedTheme = localStorage.getItem('theme'); const prefersDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches); setIsDarkMode(prefersDark); + + // Check for speech integration data + checkSpeechIntegration(); }, []); // Fetch user data when currentUser is available @@ -77,6 +116,109 @@ function Einstellungen() { } }, [user, currentLanguage, isUpdating]); + // Listen for speech integration updates + useEffect(() => { + const handleSpeechUpdate = () => { + checkSpeechIntegration(); + }; + + window.addEventListener('speechSignUpChanged', handleSpeechUpdate); + return () => { + window.removeEventListener('speechSignUpChanged', handleSpeechUpdate); + }; + }, []); + + // Handle hash navigation to speech settings + useEffect(() => { + const handleHashChange = () => { + console.log('🔍 Current hash:', window.location.hash); + if (window.location.hash === '#speech-settings') { + console.log('🔗 Hash navigation detected: #speech-settings'); + // Wait for the component to render, then scroll + const scrollToElement = () => { + const element = document.getElementById('speech-settings'); + console.log('🎯 Looking for element with id="speech-settings":', element); + if (element) { + console.log('✅ Element found, scrolling to it'); + element.scrollIntoView({ behavior: 'smooth' }); + return true; + } + console.log('❌ Element not found'); + return false; + }; + + // Try immediately, then with increasing delays + if (!scrollToElement()) { + setTimeout(() => { + if (!scrollToElement()) { + setTimeout(() => scrollToElement(), 200); + } + }, 100); + } + } + }; + + // Check hash on mount with a delay to ensure component is ready + const checkHashOnMount = () => { + console.log('🚀 Initial hash check:', window.location.hash); + if (window.location.hash === '#speech-settings') { + console.log('🚀 Initial hash check: #speech-settings'); + // Wait for component to be ready + setTimeout(() => { + handleHashChange(); + }, 100); + } + }; + + checkHashOnMount(); + + // Listen for hash changes + window.addEventListener('hashchange', handleHashChange); + return () => { + window.removeEventListener('hashchange', handleHashChange); + }; + }, []); // Remove hasSpeechIntegration dependency to avoid re-running + + // Additional effect to handle hash navigation after speech integration status changes + useEffect(() => { + if (window.location.hash === '#speech-settings') { + console.log('🔄 Speech integration status changed, checking hash navigation'); + // Wait a bit for the component to re-render with new content + setTimeout(() => { + const element = document.getElementById('speech-settings'); + console.log('🎯 Re-checking element after status change:', element); + if (element) { + console.log('✅ Element found after status change, scrolling to it'); + element.scrollIntoView({ behavior: 'smooth' }); + } else { + console.log('❌ Element still not found after status change'); + } + }, 50); + } + }, [hasSpeechIntegration]); + + // Effect to handle hash navigation when component mounts + useEffect(() => { + const handleInitialHash = () => { + if (window.location.hash === '#speech-settings') { + console.log('🚀 Component mounted with hash: #speech-settings'); + // Wait for everything to be ready + setTimeout(() => { + const element = document.getElementById('speech-settings'); + console.log('🎯 Initial mount element check:', element); + if (element) { + console.log('✅ Element found on mount, scrolling to it'); + element.scrollIntoView({ behavior: 'smooth' }); + } else { + console.log('❌ Element not found on mount'); + } + }, 200); + } + }; + + handleInitialHash(); + }, []); + const applyTheme = (isDark: boolean) => { if (isDark) { document.documentElement.classList.add('dark-theme'); @@ -376,6 +518,22 @@ function Einstellungen() {
  • + {/* Speech Integration Settings */} +
    + {hasSpeechIntegration ? ( + + ) : ( +
    +

    {t('speech.settings.no_data')}

    + +
    + )} +
    diff --git a/src/pages/Home/HomeStyles/Einstellungen.module.css b/src/pages/Home/HomeStyles/Einstellungen.module.css index d254bd5..574ba99 100644 --- a/src/pages/Home/HomeStyles/Einstellungen.module.css +++ b/src/pages/Home/HomeStyles/Einstellungen.module.css @@ -293,4 +293,25 @@ width: 100%; max-width: 200px; } +} + +.speechSettingsSection { + margin-top: 2rem; + padding-top: 2rem; + border-top: 2px solid var(--color-secondary); +} + +.noSpeechData { + text-align: center; + padding: 2rem; + background: var(--color-bg); + border-radius: 20px; + border: 1px solid var(--color-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.noSpeechData p { + margin: 0 0 1.5rem 0; + color: var(--color-text); + font-size: 1rem; } \ No newline at end of file diff --git a/src/pages/Home/HomeStyles/Speech.module.css b/src/pages/Home/HomeStyles/Speech.module.css new file mode 100644 index 0000000..aa2d256 --- /dev/null +++ b/src/pages/Home/HomeStyles/Speech.module.css @@ -0,0 +1,52 @@ +.partnerLogo { + height: 45px; + width: auto; + margin-left: 0.5rem; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); + transition: transform 0.2s ease; +} + +.partnerLogo:hover { + transform: scale(1.05); +} + +.submittedStatus { + display: flex; + align-items: center; + gap: 1rem; +} + +.statusText { + color: var(--color-secondary); + font-weight: 500; + font-size: 0.9rem; +} + +.resetButton { + padding: 0.5rem 1rem; + background: var(--color-bg); + border: 1px solid var(--color-secondary); + color: var(--color-secondary); + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.resetButton:hover { + background: var(--color-secondary); + color: var(--color-bg); +} + +@media (max-width: 768px) { + .partnerLogo { + height: 20px; + } + + .submittedStatus { + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; + } +} diff --git a/src/pages/Home/HomeStyles/SpeechTranscripts.module.css b/src/pages/Home/HomeStyles/SpeechTranscripts.module.css new file mode 100644 index 0000000..7b7b774 --- /dev/null +++ b/src/pages/Home/HomeStyles/SpeechTranscripts.module.css @@ -0,0 +1,293 @@ +.content { + display: flex; + gap: 2rem; + height: calc(100vh - 200px); + overflow: hidden; +} + +.transcriptList { + flex: 1; + overflow-y: auto; + padding-right: 1rem; +} + +.transcriptList h2 { + font-size: 1.2rem; + font-weight: bold; + color: var(--color-secondary); + margin: 0 0 1rem 0; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--color-secondary); +} + +.transcriptGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.transcriptCard { + background: var(--color-bg); + border: 1px solid var(--color-secondary); + border-radius: 20px; + padding: 1.25rem; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.transcriptCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: var(--color-secondary); +} + +.transcriptCard.selected { + border-color: var(--color-secondary); + background: var(--color-secondary); + color: white; +} + +.transcriptHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.transcriptTitle { + font-size: 1rem; + font-weight: bold; + margin: 0; + color: var(--color-text); + flex: 1; + margin-right: 0.5rem; +} + +.transcriptCard.selected .transcriptTitle { + color: white; +} + +.status { + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status.completed { + background: #10b981; + color: white; +} + +.status.processing { + background: #f59e0b; + color: white; +} + +.status.failed { + background: #ef4444; + color: white; +} + +.transcriptMeta { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + color: var(--color-text); +} + +.transcriptCard.selected .transcriptMeta { + color: rgba(255, 255, 255, 0.8); +} + +.date, .duration { + font-weight: 500; +} + +.transcriptDetails { + flex: 1; + background: var(--color-bg); + border: 1px solid var(--color-secondary); + border-radius: 20px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.detailsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid var(--color-secondary); +} + +.detailsHeader h3 { + font-size: 1.3rem; + font-weight: bold; + color: var(--color-text); + margin: 0; + flex: 1; +} + +.closeButton { + background: none; + border: none; + font-size: 1.5rem; + color: var(--color-text); + cursor: pointer; + padding: 0.25rem; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.closeButton:hover { + background: var(--color-secondary); + color: white; +} + +.detailsContent { + flex: 1; + overflow-y: auto; +} + +.metaInfo { + margin-bottom: 1.5rem; +} + +.metaItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid var(--color-primary); +} + +.metaItem:last-child { + border-bottom: none; +} + +.metaItem strong { + color: var(--color-text); + font-weight: 600; +} + +.metaItem span { + color: var(--color-text); +} + +.transcriptContent { + background: var(--color-bg); + border: 1px solid var(--color-primary); + border-radius: 15px; + padding: 1rem; +} + +.transcriptContent h4 { + font-size: 1rem; + font-weight: bold; + color: var(--color-secondary); + margin: 0 0 0.75rem 0; +} + +.transcriptContent p { + color: var(--color-text); + line-height: 1.6; + margin: 0; + white-space: pre-wrap; +} + +.processingMessage { + text-align: center; + padding: 2rem; + color: var(--color-text); +} + +.processingMessage p { + font-style: italic; + margin: 0; +} + +.emptyState { + text-align: center; + padding: 3rem 1rem; + color: var(--color-text); +} + +.emptyState p { + font-size: 1.1rem; + margin: 0; +} + +.accessDenied { + text-align: center; + padding: 3rem 2rem; + background: var(--color-bg); + border: 1px solid var(--color-secondary); + border-radius: 20px; + margin: 2rem 0; +} + +.accessDenied h2 { + font-size: 1.5rem; + font-weight: bold; + color: var(--color-text); + margin: 0 0 1rem 0; +} + +.accessDenied p { + color: var(--color-text); + margin: 0 0 2rem 0; + line-height: 1.6; +} + +@media (max-width: 1024px) { + .content { + flex-direction: column; + height: auto; + } + + .transcriptList { + overflow-y: visible; + padding-right: 0; + } + + .transcriptDetails { + margin-top: 1rem; + } +} + +@media (max-width: 640px) { + .transcriptGrid { + grid-template-columns: 1fr; + } + + .transcriptCard { + padding: 1rem; + } + + .transcriptDetails { + padding: 1rem; + } + + .detailsHeader h3 { + font-size: 1.1rem; + } + + .metaItem { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } +} diff --git a/src/pages/Home/Speech.tsx b/src/pages/Home/Speech.tsx new file mode 100644 index 0000000..6230da1 --- /dev/null +++ b/src/pages/Home/Speech.tsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react'; +import sharedStyles from '../../components/PageManager/pages.module.css'; +import { SpeechInfo, SpeechSignUp, SpeechConfirmation } from '../../components/Speech'; +import { useLanguage } from '../../contexts/LanguageContext'; +import SpitchLogo from '/logos/spitch-logo.svg'; +import styles from './HomeStyles/Speech.module.css'; + + +type SpeechPage = 'info' | 'signup' | 'confirmation'; + +interface MandateData { + id: string; + mandate_general: { + company_name: string; + industry: string; + contact_info: { + email: string; + phone: string; + street: string; + postal_code: string; + city: string; + country: string; + }; + business_hours: string; + timezone: string; + }; + setup_contacts: boolean; +} + +function Speech() { + const { t } = useLanguage(); + const [currentPage, setCurrentPage] = useState('info'); + const [hasSubmitted, setHasSubmitted] = useState(false); + const [submittedData, setSubmittedData] = useState(null); + + // Check for existing sign-up data on component mount + React.useEffect(() => { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const savedTimestamp = localStorage.getItem('speechSignUpTimestamp'); + + if (savedData && savedTimestamp) { + const data = JSON.parse(savedData); + const timestamp = parseInt(savedTimestamp); + const now = Date.now(); + + // Check if data is from current session (within last 24 hours) + const hoursDiff = (now - timestamp) / (1000 * 60 * 60); + + if (hoursDiff < 24) { + setHasSubmitted(true); + setSubmittedData(data); + setCurrentPage('confirmation'); + } else { + // Clear old data + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + } + } + } catch (error) { + console.error('Failed to load saved sign-up data:', error); + } + }, []); + + const handleSignUp = () => { + setCurrentPage('signup'); + }; + + const handleSignUpSubmit = (data: MandateData) => { + // Save to localStorage for persistence + try { + localStorage.setItem('speechSignUpData', JSON.stringify(data)); + localStorage.setItem('speechSignUpTimestamp', Date.now().toString()); + console.log('Sign-up data saved to localStorage:', data); + + // Dispatch custom event to refresh sidebar + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + } catch (error) { + console.error('Failed to save sign-up data:', error); + } + + setSubmittedData(data); + setHasSubmitted(true); + setCurrentPage('confirmation'); + console.log('Mandate data submitted:', data); + }; + + const handleBackToInfo = () => { + setCurrentPage('info'); + }; + + const handleResetSignUp = () => { + // Clear localStorage and reset state + try { + localStorage.removeItem('speechSignUpData'); + localStorage.removeItem('speechSignUpTimestamp'); + setHasSubmitted(false); + setSubmittedData(null); + setCurrentPage('info'); + + // Dispatch custom event to refresh sidebar + window.dispatchEvent(new CustomEvent('speechSignUpChanged')); + } catch (error) { + console.error('Failed to clear saved data:', error); + } + }; + + return ( +
    +
    +
    +

    {t('speech.title')}

    + {currentPage === 'info' && !hasSubmitted && ( + + )} + {hasSubmitted && ( +
    + + {t('speech.status.submitted')} + + +
    + )} +
    +
    + {t('speech.subtitle')} + Spitch.ai +
    + {currentPage === 'info' && ( + + )} + {currentPage === 'signup' && ( + + )} + {currentPage === 'confirmation' && ( + + )} +
    +
    + ); +} + +export default Speech; \ No newline at end of file diff --git a/src/pages/Home/SpeechTranscripts.tsx b/src/pages/Home/SpeechTranscripts.tsx new file mode 100644 index 0000000..5c97621 --- /dev/null +++ b/src/pages/Home/SpeechTranscripts.tsx @@ -0,0 +1,262 @@ +import { useState, useEffect } from 'react'; +import sharedStyles from '../../components/PageManager/pages.module.css'; +import { useLanguage } from '../../contexts/LanguageContext'; +import { FormGenerator, ColumnConfig } from '../../components/FormGenerator/FormGenerator'; +import { IoIosEye, IoIosDownload } from 'react-icons/io'; + +interface CallTranscript { + id: string; + mandateId: string; + start_datetime: string; + finish_datetime: string; + caller_phone: string; + recipient_phone: string; + transcript_text: string; + subject: string; + tags: string[]; + spitch_call_id: string; + created_at: string; + created_by: string; +} + +function SpeechTranscripts() { + const { t } = useLanguage(); + const [transcripts, setTranscripts] = useState([]); + const [hasSignedUp, setHasSignedUp] = useState(false); + + // Check if user has signed up for speech integration + useEffect(() => { + try { + const savedData = localStorage.getItem('speechSignUpData'); + const timestamp = localStorage.getItem('speechSignUpTimestamp'); + + if (savedData && timestamp) { + const signUpTime = parseInt(timestamp); + const now = Date.now(); + const hoursDiff = (now - signUpTime) / (1000 * 60 * 60); + + // Data is valid for 24 hours + if (hoursDiff < 24) { + setHasSignedUp(true); + } + } + } catch (error) { + console.error('Error checking speech sign-up status:', error); + } + }, []); + + // Column configuration for FormGenerator + const columns: ColumnConfig[] = [ + { + key: 'subject', + label: t('speech.transcripts.subject', 'Subject'), + type: 'string', + width: 200, + sortable: true, + filterable: true, + searchable: true + }, + { + key: 'start_datetime', + label: t('speech.transcripts.start_time', 'Start Time'), + type: 'date', + width: 180, + sortable: true, + filterable: true, + searchable: false + }, + { + key: 'finish_datetime', + label: t('speech.transcripts.end_time', 'End Time'), + type: 'date', + width: 180, + sortable: true, + filterable: true, + searchable: false + }, + { + key: 'caller_phone', + label: t('speech.transcripts.caller', 'Caller'), + type: 'string', + width: 150, + sortable: true, + filterable: true, + searchable: true + }, + { + key: 'recipient_phone', + label: t('speech.transcripts.recipient', 'Recipient'), + type: 'string', + width: 150, + sortable: true, + filterable: true, + searchable: true + }, + { + key: 'tags', + label: t('speech.transcripts.tags', 'Tags'), + type: 'string', + width: 200, + sortable: false, + filterable: true, + searchable: true, + formatter: (value: string[]) => value.join(', ') + }, + { + key: 'created_at', + label: t('speech.transcripts.created', 'Created'), + type: 'date', + width: 150, + sortable: true, + filterable: true, + searchable: false + } + ]; + + // Mock data for demonstration + useEffect(() => { + if (hasSignedUp) { + setTranscripts([ + { + id: '1', + mandateId: 'mandate-001', + start_datetime: '2024-01-15T10:30:00Z', + finish_datetime: '2024-01-15T11:15:00Z', + caller_phone: '+41 987 654 321', + recipient_phone: '+41 123 456 789', + transcript_text: 'This is a sample transcript content for a client meeting discussing project requirements and next steps...', + subject: 'Client Meeting - Project Discussion', + tags: ['client', 'meeting', 'project'], + spitch_call_id: 'spitch-call-uuid-123', + created_at: '2024-01-15T10:30:00Z', + created_by: 'system' + }, + { + id: '2', + mandateId: 'mandate-001', + start_datetime: '2024-01-14T09:00:00Z', + finish_datetime: '2024-01-14T09:15:00Z', + caller_phone: '+41 987 654 322', + recipient_phone: '+41 123 456 790', + transcript_text: 'This is another sample transcript content for a team standup meeting covering daily updates and blockers...', + subject: 'Team Standup Meeting', + tags: ['team', 'standup', 'internal'], + spitch_call_id: 'spitch-call-uuid-124', + created_at: '2024-01-14T09:00:00Z', + created_by: 'system' + }, + { + id: '3', + mandateId: 'mandate-001', + start_datetime: '2024-01-13T14:20:00Z', + finish_datetime: '2024-01-13T14:52:00Z', + caller_phone: '+41 987 654 323', + recipient_phone: '+41 123 456 791', + transcript_text: 'Customer support call regarding billing inquiry and service upgrade options...', + subject: 'Customer Support Call', + tags: ['support', 'billing', 'customer'], + spitch_call_id: 'spitch-call-uuid-125', + created_at: '2024-01-13T14:20:00Z', + created_by: 'system' + }, + { + id: '4', + mandateId: 'mandate-001', + start_datetime: '2024-01-12T16:45:00Z', + finish_datetime: '2024-01-12T17:30:00Z', + caller_phone: '+41 987 654 324', + recipient_phone: '+41 123 456 792', + transcript_text: 'Technical consultation call about system integration and API documentation...', + subject: 'Technical Consultation', + tags: ['technical', 'consultation', 'integration'], + spitch_call_id: 'spitch-call-uuid-126', + created_at: '2024-01-12T16:45:00Z', + created_by: 'system' + }, + { + id: '5', + mandateId: 'mandate-001', + start_datetime: '2024-01-11T11:10:00Z', + finish_datetime: '2024-01-11T11:25:00Z', + caller_phone: '+41 987 654 325', + recipient_phone: '+41 123 456 793', + transcript_text: 'Quick follow-up call to confirm meeting details and deliverables...', + subject: 'Follow-up Call', + tags: ['follow-up', 'meeting', 'confirmation'], + spitch_call_id: 'spitch-call-uuid-127', + created_at: '2024-01-11T11:10:00Z', + created_by: 'system' + } + ]); + } + }, [hasSignedUp]); + + if (!hasSignedUp) { + return ( +
    +
    +
    +

    {t('speech.transcripts.title')}

    +
    +
    +

    {t('speech.transcripts.access_denied_title')}

    +

    {t('speech.transcripts.access_denied_message')}

    + +
    +
    +
    + ); + } + + return ( +
    +
    +
    +

    {t('speech.transcripts.title')}

    +
    + + { + // Handle row click - could open a modal with transcript details + console.log('Selected transcript:', transcript); + }} + actions={[ + { + label: t('speech.transcripts.view', 'View'), + onClick: (transcript) => { + // Handle view action + console.log('View transcript:', transcript); + }, + icon: + }, + { + label: t('speech.transcripts.download', 'Download'), + onClick: (transcript) => { + // Handle download action + console.log('Download transcript:', transcript); + }, + icon: + } + ]} + /> +
    +
    + ); +} + +export default SpeechTranscripts; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 149d63e..95f8496 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -57,12 +57,18 @@ function Login() { } }; - const handleCredentialLogin = async () => { + const handleCredentialLogin = async (e?: React.MouseEvent) => { + e?.preventDefault(); // Prevent default form submission try { + console.log("Attempting login with:", username); await login(username, password); + console.log("Login successful, navigating to:", from); + // Only navigate if login was successful navigate(from, { replace: true }); } catch (error) { console.error("Login failed:", error); + // Stay on login page to show error message + // The error will be displayed via the loginError state from useAuth hook } }; @@ -89,6 +95,12 @@ function Login() { onChange={(e) => setUsername(e.target.value)} onFocus={() => setUsernameFocused(true)} onBlur={() => setUsernameFocused(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleCredentialLogin(); + } + }} className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`} /> @@ -101,6 +113,12 @@ function Login() { onChange={(e) => setPassword(e.target.value)} onFocus={() => setPasswordFocused(true)} onBlur={() => setPasswordFocused(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleCredentialLogin(); + } + }} className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`} />