313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import api from '../api';
|
|
import OnboardingWizard from './OnboardingWizard';
|
|
|
|
import { useLanguage } from '../providers/language/LanguageContext';
|
|
|
|
interface OnboardingStep {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
completed: boolean;
|
|
action?: () => void;
|
|
}
|
|
|
|
interface OnboardingAssistantProps {
|
|
onDismiss?: () => void;
|
|
}
|
|
|
|
const _STORAGE_KEY = 'onboarding_hidden';
|
|
|
|
export function _isOnboardingHidden(): boolean {
|
|
try {
|
|
return localStorage.getItem(_STORAGE_KEY) === 'true';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function _showOnboarding(): void {
|
|
try {
|
|
localStorage.removeItem(_STORAGE_KEY);
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
function _hideOnboarding(): void {
|
|
try {
|
|
localStorage.setItem(_STORAGE_KEY, 'true');
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
|
|
const { t, currentLanguage } = useLanguage();
|
|
const callouts = useMemo(() => ({
|
|
mandate: t('Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.'),
|
|
feature: t('Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.'),
|
|
connection: t('Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.'),
|
|
chat: t('Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.'),
|
|
}), [t]);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [hidden, setHidden] = useState(() => _isOnboardingHidden());
|
|
const [steps, setSteps] = useState<OnboardingStep[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dontShowAgain, setDontShowAgain] = useState(false);
|
|
const [showWizard, setShowWizard] = useState(false);
|
|
|
|
const _checkOnboardingState = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const onboardingSteps: OnboardingStep[] = [];
|
|
|
|
// Check admin mandates (user-owned or where user is admin)
|
|
let hasAdminMandate = false;
|
|
try {
|
|
const mandatesRes = await api.get('/api/store/mandates');
|
|
const mandates = mandatesRes.data?.mandates || mandatesRes.data || [];
|
|
hasAdminMandate = Array.isArray(mandates) && mandates.length > 0;
|
|
} catch { /* ignore */ }
|
|
|
|
// Check if user has any feature access (via navigation = mandate member)
|
|
let hasFeature = false;
|
|
let workspaceInstancePath: string | undefined;
|
|
let workspaceInstanceIds: string[] = [];
|
|
try {
|
|
const navRes = await api.get(`/api/navigation?language=${currentLanguage}`);
|
|
const blocks = navRes.data?.blocks || [];
|
|
const dynamicBlock = blocks.find((b: { type: string }) => b.type === 'dynamic');
|
|
const mandates = dynamicBlock?.mandates || [];
|
|
for (const m of mandates) {
|
|
for (const f of m.features || []) {
|
|
for (const inst of f.instances || []) {
|
|
hasFeature = true;
|
|
if (f.uiComponent === 'feature.workspace' && inst.views?.length > 0) {
|
|
workspaceInstanceIds.push(inst.id);
|
|
if (!workspaceInstancePath) {
|
|
workspaceInstancePath = inst.views[0].uiPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
const mandateStepDone = hasAdminMandate || hasFeature;
|
|
|
|
onboardingSteps.push({
|
|
id: 'mandate',
|
|
label: t('Mandat einrichten'),
|
|
description: hasAdminMandate
|
|
? t('Dein Mandant ist eingerichtet.')
|
|
: hasFeature
|
|
? t('Du bist Mitglied eines Mandanten')
|
|
: t('Erstelle deinen Arbeitsbereich'),
|
|
completed: mandateStepDone,
|
|
action: mandateStepDone ? undefined : () => setShowWizard(true),
|
|
});
|
|
|
|
onboardingSteps.push({
|
|
id: 'feature',
|
|
label: t('Aktiviere dein erstes Feature'),
|
|
description: hasFeature
|
|
? t('Du hast aktive Features')
|
|
: t('Aktiviere dein erstes Feature im'),
|
|
completed: hasFeature,
|
|
action: hasFeature ? undefined : () => navigate('/store'),
|
|
});
|
|
|
|
let hasConnection = false;
|
|
try {
|
|
const connRes = await api.get('/api/connections/');
|
|
const items = connRes.data?.items || connRes.data?.data || connRes.data || [];
|
|
hasConnection = Array.isArray(items) && items.length > 0;
|
|
} catch { /* ignore */ }
|
|
|
|
onboardingSteps.push({
|
|
id: 'connection',
|
|
label: t('Verbinde deine erste Datenquelle'),
|
|
description: hasConnection
|
|
? t('Du hast Verbindungen eingerichtet')
|
|
: t('Verbinde deine erste Datenquelle'),
|
|
completed: hasConnection,
|
|
action: hasConnection ? undefined : () => navigate('/basedata/connections'),
|
|
});
|
|
|
|
let hasChat = false;
|
|
for (const instId of workspaceInstanceIds) {
|
|
if (hasChat) break;
|
|
try {
|
|
const wfRes = await api.get(`/api/workspace/${instId}/workflows`);
|
|
const wfs = wfRes.data?.workflows || wfRes.data?.data || wfRes.data?.items || [];
|
|
if (Array.isArray(wfs) && wfs.length > 0) hasChat = true;
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
const chatAction = workspaceInstancePath ? () => navigate(workspaceInstancePath!) : undefined;
|
|
onboardingSteps.push({
|
|
id: 'chat',
|
|
label: t('Starte deinen ersten AI-Chat'),
|
|
description: hasChat
|
|
? t('Du hast bereits Chats gestartet')
|
|
: t('Starte deinen ersten Chat mit'),
|
|
completed: hasChat,
|
|
action: hasChat ? undefined : chatAction,
|
|
});
|
|
|
|
setSteps(onboardingSteps);
|
|
|
|
if (onboardingSteps.every(s => s.completed)) {
|
|
setHidden(true);
|
|
_hideOnboarding();
|
|
}
|
|
} catch (err) {
|
|
console.error('Onboarding check failed:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [navigate, t, currentLanguage]);
|
|
|
|
useEffect(() => {
|
|
const state = location.state as { showOnboarding?: number } | null;
|
|
if (state?.showOnboarding) {
|
|
setHidden(false);
|
|
window.history.replaceState({}, '');
|
|
}
|
|
}, [location.state]);
|
|
|
|
useEffect(() => {
|
|
if (!hidden) _checkOnboardingState();
|
|
}, [hidden, _checkOnboardingState]);
|
|
|
|
const _handleDismiss = () => {
|
|
if (dontShowAgain) {
|
|
_hideOnboarding();
|
|
}
|
|
setHidden(true);
|
|
onDismiss?.();
|
|
};
|
|
|
|
if (showWizard) {
|
|
return (
|
|
<OnboardingWizard
|
|
onComplete={() => {
|
|
setShowWizard(false);
|
|
_checkOnboardingState();
|
|
}}
|
|
onDismiss={() => setShowWizard(false)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (hidden || loading) return null;
|
|
|
|
const completedCount = steps.filter(s => s.completed).length;
|
|
if (completedCount === steps.length) return null;
|
|
|
|
return (
|
|
<div style={{
|
|
padding: 16, margin: '0 0 20px 0', borderRadius: 12,
|
|
border: '1px solid var(--border-color, #e5e7eb)',
|
|
background: 'linear-gradient(135deg, var(--bg-primary, #fff) 0%, #eef2ff 100%)',
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
|
|
<div>
|
|
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('Willkommen bei Poweron')}</h3>
|
|
<p style={{ margin: '4px 0 0', fontSize: '0.85rem', color: 'var(--text-secondary, #6b7280)' }}>
|
|
{t('{completed} von {total} Schritten abgeschlossen', { completed: String(completedCount), total: String(steps.length) })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 4, marginBottom: 16, height: 4 }}>
|
|
{steps.map((step) => (
|
|
<div
|
|
key={step.id}
|
|
style={{
|
|
flex: 1, borderRadius: 2, height: 4,
|
|
background: step.completed ? 'var(--accent, #4f46e5)' : 'var(--border-color, #e5e7eb)',
|
|
transition: 'background 0.3s',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{steps.map((step, idx) => {
|
|
const isNextStep = !step.completed && steps.slice(0, idx).every(s => s.completed);
|
|
return (
|
|
<div key={step.id}>
|
|
<div
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
|
|
borderRadius: 8, background: step.completed ? 'transparent' : 'var(--bg-primary, #fff)',
|
|
border: step.completed ? 'none' : isNextStep ? '1px solid var(--accent, #4f46e5)' : '1px solid var(--border-color, #e5e7eb)',
|
|
opacity: step.completed ? 0.6 : 1,
|
|
cursor: step.action ? 'pointer' : 'default',
|
|
}}
|
|
onClick={step.action}
|
|
>
|
|
<span style={{ fontSize: '1.1rem', flexShrink: 0, width: 24, textAlign: 'center' }}>
|
|
{step.completed ? '\u2713' : '\u25CB'}
|
|
</span>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontWeight: 500, fontSize: '0.85rem', textDecoration: step.completed ? 'line-through' : 'none' }}>
|
|
{step.label}
|
|
</div>
|
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #6b7280)' }}>
|
|
{step.description}
|
|
</div>
|
|
</div>
|
|
{step.action && !step.completed && (
|
|
<span style={{ fontSize: '0.8rem', color: 'var(--accent, #4f46e5)', fontWeight: 500 }}>{'\u2192'}</span>
|
|
)}
|
|
</div>
|
|
{isNextStep && callouts[step.id as keyof typeof callouts] && (
|
|
<div style={{
|
|
marginTop: 4, marginLeft: 34, padding: '6px 10px',
|
|
fontSize: '0.78rem', color: 'var(--accent, #4f46e5)',
|
|
background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6,
|
|
borderLeft: '3px solid var(--accent, #4f46e5)',
|
|
}}>
|
|
{callouts[step.id as keyof typeof callouts]}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
marginTop: 14, paddingTop: 10,
|
|
borderTop: '1px solid var(--border-color, #e5e7eb)',
|
|
}}>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '0.8rem', color: 'var(--text-secondary, #6b7280)', cursor: 'pointer' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={dontShowAgain}
|
|
onChange={(e) => setDontShowAgain(e.target.checked)}
|
|
style={{ margin: 0 }}
|
|
/>
|
|
{t('Nicht wieder anzeigen')}
|
|
</label>
|
|
<button
|
|
onClick={_handleDismiss}
|
|
style={{
|
|
border: '1px solid var(--border-color, #d1d5db)',
|
|
background: 'transparent',
|
|
cursor: 'pointer',
|
|
fontSize: '0.8rem',
|
|
color: 'var(--text-secondary, #6b7280)',
|
|
padding: '4px 12px',
|
|
borderRadius: 6,
|
|
}}
|
|
>
|
|
{t('Schliessen')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OnboardingAssistant;
|