feat: added speech integration prototype
This commit is contained in:
parent
0bd60916c9
commit
9fc33c7c73
42 changed files with 4971 additions and 124 deletions
1
public/logos/spitch-logo.svg
Normal file
1
public/logos/spitch-logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="60" fill="none" xmlns:v="https://vecta.io/nano"><g clip-path="url(#A)" fill="#fffefd"><path d="M9.001 28.103c-3.277-.84-4.094-1.246-4.094-2.492v-.055c0-.923.84-1.657 2.442-1.657s3.249.706 4.929 1.869l2.169-3.143c-1.92-1.546-4.278-2.414-7.043-2.414-3.877 0-6.637 2.275-6.637 5.718v.055c0 3.766 2.465 4.823 6.286 5.802 3.171.812 3.822 1.357 3.822 2.414v.055c0 1.112-1.029 1.791-2.737 1.791-2.169 0-3.955-.895-5.663-2.303L.01 36.697c2.275 2.031 5.174 3.032 8.049 3.032 4.094 0 6.965-2.114 6.965-5.88v-.055c0-3.305-2.169-4.689-6.018-5.691h-.005zm22.582-.923c0 1.625-1.218 2.871-3.305 2.871h-3.309v-5.797h3.226c2.086 0 3.388 1.002 3.388 2.871v.055zm-3.037-6.692h-7.749v18.969h4.172v-5.691h3.171c4.251 0 7.671-2.275 7.671-6.665v-.055c0-3.877-2.737-6.558-7.265-6.558zm16.939 0h-4.172v18.969h4.172V20.488zm4.384 3.664v.18h5.769v15.125h4.172V24.332h5.774v-3.845H49.869v3.665zm27.794 11.783c-3.254 0-5.501-2.709-5.501-5.963v-.055c0-3.249 2.303-5.908 5.501-5.908 1.897 0 3.388.812 4.846 2.142l2.654-3.06c-1.758-1.735-3.905-2.926-7.477-2.926-5.829 0-9.891 4.417-9.891 9.808v.055c0 5.446 4.145 9.757 9.729 9.757 3.66 0 5.825-1.301 7.777-3.388l-2.654-2.681c-1.491 1.352-2.82 2.22-4.985 2.22zm23.746-15.447v7.509h-7.694v-7.509h-4.172v18.969h4.172v-7.615h7.694v7.615h4.172V20.488h-4.172zM56.995 0h-8.571l4.288 7.615L56.995 0zm-8.571 60h8.571l-4.283-7.615L48.424 60z"/></g><defs><clipPath id="A"><path fill="#fff" d="M0 0h105.582v60H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
15
src/api.ts
15
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 '-';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<T = any> {
|
|||
}[];
|
||||
onDelete?: (row: T) => void;
|
||||
onDeleteMultiple?: (rows: T[]) => void;
|
||||
onRefresh?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +66,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
actions = [],
|
||||
onDelete,
|
||||
onDeleteMultiple,
|
||||
onRefresh,
|
||||
className = ''
|
||||
}: FormGeneratorProps<T>) {
|
||||
const { t } = useLanguage();
|
||||
|
|
@ -370,7 +374,24 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
|
||||
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<T extends Record<string, any>>({
|
|||
/>
|
||||
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>{t('formgen.search.placeholder')}</label>
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className={styles.refreshButton}
|
||||
title={t('formgen.refresh.tooltip', 'Refresh data')}
|
||||
disabled={loading}
|
||||
>
|
||||
<span className={styles.refreshIcon}><IoIosRefresh /></span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function MitgliederTable({ className = '' }: MitgliederTableProps) {
|
|||
selectable={false}
|
||||
loading={loading}
|
||||
actions={actions}
|
||||
onRefresh={refetch}
|
||||
className={styles.mitgliederFormGenerator}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,10 +28,34 @@ const PageManager: React.FC<PageManagerProps> = ({ 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<PageManagerProps> = ({ loadingComponent: LoadingComp
|
|||
|
||||
const config = pageConfigs.find(c => c.path === currentPath);
|
||||
|
||||
if (!config || !config.moduleEnabled) {
|
||||
if (!config || !config.moduleEnabled || !checkSpeechAccess(currentPath)) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
onDeactivate?: () => void | Promise<void>;
|
||||
|
|
@ -37,4 +40,5 @@ export interface SidebarItem {
|
|||
icon: IconType;
|
||||
moduleEnabled: boolean;
|
||||
order: number;
|
||||
submenu?: SidebarSubmenuItemData[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ function PromptsTable({ className = '' }: PromptsTableProps) {
|
|||
actions={actions}
|
||||
onDelete={handleDeleteSingle}
|
||||
onDeleteMultiple={handleDeleteMultiple}
|
||||
onRefresh={refetch}
|
||||
className={styles.promptsFormGenerator}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
|
|||
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<SidebarItemProps> = ({
|
|||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Allow normal navigation for the main link
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}>
|
||||
<li className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}>
|
||||
{/* Icon is always present */}
|
||||
{/* Icon - always visible */}
|
||||
{Icon && <Icon className={`${styles.icon} ${isDisabled ? styles.disabledIcon : ''}`} />}
|
||||
|
||||
{/* Text content - always present but hidden when minimized */}
|
||||
{hasSubItems ? (
|
||||
<a
|
||||
href="#"
|
||||
onClick={toggleSubmenu}
|
||||
className={`${styles.menuLink} ${isDisabled ? styles.disabledLink : ''}`}
|
||||
aria-disabled={isDisabled}
|
||||
title={isDisabled ? `${item.name} (Module disabled)` : item.name}
|
||||
>
|
||||
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
|
||||
{item.name}
|
||||
</span>
|
||||
<IoIosArrowDown className={`${styles.hassubmenu} ${isOpen ? styles.rotated : ''} ${isDisabled ? styles.disabledArrow : ''}`} />
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to={isDisabled ? "#" : (item.link || "#")}
|
||||
className={`${styles.menuLink} ${isDisabled ? styles.disabledLink : ''}`}
|
||||
onClick={handleLinkClick}
|
||||
aria-disabled={isDisabled}
|
||||
title={isDisabled ? `${item.name} (Module disabled)` : item.name}
|
||||
>
|
||||
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
{/* Text and arrow - hidden when minimized */}
|
||||
{!isMinimized && (
|
||||
<>
|
||||
<Link
|
||||
to={isDisabled ? "#" : (item.link || "#")}
|
||||
className={`${styles.menuTextLink} ${isDisabled ? styles.disabledLink : ''}`}
|
||||
onClick={handleLinkClick}
|
||||
aria-disabled={isDisabled}
|
||||
title={isDisabled ? `${item.name} (Module disabled)` : item.name}
|
||||
>
|
||||
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Arrow button separate from link */}
|
||||
{hasSubItems && (
|
||||
<button
|
||||
onClick={toggleSubmenu}
|
||||
className={`${styles.arrowButton} ${isDisabled ? styles.disabledArrow : ''}`}
|
||||
aria-disabled={isDisabled}
|
||||
title={isDisabled ? `${item.name} (Module disabled)` : `Toggle ${item.name} submenu`}
|
||||
>
|
||||
<IoIosArrowDown className={`${styles.hassubmenu} ${isOpen ? styles.rotated : ''} ${isDisabled ? styles.disabledArrow : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
331
src/components/Speech/SpeechConfirmation.module.css
Normal file
331
src/components/Speech/SpeechConfirmation.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
170
src/components/Speech/SpeechConfirmation.tsx
Normal file
170
src/components/Speech/SpeechConfirmation.tsx
Normal file
|
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.iconContainer}>
|
||||
<IoIosCheckmarkCircle className={styles.successIcon} />
|
||||
</div>
|
||||
|
||||
<h1 className={styles.title}>{t('speech.confirmation.title')}</h1>
|
||||
|
||||
<p className={styles.message}>
|
||||
{t('speech.confirmation.message')}
|
||||
</p>
|
||||
|
||||
{submittedData && (
|
||||
<div className={styles.submittedData}>
|
||||
<h3>{t('speech.confirmation.submitted_data')}</h3>
|
||||
<div className={styles.dataGrid}>
|
||||
<div className={styles.dataItem}>
|
||||
<strong>{t('speech.confirmation.company')}:</strong>
|
||||
<span>{submittedData.mandate_general.company_name}</span>
|
||||
</div>
|
||||
<div className={styles.dataItem}>
|
||||
<strong>{t('speech.confirmation.industry')}:</strong>
|
||||
<span>{submittedData.mandate_general.industry}</span>
|
||||
</div>
|
||||
<div className={styles.dataItem}>
|
||||
<strong>{t('speech.confirmation.email')}:</strong>
|
||||
<span>{submittedData.mandate_general.contact_info.email}</span>
|
||||
</div>
|
||||
<div className={styles.dataItem}>
|
||||
<strong>{t('speech.confirmation.phone')}:</strong>
|
||||
<span>{submittedData.mandate_general.contact_info.phone}</span>
|
||||
</div>
|
||||
<div className={styles.dataItem}>
|
||||
<strong>{t('speech.confirmation.address')}:</strong>
|
||||
<span>
|
||||
{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}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.dataItem}>
|
||||
<strong>{t('speech.confirmation.timezone')}:</strong>
|
||||
<span>{submittedData.mandate_general.timezone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.nextSteps}>
|
||||
<h2>{t('speech.confirmation.next_steps')}</h2>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepIcon}>
|
||||
<IoIosMail className={styles.stepIconInner} />
|
||||
</div>
|
||||
<div className={styles.stepContent}>
|
||||
<h3>{t('speech.confirmation.email_confirmation')}</h3>
|
||||
<p>{t('speech.confirmation.email_confirmation_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepIcon}>
|
||||
<IoIosTime className={styles.stepIconInner} />
|
||||
</div>
|
||||
<div className={styles.stepContent}>
|
||||
<h3>{t('speech.confirmation.review_process')}</h3>
|
||||
<p>{t('speech.confirmation.review_process_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepIcon}>
|
||||
<IoIosCall className={styles.stepIconInner} />
|
||||
</div>
|
||||
<div className={styles.stepContent}>
|
||||
<h3>{t('speech.confirmation.setup_call')}</h3>
|
||||
<p>{t('speech.confirmation.setup_call_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.contactInfo}>
|
||||
<h3>{t('speech.confirmation.questions')}</h3>
|
||||
<p>
|
||||
{t('speech.confirmation.questions_desc')}
|
||||
</p>
|
||||
<div className={styles.contactMethods}>
|
||||
<div className={styles.contactMethod}>
|
||||
<IoIosMail className={styles.contactIcon} />
|
||||
<span>support@spitch.ai</span>
|
||||
</div>
|
||||
<div className={styles.contactMethod}>
|
||||
<IoIosCall className={styles.contactIcon} />
|
||||
<span>+1 (555) 123-4567</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
className={`${sharedStyles.secondaryButton} ${styles.backButton}`}
|
||||
onClick={onBackToInfo}
|
||||
>
|
||||
{t('speech.confirmation.back')}
|
||||
</button>
|
||||
{onReset && (
|
||||
<button
|
||||
className={`${sharedStyles.primaryButton} ${styles.resetButton}`}
|
||||
onClick={onReset}
|
||||
>
|
||||
<IoIosRefresh className={styles.resetIcon} />
|
||||
{t('speech.confirmation.reset')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.additionalActions}>
|
||||
<button
|
||||
className={`${sharedStyles.primaryButton} ${styles.transcriptButton}`}
|
||||
onClick={() => navigate('/speech/transcripts')}
|
||||
>
|
||||
{t('speech.confirmation.transcript_management')}
|
||||
</button>
|
||||
<button
|
||||
className={`${sharedStyles.secondaryButton} ${styles.settingsButton}`}
|
||||
onClick={() => navigate('/einstellungen#speech-settings')}
|
||||
>
|
||||
{t('speech.confirmation.speech_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeechConfirmation;
|
||||
283
src/components/Speech/SpeechInfo.module.css
Normal file
283
src/components/Speech/SpeechInfo.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
112
src/components/Speech/SpeechInfo.tsx
Normal file
112
src/components/Speech/SpeechInfo.tsx
Normal file
|
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.features}>
|
||||
{features.map((feature) => (
|
||||
<div
|
||||
key={feature.key}
|
||||
className={`${styles.feature}`}
|
||||
style={{ '--feature-color': feature.color } as React.CSSProperties}
|
||||
>
|
||||
<div className={styles.featureIconContainer}>
|
||||
<span className={styles.featureAcronym}>{feature.key.toUpperCase()}</span>
|
||||
</div>
|
||||
<h3 className={styles.featureTitle}>{feature.title}</h3>
|
||||
<p className={styles.featureDescription}>{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.partnerInfo}>
|
||||
<h2>{t('speech.info.about')}</h2>
|
||||
<p className={styles.introText}>
|
||||
{t('speech.info.about_intro')}
|
||||
</p>
|
||||
|
||||
<div className={styles.featureSection}>
|
||||
<div className={styles.featureItem}>
|
||||
<h3>🎯 {t('speech.info.workflow_title')}</h3>
|
||||
<p>{t('speech.info.workflow_description')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.featureItem}>
|
||||
<h3>🤖 {t('speech.info.ai_title')}</h3>
|
||||
<p>{t('speech.info.ai_description')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.featureItem}>
|
||||
<h3>🔄 {t('speech.info.sync_title')}</h3>
|
||||
<p>{t('speech.info.sync_description')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.featureItem}>
|
||||
<h3>💰 {t('speech.info.cost_title')}</h3>
|
||||
<p>{t('speech.info.cost_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.partnerLink}>
|
||||
<IoIosLink className={styles.linkIcon} />
|
||||
<a href="https://spitch.ai" target="_blank" rel="noopener noreferrer">
|
||||
{t('speech.info.about_link')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeechInfo;
|
||||
225
src/components/Speech/SpeechSettings.module.css
Normal file
225
src/components/Speech/SpeechSettings.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
482
src/components/Speech/SpeechSettings.tsx
Normal file
482
src/components/Speech/SpeechSettings.tsx
Normal file
|
|
@ -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<MandateData | null>(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<Set<string>>(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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.loading}>
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!formData) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.noData}>
|
||||
<p>{t('speech.settings.no_data')}</p>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={() => window.location.href = '/speech'}
|
||||
>
|
||||
{t('speech.settings.sign_up_now')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>{t('speech.settings.title')}</h2>
|
||||
<p className={styles.description}>{t('speech.settings.description')}</p>
|
||||
</div>
|
||||
|
||||
{saveMessage && (
|
||||
<div className={`${styles.message} ${saveMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
|
||||
{saveMessage.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.form}>
|
||||
{/* Company Information Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<IoIosBusiness className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>{t('speech.settings.company_info')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="company_name"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.company_name}
|
||||
onChange={(e) => handleInputChange('mandate_general.company_name', e.target.value)}
|
||||
onFocus={() => handleFocus('company_name')}
|
||||
onBlur={() => handleBlur('company_name')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="company_name"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.company_name || focusedFields.has('company_name') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.company_name')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="industry"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.industry}
|
||||
onChange={(e) => handleInputChange('mandate_general.industry', e.target.value)}
|
||||
onFocus={() => handleFocus('industry')}
|
||||
onBlur={() => handleBlur('industry')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="industry"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.industry || focusedFields.has('industry') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.industry')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<IoIosContact className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>{t('speech.settings.contact_info')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.email}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.email', e.target.value)}
|
||||
onFocus={() => handleFocus('email')}
|
||||
onBlur={() => handleBlur('email')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.contact_info.email || focusedFields.has('email') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.email')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.phone}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.phone', e.target.value)}
|
||||
onFocus={() => handleFocus('phone')}
|
||||
onBlur={() => handleBlur('phone')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.contact_info.phone || focusedFields.has('phone') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.phone')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="street"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.street}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.street', e.target.value)}
|
||||
onFocus={() => handleFocus('street')}
|
||||
onBlur={() => handleBlur('street')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="street"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.contact_info.street || focusedFields.has('street') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.street')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="postal_code"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.postal_code}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.postal_code', e.target.value)}
|
||||
onFocus={() => handleFocus('postal_code')}
|
||||
onBlur={() => handleBlur('postal_code')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="postal_code"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.contact_info.postal_code || focusedFields.has('postal_code') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.postal_code')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="city"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.city}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.city', e.target.value)}
|
||||
onFocus={() => handleFocus('city')}
|
||||
onBlur={() => handleBlur('city')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="city"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.contact_info.city || focusedFields.has('city') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.city')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="country"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.country}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.country', e.target.value)}
|
||||
onFocus={() => handleFocus('country')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="country"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.contact_info.country || focusedFields.has('country') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.country')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Business Hours Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<IoIosTime className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>{t('speech.settings.business_hours')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
type="text"
|
||||
id="business_hours"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.business_hours}
|
||||
onChange={(e) => handleInputChange('mandate_general.business_hours', e.target.value)}
|
||||
onFocus={() => handleFocus('business_hours')}
|
||||
onBlur={() => handleBlur('business_hours')}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
htmlFor="business_hours"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.business_hours || focusedFields.has('business_hours') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.business_hours')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.inputContainer}>
|
||||
<select
|
||||
id="timezone"
|
||||
className={styles.formSelect}
|
||||
value={formData.mandate_general.timezone}
|
||||
onChange={(e) => handleInputChange('mandate_general.timezone', e.target.value)}
|
||||
onFocus={() => handleFocus('timezone')}
|
||||
onBlur={() => handleBlur('timezone')}
|
||||
required
|
||||
>
|
||||
<option value="">{t('speech.signup.select_timezone')}</option>
|
||||
<option value="UTC-12">UTC-12 (Baker Island)</option>
|
||||
<option value="UTC-11">UTC-11 (American Samoa)</option>
|
||||
<option value="UTC-10">UTC-10 (Hawaii)</option>
|
||||
<option value="UTC-9">UTC-9 (Alaska)</option>
|
||||
<option value="UTC-8">UTC-8 (Pacific Time)</option>
|
||||
<option value="UTC-7">UTC-7 (Mountain Time)</option>
|
||||
<option value="UTC-6">UTC-6 (Central Time)</option>
|
||||
<option value="UTC-5">UTC-5 (Eastern Time)</option>
|
||||
<option value="UTC-4">UTC-4 (Atlantic Time)</option>
|
||||
<option value="UTC-3">UTC-3 (Brazil)</option>
|
||||
<option value="UTC-2">UTC-2 (Mid-Atlantic)</option>
|
||||
<option value="UTC-1">UTC-1 (Azores)</option>
|
||||
<option value="UTC+0">UTC+0 (Greenwich Mean Time)</option>
|
||||
<option value="UTC+1">UTC+1 (Central European Time)</option>
|
||||
<option value="UTC+2">UTC+2 (Eastern European Time)</option>
|
||||
<option value="UTC+3">UTC+3 (Moscow Time)</option>
|
||||
<option value="UTC+4">UTC+4 (Gulf Standard Time)</option>
|
||||
<option value="UTC+5">UTC+5 (Pakistan Standard Time)</option>
|
||||
<option value="UTC+6">UTC+6 (Bangladesh Standard Time)</option>
|
||||
<option value="UTC+7">UTC+7 (Indochina Time)</option>
|
||||
<option value="UTC+8">UTC+8 (China Standard Time)</option>
|
||||
<option value="UTC+9">UTC+9 (Japan Standard Time)</option>
|
||||
<option value="UTC+10">UTC+10 (Australian Eastern Time)</option>
|
||||
<option value="UTC+11">UTC+11 (Solomon Islands)</option>
|
||||
<option value="UTC+12">UTC+12 (New Zealand)</option>
|
||||
</select>
|
||||
<label
|
||||
htmlFor="timezone"
|
||||
className={`${styles.floatingLabel} ${formData.mandate_general.timezone || focusedFields.has('timezone') ? styles.floatingLabelActive : ''}`}
|
||||
>
|
||||
{t('speech.signup.timezone')} *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
className={sharedStyles.secondaryButton}
|
||||
onClick={handleReset}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<IoIosRefresh className={styles.resetIcon} />
|
||||
{t('speech.settings.reset')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? t('speech.settings.saving') : t('speech.settings.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeechSettings;
|
||||
384
src/components/Speech/SpeechSignUp.module.css
Normal file
384
src/components/Speech/SpeechSignUp.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
342
src/components/Speech/SpeechSignUp.tsx
Normal file
342
src/components/Speech/SpeechSignUp.tsx
Normal file
|
|
@ -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<MandateData>({
|
||||
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<Record<string, string>>({});
|
||||
const [focusedFields, setFocusedFields] = useState<Set<string>>(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<string, string> = {};
|
||||
|
||||
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 (
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => handleInputChange(field, e.target.value)}
|
||||
onFocus={() => handleFocus(field)}
|
||||
onBlur={() => handleBlur(field)}
|
||||
className={`${styles.input} ${hasError ? styles.inputError : ''}`}
|
||||
placeholder=""
|
||||
/>
|
||||
<label className={
|
||||
isFocused || hasValue
|
||||
? hasError
|
||||
? styles.focusedLabel
|
||||
: styles.activeFocusedLabel
|
||||
: styles.floatingLabel
|
||||
}>
|
||||
{icon}
|
||||
{placeholder}
|
||||
</label>
|
||||
{hasError && <span className={styles.errorText}>{hasError}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<button
|
||||
className={styles.backButton}
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
>
|
||||
<IoIosArrowBack className={styles.backIcon} />
|
||||
{t('speech.signup.back')}
|
||||
</button>
|
||||
<h1 className={styles.title}>{t('speech.signup.title')}</h1>
|
||||
<p className={styles.subtitle}>
|
||||
{t('speech.signup.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<IoIosBusiness className={styles.sectionIcon} />
|
||||
{t('speech.signup.company_info')}
|
||||
</h2>
|
||||
|
||||
<div className={styles.formGrid}>
|
||||
{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',
|
||||
|
||||
)}
|
||||
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<select
|
||||
value={formData.mandate_general.timezone}
|
||||
onChange={(e) => handleInputChange('mandate_general.timezone', e.target.value)}
|
||||
onFocus={() => handleFocus('mandate_general.timezone')}
|
||||
onBlur={() => handleBlur('mandate_general.timezone')}
|
||||
className={styles.input}
|
||||
>
|
||||
<option value="Europe/Zurich">Europe/Zurich</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
</select>
|
||||
<label className={
|
||||
focusedFields.has('mandate_general.timezone') || formData.mandate_general.timezone.trim() !== ''
|
||||
? styles.activeFocusedLabel
|
||||
: styles.floatingLabel
|
||||
}>
|
||||
{t('speech.signup.timezone')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
|
||||
{t('speech.signup.contact_info')}
|
||||
</h2>
|
||||
|
||||
<div className={styles.formGrid}>
|
||||
{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')} *`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<IoIosPeople className={styles.sectionIcon} />
|
||||
{t('speech.signup.contacts_setup')}
|
||||
</h2>
|
||||
<div className={styles.contactsSection}>
|
||||
<p className={styles.contactsDescription}>
|
||||
{t('speech.signup.contacts_description')}
|
||||
</p>
|
||||
<div className={styles.contactsActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${sharedStyles.secondaryButton} ${styles.skipButton}`}
|
||||
onClick={() => handleInputChange('setup_contacts', 'false')}
|
||||
>
|
||||
{t('speech.signup.skip_for_now')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${sharedStyles.primaryButton} ${styles.setupButton}`}
|
||||
onClick={() => handleInputChange('setup_contacts', 'true')}
|
||||
>
|
||||
{t('speech.signup.setup_contacts')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${sharedStyles.secondaryButton} ${styles.cancelButton}`}
|
||||
onClick={onBack}
|
||||
>
|
||||
{t('speech.signup.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={`${sharedStyles.primaryButton} ${styles.submitButton}`}
|
||||
>
|
||||
{t('speech.signup.submit')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeechSignUp;
|
||||
4
src/components/Speech/index.ts
Normal file
4
src/components/Speech/index.ts
Normal file
|
|
@ -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';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 '-';
|
||||
|
|
|
|||
290
src/components/settings/settingsSpeech.module.css
Normal file
290
src/components/settings/settingsSpeech.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
419
src/components/settings/settingsSpeech.tsx
Normal file
419
src/components/settings/settingsSpeech.tsx
Normal file
|
|
@ -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<MandateData | null>(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<Set<string>>(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 (
|
||||
<div className={styles.speechSettingsForm}>
|
||||
<div className={styles.loading}>
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!formData) {
|
||||
return (
|
||||
<div className={styles.speechSettingsForm}>
|
||||
<div className={styles.noData}>
|
||||
<p>{t('speech.settings.no_data')}</p>
|
||||
<button
|
||||
className={styles.signUpButton}
|
||||
onClick={() => navigate('/speech')}
|
||||
>
|
||||
{t('speech.settings.sign_up_now')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.speechSettingsForm}>
|
||||
<span className={styles.settingLabel}>{t('speech.settings.title')}</span>
|
||||
<span className={styles.settingDescription}>{t('speech.settings.description')}</span>
|
||||
|
||||
{saveMessage && (
|
||||
<div className={`${styles.updateMessage} ${saveMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
|
||||
{saveMessage.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Company Information Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<IoIosBusiness className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>{t('speech.settings.company_info')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.company_name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.company_name}
|
||||
onChange={(e) => handleInputChange('mandate_general.company_name', e.target.value)}
|
||||
onFocus={() => handleFocus('company_name')}
|
||||
onBlur={() => handleBlur('company_name')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.industry')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.industry}
|
||||
onChange={(e) => handleInputChange('mandate_general.industry', e.target.value)}
|
||||
onFocus={() => handleFocus('industry')}
|
||||
onBlur={() => handleBlur('industry')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<IoIosContact className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>{t('speech.settings.contact_info')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.email')} *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.email}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.email', e.target.value)}
|
||||
onFocus={() => handleFocus('email')}
|
||||
onBlur={() => handleBlur('email')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.phone')} *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.phone}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.phone', e.target.value)}
|
||||
onFocus={() => handleFocus('phone')}
|
||||
onBlur={() => handleBlur('phone')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.street')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.street}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.street', e.target.value)}
|
||||
onFocus={() => handleFocus('street')}
|
||||
onBlur={() => handleBlur('street')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.postal_code')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.postal_code}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.postal_code', e.target.value)}
|
||||
onFocus={() => handleFocus('postal_code')}
|
||||
onBlur={() => handleBlur('postal_code')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.city')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.city}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.city', e.target.value)}
|
||||
onFocus={() => handleFocus('city')}
|
||||
onBlur={() => handleBlur('city')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.country')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.contact_info.country}
|
||||
onChange={(e) => handleInputChange('mandate_general.contact_info.country', e.target.value)}
|
||||
onFocus={() => handleFocus('country')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Business Hours Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<IoIosTime className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>{t('speech.settings.business_hours')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.business_hours')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={formData.mandate_general.business_hours}
|
||||
onChange={(e) => handleInputChange('mandate_general.business_hours', e.target.value)}
|
||||
onFocus={() => handleFocus('business_hours')}
|
||||
onBlur={() => handleBlur('business_hours')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('speech.signup.timezone')} *
|
||||
</label>
|
||||
<select
|
||||
className={styles.formSelect}
|
||||
value={formData.mandate_general.timezone}
|
||||
onChange={(e) => handleInputChange('mandate_general.timezone', e.target.value)}
|
||||
onFocus={() => handleFocus('timezone')}
|
||||
onBlur={() => handleBlur('timezone')}
|
||||
required
|
||||
>
|
||||
<option value="">{t('speech.signup.select_timezone')}</option>
|
||||
<option value="UTC-12">UTC-12 (Baker Island)</option>
|
||||
<option value="UTC-11">UTC-11 (American Samoa)</option>
|
||||
<option value="UTC-10">UTC-10 (Hawaii)</option>
|
||||
<option value="UTC-9">UTC-9 (Alaska)</option>
|
||||
<option value="UTC-8">UTC-8 (Pacific Time)</option>
|
||||
<option value="UTC-7">UTC-7 (Mountain Time)</option>
|
||||
<option value="UTC-6">UTC-6 (Central Time)</option>
|
||||
<option value="UTC-5">UTC-5 (Eastern Time)</option>
|
||||
<option value="UTC-4">UTC-4 (Atlantic Time)</option>
|
||||
<option value="UTC-3">UTC-3 (Brazil)</option>
|
||||
<option value="UTC-2">UTC-2 (Mid-Atlantic)</option>
|
||||
<option value="UTC-1">UTC-1 (Azores)</option>
|
||||
<option value="UTC+0">UTC+0 (Greenwich Mean Time)</option>
|
||||
<option value="UTC+1">UTC+1 (Central European Time)</option>
|
||||
<option value="UTC+2">UTC+2 (Eastern European Time)</option>
|
||||
<option value="UTC+3">UTC+3 (Moscow Time)</option>
|
||||
<option value="UTC+4">UTC+4 (Gulf Standard Time)</option>
|
||||
<option value="UTC+5">UTC+5 (Pakistan Standard Time)</option>
|
||||
<option value="UTC+6">UTC+6 (Bangladesh Standard Time)</option>
|
||||
<option value="UTC+7">UTC+7 (Indochina Time)</option>
|
||||
<option value="UTC+8">UTC+8 (China Standard Time)</option>
|
||||
<option value="UTC+9">UTC+9 (Japan Standard Time)</option>
|
||||
<option value="UTC+10">UTC+10 (Australian Eastern Time)</option>
|
||||
<option value="UTC+11">UTC+11 (Solomon Islands)</option>
|
||||
<option value="UTC+12">UTC+12 (New Zealand)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className={styles.formActions}>
|
||||
<button
|
||||
className={styles.resetButton}
|
||||
onClick={handleReset}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<IoIosRefresh className={styles.resetIcon} />
|
||||
{t('speech.settings.reset')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.saveButton}
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? t('speech.settings.saving') : t('speech.settings.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsSpeech;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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<User | null>(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() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Speech Integration Settings */}
|
||||
<div id="speech-settings" className={styles.speechSettingsSection}>
|
||||
{hasSpeechIntegration ? (
|
||||
<SettingsSpeech />
|
||||
) : (
|
||||
<div className={styles.noSpeechData}>
|
||||
<p>{t('speech.settings.no_data')}</p>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={() => navigate('/speech')}
|
||||
>
|
||||
{t('speech.settings.sign_up_now')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
52
src/pages/Home/HomeStyles/Speech.module.css
Normal file
52
src/pages/Home/HomeStyles/Speech.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
293
src/pages/Home/HomeStyles/SpeechTranscripts.module.css
Normal file
293
src/pages/Home/HomeStyles/SpeechTranscripts.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
160
src/pages/Home/Speech.tsx
Normal file
160
src/pages/Home/Speech.tsx
Normal file
|
|
@ -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<SpeechPage>('info');
|
||||
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false);
|
||||
const [submittedData, setSubmittedData] = useState<MandateData | null>(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 (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('speech.title')}</h1>
|
||||
{currentPage === 'info' && !hasSubmitted && (
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={handleSignUp}
|
||||
>
|
||||
{t('speech.signup.button')}
|
||||
</button>
|
||||
)}
|
||||
{hasSubmitted && (
|
||||
<div className={styles.submittedStatus}>
|
||||
<span className={styles.statusText}>
|
||||
{t('speech.status.submitted')}
|
||||
</span>
|
||||
<button
|
||||
className={styles.resetButton}
|
||||
onClick={handleResetSignUp}
|
||||
>
|
||||
{t('speech.status.reset')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={sharedStyles.pageSubtitle}>
|
||||
<span>{t('speech.subtitle')}</span>
|
||||
<img src={SpitchLogo} alt="Spitch.ai" className={styles.partnerLogo} />
|
||||
</div>
|
||||
{currentPage === 'info' && (
|
||||
<SpeechInfo />
|
||||
)}
|
||||
{currentPage === 'signup' && (
|
||||
<SpeechSignUp
|
||||
onBack={handleBackToInfo}
|
||||
onSubmit={handleSignUpSubmit}
|
||||
/>
|
||||
)}
|
||||
{currentPage === 'confirmation' && (
|
||||
<SpeechConfirmation
|
||||
onBackToInfo={handleBackToInfo}
|
||||
submittedData={submittedData}
|
||||
onReset={handleResetSignUp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Speech;
|
||||
262
src/pages/Home/SpeechTranscripts.tsx
Normal file
262
src/pages/Home/SpeechTranscripts.tsx
Normal file
|
|
@ -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<CallTranscript[]>([]);
|
||||
const [hasSignedUp, setHasSignedUp] = useState<boolean>(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 (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('speech.transcripts.title')}</h1>
|
||||
</div>
|
||||
<div className={sharedStyles.pageContent}>
|
||||
<h2>{t('speech.transcripts.access_denied_title')}</h2>
|
||||
<p>{t('speech.transcripts.access_denied_message')}</p>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={() => window.location.href = '/speech'}
|
||||
>
|
||||
{t('speech.transcripts.sign_up_now')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('speech.transcripts.title')}</h1>
|
||||
</div>
|
||||
|
||||
<FormGenerator
|
||||
data={transcripts}
|
||||
columns={columns}
|
||||
title={t('speech.transcripts.recent_transcripts')}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
resizable={true}
|
||||
pagination={true}
|
||||
pageSize={10}
|
||||
selectable={true}
|
||||
onRowClick={(transcript) => {
|
||||
// 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: <IoIosEye />
|
||||
},
|
||||
{
|
||||
label: t('speech.transcripts.download', 'Download'),
|
||||
onClick: (transcript) => {
|
||||
// Handle download action
|
||||
console.log('Download transcript:', transcript);
|
||||
},
|
||||
icon: <IoIosDownload />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeechTranscripts;
|
||||
|
|
@ -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 : ''}`}
|
||||
/>
|
||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
||||
|
|
@ -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 : ''}`}
|
||||
/>
|
||||
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>Passwort</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue