39333-vm/frontend/src/pages/pulse-hq.tsx
Flatlogic Bot be323ebbec puls
2026-03-26 17:54:46 +00:00

937 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
mdiArrowRight,
mdiBrain,
mdiChartLine,
mdiClipboardTextClockOutline,
mdiCreation,
mdiLightningBolt,
mdiRobotOutline,
mdiShieldCheck,
mdiTarget,
} from '@mdi/js';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import NotificationBar from '../components/NotificationBar';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import { useAppSelector } from '../stores/hooks';
type TemplateSummary = {
key: string;
title: string;
accent: string;
description: string;
defaultCadence: string;
defaultVisibility: string;
questionCount: number;
previewQuestions: Array<{
prompt: string;
question_type: string;
}>;
};
type RecentSurvey = {
id: string;
title: string;
description: string;
status: string;
visibility: string;
cadence: string;
opens_at: string | null;
closes_at: string | null;
createdAt: string;
templateName: string;
questionCount: number;
responseCount: number;
invitationCount: number;
responseRate: number;
latestAnalysisAt: string | null;
latestAnalysisScore: number | null;
};
type LaunchpadSummary = {
overview: {
totalSurveys: number;
activeSurveys: number;
totalResponses: number;
totalInvitations: number;
responseRate: number;
aiReadyCount: number;
activeDepartmentCount: number;
};
templates: TemplateSummary[];
recentSurveys: RecentSurvey[];
responseTrend: Array<{
key: string;
label: string;
count: number;
}>;
departmentBreakdown: Array<{
id: string;
name: string;
responseCount: number;
}>;
};
type AnalysisResult = {
overallSentimentScore: number | null;
executiveSummary: string;
topThemes: Array<{
theme: string;
confidence: string;
quote: string;
insight: string;
}>;
departmentRisks: Array<{
department: string;
riskLevel: string;
riskScore: number;
signal: string;
}>;
recommendedActions: Array<{
title: string;
owner: string;
timeline: string;
template: string;
}>;
};
const cadenceLabels: Record<string, string> = {
one_time: 'One-time',
weekly: 'Weekly',
biweekly: 'Biweekly',
monthly: 'Monthly',
quarterly: 'Quarterly',
};
const visibilityLabels: Record<string, string> = {
anonymous: 'Anonymous',
identified: 'Identified',
};
const statusClassNames: Record<string, string> = {
draft: 'bg-slate-500/15 text-slate-200',
scheduled: 'bg-sky-500/15 text-sky-200',
sending: 'bg-violet-500/15 text-violet-200',
active: 'bg-emerald-500/15 text-emerald-200',
closed: 'bg-amber-500/15 text-amber-200',
archived: 'bg-zinc-500/15 text-zinc-300',
};
const questionTypeLabels: Record<string, string> = {
rating_1_10: '110 rating',
multiple_choice: 'Multiple choice',
open_text: 'Open text',
emoji_reaction: 'Emoji reaction',
};
const getDefaultTitle = (template?: TemplateSummary) => {
if (!template) {
return '';
}
const today = new Date().toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
return `${template.title}${today}`;
};
const PulseHQPage = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const canCreateSurveys = Boolean(currentUser && hasPermission(currentUser, 'CREATE_SURVEYS'));
const [summary, setSummary] = useState<LaunchpadSummary | null>(null);
const [loading, setLoading] = useState(true);
const [pageError, setPageError] = useState('');
const [selectedTemplateKey, setSelectedTemplateKey] = useState('weekly_pulse');
const [launching, setLaunching] = useState(false);
const [launchError, setLaunchError] = useState('');
const [launchSuccess, setLaunchSuccess] = useState<null | {
surveyId: string;
title: string;
questionCount: number;
templateTitle: string;
}>(null);
const [selectedSurveyId, setSelectedSurveyId] = useState('');
const [analysisLoading, setAnalysisLoading] = useState(false);
const [analysisError, setAnalysisError] = useState('');
const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
const [form, setForm] = useState({
templateKey: 'weekly_pulse',
title: '',
description: '',
cadence: 'weekly',
visibility: 'anonymous',
opens_at: '',
closes_at: '',
include_open_text: true,
});
const selectedTemplate = useMemo(
() => summary?.templates.find((template) => template.key === selectedTemplateKey) || null,
[selectedTemplateKey, summary?.templates],
);
const loadSummary = async (preferredSurveyId?: string) => {
try {
setLoading(true);
setPageError('');
const response = await axios.get('/surveys/launchpad/summary');
const nextSummary = response.data as LaunchpadSummary;
setSummary(nextSummary);
setSelectedSurveyId((current) => {
if (preferredSurveyId) {
return preferredSurveyId;
}
if (current && nextSummary.recentSurveys.some((survey) => survey.id === current)) {
return current;
}
return nextSummary.recentSurveys[0]?.id || '';
});
setSelectedTemplateKey((current) => {
if (nextSummary.templates.some((template) => template.key === current)) {
return current;
}
return nextSummary.templates[0]?.key || 'weekly_pulse';
});
setForm((current) => {
const template =
nextSummary.templates.find((item) => item.key === current.templateKey) || nextSummary.templates[0];
return {
...current,
templateKey: template?.key || current.templateKey,
cadence: current.cadence || template?.defaultCadence || 'weekly',
visibility: current.visibility || template?.defaultVisibility || 'anonymous',
description: current.description || template?.description || '',
title: current.title || getDefaultTitle(template),
};
});
} catch (error: any) {
console.error('Failed to load Pulse HQ summary:', error);
setPageError(error?.response?.data || error?.message || 'Failed to load Pulse HQ.');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadSummary();
}, []);
useEffect(() => {
if (!selectedTemplate) {
return;
}
setForm((current) => ({
...current,
templateKey: selectedTemplate.key,
description:
current.description && current.templateKey === selectedTemplate.key
? current.description
: selectedTemplate.description,
cadence: selectedTemplate.defaultCadence,
visibility: selectedTemplate.defaultVisibility,
title: current.title ? current.title : getDefaultTitle(selectedTemplate),
}));
}, [selectedTemplateKey, selectedTemplate?.key]);
const trendMax = useMemo(() => {
const counts = summary?.responseTrend.map((item) => item.count) || [];
return Math.max(...counts, 1);
}, [summary?.responseTrend]);
const selectedSurvey = useMemo(
() => summary?.recentSurveys.find((survey) => survey.id === selectedSurveyId) || null,
[selectedSurveyId, summary?.recentSurveys],
);
const handleFieldChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
) => {
const { name, value, type } = event.target;
const nextValue = type === 'checkbox' ? (event.target as HTMLInputElement).checked : value;
setForm((current) => ({
...current,
[name]: nextValue,
}));
};
const handleTemplateSelect = (templateKey: string) => {
setSelectedTemplateKey(templateKey);
const template = summary?.templates.find((item) => item.key === templateKey);
if (!template) {
return;
}
setForm((current) => ({
...current,
templateKey,
cadence: template.defaultCadence,
visibility: template.defaultVisibility,
description: template.description,
title: getDefaultTitle(template),
}));
};
const handleLaunch = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLaunchError('');
setLaunchSuccess(null);
setAnalysisError('');
if (!form.title.trim()) {
setLaunchError('Give this launch a clear title so your team recognizes it in the dashboard.');
return;
}
if (form.opens_at && form.closes_at && new Date(form.opens_at) > new Date(form.closes_at)) {
setLaunchError('Choose a close date that comes after the open date.');
return;
}
try {
setLaunching(true);
const response = await axios.post('/surveys/launchpad/create', form);
const payload = response.data;
setLaunchSuccess({
surveyId: payload.survey.id,
title: payload.survey.title,
questionCount: payload.questionCount,
templateTitle: payload.template.title,
});
setAnalysisResult(null);
await loadSummary(payload.survey.id);
} catch (error: any) {
console.error('Failed to launch survey:', error);
setLaunchError(error?.response?.data || error?.message || 'Survey launch failed.');
} finally {
setLaunching(false);
}
};
const handleGenerateAnalysis = async (surveyId: string) => {
if (!surveyId) {
setAnalysisError('Pick a survey with responses before generating AI analysis.');
return;
}
try {
setAnalysisLoading(true);
setAnalysisError('');
const response = await axios.post(`/surveys/${surveyId}/pulse-analysis`);
setAnalysisResult(response.data.analysis);
await loadSummary(surveyId);
} catch (error: any) {
console.error('Failed to generate AI analysis:', error);
setAnalysisError(
error?.response?.data || error?.message || 'AI analysis could not be generated right now.',
);
} finally {
setAnalysisLoading(false);
}
};
return (
<>
<Head>
<title>{getPageTitle('Pulse HQ')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiLightningBolt} title='Pulse HQ' main>
<BaseButton color='info' href='/surveys/surveys-list' label='Open survey library' />
</SectionTitleLineWithButton>
{pageError && (
<NotificationBar color='danger' icon={mdiShieldCheck}>
{pageError}
</NotificationBar>
)}
{launchSuccess && (
<NotificationBar
color='success'
icon={mdiCreation}
button={
<div className='flex flex-wrap gap-2'>
<BaseButton
color='success'
outline
href={`/surveys/surveys-view/?id=${launchSuccess.surveyId}`}
label='View survey'
/>
<BaseButton color='success' href={`/surveys/${launchSuccess.surveyId}`} label='Edit settings' />
</div>
}
>
{`${launchSuccess.title} is ready. ${launchSuccess.questionCount} questions were created from ${launchSuccess.templateTitle}.`}
</NotificationBar>
)}
<div className='grid gap-6 xl:grid-cols-[1.3fr,0.7fr]'>
<CardBox className='overflow-hidden border border-white/10 bg-gradient-to-br from-slate-950 via-slate-900 to-violet-950 text-white shadow-2xl'>
<div className='absolute pointer-events-none inset-y-0 right-0 hidden w-1/3 bg-[radial-gradient(circle_at_top,_rgba(168,85,247,0.32),_transparent_70%)] lg:block' />
<div className='grid gap-8 lg:grid-cols-[1.1fr,0.9fr]'>
<div className='space-y-6'>
<div className='inline-flex items-center gap-2 rounded-full border border-violet-400/30 bg-violet-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-violet-200'>
<BaseIcon path={mdiBrain} size={16} />
PulseSurvey launchpad
</div>
<div className='space-y-4'>
<h1 className='text-3xl font-semibold tracking-tight md:text-4xl'>
Launch recurring employee pulse surveys in minutes then turn responses into AI-backed action.
</h1>
<p className='max-w-2xl text-sm leading-6 text-slate-300 md:text-base'>
This first slice gives your People team one polished workspace to launch a survey, review recent sends,
and generate an executive-ready AI analysis without leaving the app.
</p>
</div>
<div className='grid gap-3 sm:grid-cols-3'>
{[
{
label: 'Survey volume',
value: summary?.overview.totalSurveys ?? '—',
note: 'Total launches tracked',
},
{
label: 'Live response rate',
value: summary ? `${summary.overview.responseRate}%` : '—',
note: 'Across all invitations',
},
{
label: 'AI-ready surveys',
value: summary?.overview.aiReadyCount ?? '—',
note: 'Completed responses collected',
},
].map((metric) => (
<div key={metric.label} className='rounded-2xl border border-white/10 bg-white/5 p-4 backdrop-blur'>
<p className='text-xs uppercase tracking-[0.22em] text-slate-400'>{metric.label}</p>
<p className='mt-3 text-3xl font-semibold text-white'>{metric.value}</p>
<p className='mt-2 text-sm text-slate-400'>{metric.note}</p>
</div>
))}
</div>
<div className='flex flex-wrap gap-3'>
<BaseButton color='info' href='/dashboard' label='Admin interface' />
<BaseButton color='white' outline href='/survey_templates/survey_templates-list' label='Template records' />
</div>
</div>
<div className='rounded-3xl border border-white/10 bg-slate-950/70 p-5 shadow-[0_24px_60px_rgba(15,23,42,0.45)]'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-400'>Template lineup</p>
<h2 className='mt-2 text-xl font-semibold text-white'>Choose your first pulse</h2>
</div>
<div className='rounded-2xl border border-white/10 bg-white/5 p-2 text-slate-200'>
<BaseIcon path={mdiCreation} size={22} />
</div>
</div>
<div className='mt-5 grid gap-3'>
{summary?.templates.map((template) => {
const active = selectedTemplateKey === template.key;
return (
<button
key={template.key}
type='button'
onClick={() => handleTemplateSelect(template.key)}
className={`rounded-2xl border px-4 py-4 text-left transition duration-150 ${
active
? 'border-white/30 bg-white/10 shadow-lg'
: 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-white/5'
}`}
>
<div className='flex items-start justify-between gap-3'>
<div>
<div className='flex items-center gap-2'>
<span
className='inline-flex h-2.5 w-2.5 rounded-full'
style={{ backgroundColor: template.accent }}
/>
<p className='font-semibold text-white'>{template.title}</p>
</div>
<p className='mt-2 text-sm text-slate-300'>{template.description}</p>
</div>
<span className='rounded-full bg-white/10 px-2.5 py-1 text-xs text-slate-300'>
{template.questionCount} Qs
</span>
</div>
</button>
);
})}
</div>
</div>
</div>
</CardBox>
<CardBox className='border border-white/10 bg-slate-950 text-slate-100 shadow-xl'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-500'>Workflow preview</p>
<h2 className='mt-2 text-xl font-semibold'>This launch creates</h2>
</div>
<BaseIcon path={mdiClipboardTextClockOutline} size={24} className='text-violet-300' />
</div>
<div className='mt-6 space-y-4'>
{selectedTemplate?.previewQuestions.map((question, index) => (
<div key={`${question.prompt}-${index}`} className='rounded-2xl border border-white/10 bg-white/5 p-4'>
<div className='flex items-center justify-between gap-2'>
<span className='text-sm font-medium text-white'>{`Q${index + 1}`}</span>
<span className='rounded-full bg-white/10 px-2.5 py-1 text-xs text-slate-300'>
{questionTypeLabels[question.question_type] || question.question_type}
</span>
</div>
<p className='mt-3 text-sm leading-6 text-slate-300'>{question.prompt}</p>
</div>
))}
</div>
<div className='mt-6 rounded-2xl border border-emerald-400/20 bg-emerald-500/10 p-4 text-sm text-emerald-100'>
After launch, your survey appears below, inherits cadence + visibility defaults, and becomes eligible for AI
analysis as soon as completed responses start coming in.
</div>
</CardBox>
</div>
<div className='mt-6 grid gap-6 xl:grid-cols-[1.1fr,0.9fr]'>
<CardBox className='border border-slate-200/10 bg-slate-950 text-slate-100'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-500'>Launch survey</p>
<h2 className='mt-2 text-xl font-semibold'>Create a branded pulse from a proven template</h2>
</div>
<BaseIcon path={mdiTarget} size={24} className='text-sky-300' />
</div>
{!canCreateSurveys && (
<div className='mt-5 rounded-2xl border border-amber-400/20 bg-amber-500/10 p-4 text-sm text-amber-100'>
You have read access to Pulse HQ, but launching surveys requires the CREATE_SURVEYS permission.
</div>
)}
{launchError && (
<div className='mt-5 rounded-2xl border border-red-400/20 bg-red-500/10 p-4 text-sm text-red-100'>
{launchError}
</div>
)}
<form className='mt-6 space-y-5' onSubmit={handleLaunch}>
<div className='grid gap-5 md:grid-cols-2'>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Survey title</span>
<input
className='w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-white outline-none transition focus:border-violet-400/60'
name='title'
value={form.title}
onChange={handleFieldChange}
placeholder='Weekly Pulse • Team Alpha'
disabled={!canCreateSurveys || launching}
/>
</label>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Cadence</span>
<select
className='w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-white outline-none transition focus:border-violet-400/60'
name='cadence'
value={form.cadence}
onChange={handleFieldChange}
disabled={!canCreateSurveys || launching}
>
{Object.entries(cadenceLabels).map(([value, label]) => (
<option key={value} value={value} className='bg-slate-900'>
{label}
</option>
))}
</select>
</label>
</div>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Launch notes</span>
<textarea
className='min-h-[120px] w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-white outline-none transition focus:border-violet-400/60'
name='description'
value={form.description}
onChange={handleFieldChange}
placeholder='Optional context shown to admins when reviewing this survey later.'
disabled={!canCreateSurveys || launching}
/>
</label>
<div className='grid gap-5 md:grid-cols-3'>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Visibility</span>
<select
className='w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-white outline-none transition focus:border-violet-400/60'
name='visibility'
value={form.visibility}
onChange={handleFieldChange}
disabled={!canCreateSurveys || launching}
>
{Object.entries(visibilityLabels).map(([value, label]) => (
<option key={value} value={value} className='bg-slate-900'>
{label}
</option>
))}
</select>
</label>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Open date</span>
<input
type='datetime-local'
className='w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-white outline-none transition focus:border-violet-400/60'
name='opens_at'
value={form.opens_at}
onChange={handleFieldChange}
disabled={!canCreateSurveys || launching}
/>
</label>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Close date</span>
<input
type='datetime-local'
className='w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-white outline-none transition focus:border-violet-400/60'
name='closes_at'
value={form.closes_at}
onChange={handleFieldChange}
disabled={!canCreateSurveys || launching}
/>
</label>
</div>
<label className='flex items-center justify-between gap-4 rounded-2xl border border-white/10 bg-white/5 px-4 py-4 text-sm text-slate-300'>
<div>
<p className='font-medium text-white'>Include open text follow-up</p>
<p className='mt-1 text-slate-400'>Keep qualitative context in the survey metadata for future AI analysis.</p>
</div>
<input
type='checkbox'
name='include_open_text'
checked={form.include_open_text}
onChange={handleFieldChange}
disabled={!canCreateSurveys || launching}
className='h-5 w-5 rounded border-white/20 bg-slate-900 text-violet-500 focus:ring-violet-400'
/>
</label>
<div className='flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-white/10 bg-slate-900/70 p-4'>
<div>
<p className='text-sm font-medium text-white'>Ready to launch</p>
<p className='mt-1 text-sm text-slate-400'>Creates survey settings + question set in one action.</p>
</div>
<BaseButton
type='submit'
color='info'
icon={mdiArrowRight}
label={launching ? 'Launching…' : 'Launch survey'}
disabled={!canCreateSurveys || launching}
/>
</div>
</form>
</CardBox>
<div className='grid gap-6'>
<CardBox className='border border-slate-200/10 bg-slate-950 text-slate-100'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-500'>Response momentum</p>
<h2 className='mt-2 text-xl font-semibold'>Last 6 months</h2>
</div>
<BaseIcon path={mdiChartLine} size={24} className='text-violet-300' />
</div>
<div className='mt-6 grid grid-cols-6 items-end gap-3'>
{(summary?.responseTrend || []).map((point) => (
<div key={point.key} className='flex flex-col items-center gap-3'>
<div className='text-xs text-slate-500'>{point.count}</div>
<div className='flex h-40 w-full items-end'>
<div
className='w-full rounded-t-2xl bg-gradient-to-t from-violet-500 via-fuchsia-500 to-sky-400 transition-all duration-300'
style={{ height: `${Math.max((point.count / trendMax) * 100, 10)}%` }}
/>
</div>
<div className='text-xs uppercase tracking-[0.2em] text-slate-500'>{point.label}</div>
</div>
))}
</div>
</CardBox>
<CardBox className='border border-slate-200/10 bg-slate-950 text-slate-100'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-500'>Department signal</p>
<h2 className='mt-2 text-xl font-semibold'>Who is responding</h2>
</div>
<span className='rounded-full border border-white/10 px-3 py-1 text-xs text-slate-300'>
{summary?.overview.activeDepartmentCount ?? 0} active departments
</span>
</div>
<div className='mt-6 space-y-4'>
{(summary?.departmentBreakdown || []).length === 0 && (
<div className='rounded-2xl border border-dashed border-white/10 px-4 py-6 text-sm text-slate-400'>
Department response data will appear here once invitations start turning into submissions.
</div>
)}
{(summary?.departmentBreakdown || []).map((department) => {
const maxDepartmentResponses = Math.max(
...(summary?.departmentBreakdown || []).map((item) => item.responseCount),
1,
);
return (
<div key={department.id}>
<div className='flex items-center justify-between text-sm'>
<span className='text-slate-200'>{department.name}</span>
<span className='text-slate-400'>{department.responseCount} responses</span>
</div>
<div className='mt-2 h-2 rounded-full bg-white/10'>
<div
className='h-2 rounded-full bg-gradient-to-r from-sky-400 via-violet-500 to-fuchsia-500'
style={{ width: `${Math.max((department.responseCount / maxDepartmentResponses) * 100, 8)}%` }}
/>
</div>
</div>
);
})}
</div>
</CardBox>
</div>
</div>
<div className='mt-6 grid gap-6 xl:grid-cols-[1.05fr,0.95fr]'>
<CardBox className='border border-slate-200/10 bg-slate-950 text-slate-100'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-500'>Recent launches</p>
<h2 className='mt-2 text-xl font-semibold'>Survey list + detail shortcuts</h2>
</div>
<BaseButton color='white' outline href='/surveys/surveys-list' label='All surveys' />
</div>
{loading && (
<div className='mt-6 rounded-2xl border border-white/10 bg-white/5 px-4 py-10 text-center text-sm text-slate-400'>
Loading recent survey activity
</div>
)}
{!loading && (summary?.recentSurveys || []).length === 0 && (
<div className='mt-6 rounded-2xl border border-dashed border-white/10 px-4 py-10 text-center text-sm text-slate-400'>
No surveys yet. Launch your first pulse above and it will appear here instantly.
</div>
)}
<div className='mt-6 grid gap-4'>
{(summary?.recentSurveys || []).map((survey) => (
<div key={survey.id} className='rounded-3xl border border-white/10 bg-white/[0.03] p-5'>
<div className='flex flex-wrap items-start justify-between gap-3'>
<div>
<div className='flex flex-wrap items-center gap-2'>
<h3 className='text-lg font-semibold text-white'>{survey.title}</h3>
<span className={`rounded-full px-2.5 py-1 text-xs ${statusClassNames[survey.status] || statusClassNames.draft}`}>
{survey.status}
</span>
</div>
<p className='mt-2 text-sm text-slate-400'>
{survey.description || `${survey.templateName} survey ready for audience and invitation setup.`}
</p>
</div>
<label className='inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1 text-xs text-slate-300'>
<input
type='radio'
checked={selectedSurveyId === survey.id}
onChange={() => setSelectedSurveyId(survey.id)}
className='h-3.5 w-3.5 border-white/20 bg-slate-900 text-violet-400 focus:ring-violet-400'
/>
Focus for AI
</label>
</div>
<div className='mt-5 grid gap-3 md:grid-cols-4'>
{[
['Questions', survey.questionCount],
['Responses', survey.responseCount],
['Invitations', survey.invitationCount],
['Response rate', `${survey.responseRate}%`],
].map(([label, value]) => (
<div key={`${survey.id}-${label}`} className='rounded-2xl border border-white/10 bg-slate-900/80 p-3'>
<p className='text-xs uppercase tracking-[0.18em] text-slate-500'>{label}</p>
<p className='mt-2 text-xl font-semibold text-white'>{value}</p>
</div>
))}
</div>
<div className='mt-5 flex flex-wrap items-center justify-between gap-3 text-sm text-slate-400'>
<div className='flex flex-wrap items-center gap-4'>
<span>{cadenceLabels[survey.cadence] || survey.cadence}</span>
<span>{visibilityLabels[survey.visibility] || survey.visibility}</span>
<span>
{survey.opens_at
? `Opens ${new Date(survey.opens_at).toLocaleDateString()}`
: `Created ${new Date(survey.createdAt).toLocaleDateString()}`}
</span>
<span>
{survey.latestAnalysisAt
? `Last AI run ${new Date(survey.latestAnalysisAt).toLocaleDateString()}`
: 'No AI analysis yet'}
</span>
</div>
<div className='flex flex-wrap gap-2'>
<Link href={`/surveys/surveys-view/?id=${survey.id}`} className='rounded-full border border-white/10 px-3 py-1.5 text-white transition hover:border-white/30'>
View
</Link>
<Link href={`/surveys/${survey.id}`} className='rounded-full border border-white/10 px-3 py-1.5 text-white transition hover:border-white/30'>
Edit
</Link>
</div>
</div>
</div>
))}
</div>
</CardBox>
<CardBox className='border border-slate-200/10 bg-slate-950 text-slate-100'>
<div className='flex items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-slate-500'>AI insights</p>
<h2 className='mt-2 text-xl font-semibold'>Generate executive-ready analysis</h2>
</div>
<BaseIcon path={mdiRobotOutline} size={24} className='text-fuchsia-300' />
</div>
<p className='mt-3 text-sm leading-6 text-slate-400'>
Use the currently selected survey to synthesize sentiment, themes, burnout risk, and recommended actions.
</p>
<div className='mt-5 rounded-2xl border border-white/10 bg-white/5 p-4'>
<label className='block text-sm'>
<span className='mb-2 block text-slate-300'>Survey focus</span>
<select
className='w-full rounded-2xl border border-white/10 bg-slate-950 px-4 py-3 text-white outline-none transition focus:border-fuchsia-400/60'
value={selectedSurveyId}
onChange={(event) => setSelectedSurveyId(event.target.value)}
>
{(summary?.recentSurveys || []).map((survey) => (
<option key={survey.id} value={survey.id} className='bg-slate-950'>
{survey.title}
</option>
))}
</select>
</label>
<div className='mt-4 flex flex-wrap items-center justify-between gap-3'>
<div className='text-sm text-slate-400'>
{selectedSurvey
? `${selectedSurvey.responseCount} responses across ${selectedSurvey.questionCount} questions.`
: 'Pick a survey to begin.'}
</div>
<BaseButton
color='info'
icon={mdiBrain}
label={analysisLoading ? 'Generating…' : 'Generate AI analysis'}
onClick={() => handleGenerateAnalysis(selectedSurveyId)}
disabled={analysisLoading || !selectedSurveyId}
/>
</div>
</div>
{analysisError && (
<div className='mt-5 rounded-2xl border border-red-400/20 bg-red-500/10 p-4 text-sm text-red-100'>
{analysisError}
</div>
)}
{!analysisResult && !analysisLoading && !analysisError && (
<div className='mt-6 rounded-2xl border border-dashed border-white/10 px-4 py-8 text-sm text-slate-400'>
Your AI summary will appear here after you run the analysis.
</div>
)}
{analysisLoading && (
<div className='mt-6 rounded-2xl border border-white/10 bg-white/5 px-4 py-10 text-center text-sm text-slate-300'>
Reviewing survey answers, clustering themes, and drafting next-step recommendations
</div>
)}
{analysisResult && (
<div className='mt-6 space-y-5'>
<div className='rounded-3xl border border-fuchsia-400/20 bg-fuchsia-500/10 p-5'>
<div className='flex flex-wrap items-center justify-between gap-3'>
<div>
<p className='text-xs uppercase tracking-[0.22em] text-fuchsia-200/80'>Overall sentiment</p>
<p className='mt-2 text-4xl font-semibold text-white'>
{analysisResult.overallSentimentScore ?? '—'}
<span className='ml-1 text-lg text-fuchsia-100/70'>/ 100</span>
</p>
</div>
<div className='max-w-xl text-sm leading-6 text-fuchsia-50/90'>
{analysisResult.executiveSummary}
</div>
</div>
</div>
<div className='space-y-3'>
<h3 className='text-sm font-semibold uppercase tracking-[0.22em] text-slate-400'>Top themes</h3>
{analysisResult.topThemes.map((theme, index) => (
<div key={`${theme.theme}-${index}`} className='rounded-2xl border border-white/10 bg-white/5 p-4'>
<div className='flex flex-wrap items-center justify-between gap-2'>
<p className='font-semibold text-white'>{theme.theme}</p>
<span className='rounded-full bg-white/10 px-2.5 py-1 text-xs text-slate-300'>
{theme.confidence}
</span>
</div>
<p className='mt-2 text-sm leading-6 text-slate-300'>{theme.insight}</p>
{theme.quote && <blockquote className='mt-3 border-l-2 border-fuchsia-300/40 pl-3 text-sm italic text-fuchsia-100/90'>{theme.quote}</blockquote>}
</div>
))}
</div>
<div className='grid gap-5 md:grid-cols-2'>
<div className='space-y-3'>
<h3 className='text-sm font-semibold uppercase tracking-[0.22em] text-slate-400'>Department risk</h3>
{analysisResult.departmentRisks.map((risk, index) => (
<div key={`${risk.department}-${index}`} className='rounded-2xl border border-white/10 bg-white/5 p-4'>
<div className='flex items-center justify-between gap-3'>
<p className='font-semibold text-white'>{risk.department}</p>
<span className='rounded-full bg-white/10 px-2.5 py-1 text-xs text-slate-300'>
{risk.riskLevel} · {risk.riskScore}
</span>
</div>
<p className='mt-2 text-sm leading-6 text-slate-300'>{risk.signal}</p>
</div>
))}
</div>
<div className='space-y-3'>
<h3 className='text-sm font-semibold uppercase tracking-[0.22em] text-slate-400'>Recommended actions</h3>
{analysisResult.recommendedActions.map((action, index) => (
<div key={`${action.title}-${index}`} className='rounded-2xl border border-white/10 bg-white/5 p-4'>
<p className='font-semibold text-white'>{action.title}</p>
<p className='mt-2 text-sm leading-6 text-slate-300'>{action.template}</p>
<div className='mt-3 flex flex-wrap gap-2 text-xs text-slate-400'>
<span className='rounded-full bg-white/10 px-2.5 py-1'>Owner: {action.owner}</span>
<span className='rounded-full bg-white/10 px-2.5 py-1'>Timeline: {action.timeline}</span>
</div>
</div>
))}
</div>
</div>
</div>
)}
</CardBox>
</div>
</SectionMain>
</>
);
};
PulseHQPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission='READ_SURVEYS'>{page}</LayoutAuthenticated>;
};
export default PulseHQPage;