refactor(reporting-layout): streamline sidebar and content layout
- Replaced the existing sidebar implementation with a new ReportingSider component that accepts collapse state and toggle function as props. - Simplified the ReportingCollapsedButton component for better readability and functionality. - Updated layout styles to enhance responsiveness and maintain consistent margins. - Removed unused CSS styles related to the sidebar for cleaner code.
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"Bash(move:*)",
|
"Bash(move:*)",
|
||||||
"Bash(mv:*)",
|
"Bash(mv:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
|
"Bash(rm:*)",
|
||||||
"Bash(rm:*)"
|
"Bash(rm:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
|
|||||||
@@ -164,4 +164,38 @@ export default class SurveyController extends WorklenzControllerBase {
|
|||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, response));
|
return res.status(200).send(new ServerResponse(true, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async checkAccountSetupSurveyStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM survey_responses sr
|
||||||
|
INNER JOIN surveys s ON sr.survey_id = s.id
|
||||||
|
WHERE sr.user_id = $1
|
||||||
|
AND s.survey_type = 'account_setup'
|
||||||
|
AND sr.is_completed = true
|
||||||
|
) as is_completed,
|
||||||
|
(
|
||||||
|
SELECT sr.completed_at
|
||||||
|
FROM survey_responses sr
|
||||||
|
INNER JOIN surveys s ON sr.survey_id = s.id
|
||||||
|
WHERE sr.user_id = $1
|
||||||
|
AND s.survey_type = 'account_setup'
|
||||||
|
AND sr.is_completed = true
|
||||||
|
LIMIT 1
|
||||||
|
) as completed_at;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await db.query(q, [userId]);
|
||||||
|
const status = result.rows[0] || { is_completed: false, completed_at: null };
|
||||||
|
|
||||||
|
return res.status(200).send(new ServerResponse(true, status));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,9 @@ const surveyApiRouter = express.Router();
|
|||||||
// Get account setup survey with questions
|
// Get account setup survey with questions
|
||||||
surveyApiRouter.get("/account-setup", safeControllerFunction(SurveyController.getAccountSetupSurvey));
|
surveyApiRouter.get("/account-setup", safeControllerFunction(SurveyController.getAccountSetupSurvey));
|
||||||
|
|
||||||
|
// Check if user has completed account setup survey
|
||||||
|
surveyApiRouter.get("/account-setup/status", safeControllerFunction(SurveyController.checkAccountSetupSurveyStatus));
|
||||||
|
|
||||||
// Submit survey response
|
// Submit survey response
|
||||||
surveyApiRouter.post("/responses", surveySubmissionValidator, safeControllerFunction(SurveyController.submitSurveyResponse));
|
surveyApiRouter.post("/responses", surveySubmissionValidator, safeControllerFunction(SurveyController.submitSurveyResponse));
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@
|
|||||||
"discoveryQuestion": "Si dëgjove për ne?",
|
"discoveryQuestion": "Si dëgjove për ne?",
|
||||||
"allSetTitle": "Çdo gjë gati!",
|
"allSetTitle": "Çdo gjë gati!",
|
||||||
"allSetDescription": "Le të krijojmë projektin tënd të parë dhe të fillojmë me Worklenz",
|
"allSetDescription": "Le të krijojmë projektin tënd të parë dhe të fillojmë me Worklenz",
|
||||||
|
"surveyCompleteTitle": "Faleminderit!",
|
||||||
|
"surveyCompleteDescription": "Përgjigjet tuaja na ndihmojnë të përmirësojmë Worklenz për të gjithë",
|
||||||
"aboutYouStepName": "Rreth teje",
|
"aboutYouStepName": "Rreth teje",
|
||||||
"yourNeedsStepName": "Nevojat e tua",
|
"yourNeedsStepName": "Nevojat e tua",
|
||||||
"discoveryStepName": "Zbulimi",
|
"discoveryStepName": "Zbulimi",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
"discoveryQuestion": "Wie haben Sie von uns erfahren?",
|
"discoveryQuestion": "Wie haben Sie von uns erfahren?",
|
||||||
"allSetTitle": "Sie sind bereit!",
|
"allSetTitle": "Sie sind bereit!",
|
||||||
"allSetDescription": "Lassen Sie uns Ihr erstes Projekt erstellen und mit Worklenz beginnen",
|
"allSetDescription": "Lassen Sie uns Ihr erstes Projekt erstellen und mit Worklenz beginnen",
|
||||||
|
"surveyCompleteTitle": "Vielen Dank!",
|
||||||
|
"surveyCompleteDescription": "Ihr Feedback hilft uns, Worklenz für alle zu verbessern",
|
||||||
"aboutYouStepName": "Über Sie",
|
"aboutYouStepName": "Über Sie",
|
||||||
"yourNeedsStepName": "Ihre Bedürfnisse",
|
"yourNeedsStepName": "Ihre Bedürfnisse",
|
||||||
"discoveryStepName": "Entdeckung",
|
"discoveryStepName": "Entdeckung",
|
||||||
|
|||||||
@@ -88,6 +88,8 @@
|
|||||||
"discoveryQuestion": "How did you hear about us?",
|
"discoveryQuestion": "How did you hear about us?",
|
||||||
"allSetTitle": "You're all set!",
|
"allSetTitle": "You're all set!",
|
||||||
"allSetDescription": "Let's create your first project and get started with Worklenz",
|
"allSetDescription": "Let's create your first project and get started with Worklenz",
|
||||||
|
"surveyCompleteTitle": "Thank you!",
|
||||||
|
"surveyCompleteDescription": "Your feedback helps us improve Worklenz for everyone",
|
||||||
"aboutYouStepName": "About You",
|
"aboutYouStepName": "About You",
|
||||||
"yourNeedsStepName": "Your Needs",
|
"yourNeedsStepName": "Your Needs",
|
||||||
"discoveryStepName": "Discovery",
|
"discoveryStepName": "Discovery",
|
||||||
|
|||||||
@@ -91,6 +91,8 @@
|
|||||||
"discoveryQuestion": "¿Cómo te enteraste de nosotros?",
|
"discoveryQuestion": "¿Cómo te enteraste de nosotros?",
|
||||||
"allSetTitle": "¡Ya estás listo!",
|
"allSetTitle": "¡Ya estás listo!",
|
||||||
"allSetDescription": "Vamos a crear tu primer proyecto y comenzar con Worklenz",
|
"allSetDescription": "Vamos a crear tu primer proyecto y comenzar con Worklenz",
|
||||||
|
"surveyCompleteTitle": "¡Gracias!",
|
||||||
|
"surveyCompleteDescription": "Tu retroalimentación nos ayuda a mejorar Worklenz para todos",
|
||||||
"aboutYouStepName": "Sobre ti",
|
"aboutYouStepName": "Sobre ti",
|
||||||
"yourNeedsStepName": "Tus necesidades",
|
"yourNeedsStepName": "Tus necesidades",
|
||||||
"discoveryStepName": "Descubrimiento",
|
"discoveryStepName": "Descubrimiento",
|
||||||
|
|||||||
@@ -91,6 +91,8 @@
|
|||||||
"discoveryQuestion": "Como você soube sobre nós?",
|
"discoveryQuestion": "Como você soube sobre nós?",
|
||||||
"allSetTitle": "Você está pronto!",
|
"allSetTitle": "Você está pronto!",
|
||||||
"allSetDescription": "Vamos criar seu primeiro projeto e começar com o Worklenz",
|
"allSetDescription": "Vamos criar seu primeiro projeto e começar com o Worklenz",
|
||||||
|
"surveyCompleteTitle": "Obrigado!",
|
||||||
|
"surveyCompleteDescription": "Seu feedback nos ajuda a melhorar o Worklenz para todos",
|
||||||
"aboutYouStepName": "Sobre você",
|
"aboutYouStepName": "Sobre você",
|
||||||
"yourNeedsStepName": "Suas necessidades",
|
"yourNeedsStepName": "Suas necessidades",
|
||||||
"discoveryStepName": "Descoberta",
|
"discoveryStepName": "Descoberta",
|
||||||
|
|||||||
@@ -89,6 +89,8 @@
|
|||||||
"discoveryQuestion": "您是如何听说我们的?",
|
"discoveryQuestion": "您是如何听说我们的?",
|
||||||
"allSetTitle": "一切就绪!",
|
"allSetTitle": "一切就绪!",
|
||||||
"allSetDescription": "让我们创建您的第一个项目并开始使用 Worklenz 吧",
|
"allSetDescription": "让我们创建您的第一个项目并开始使用 Worklenz 吧",
|
||||||
|
"surveyCompleteTitle": "谢谢!",
|
||||||
|
"surveyCompleteDescription": "您的反馈有助于我们为所有人改进 Worklenz",
|
||||||
"aboutYouStepName": "关于您",
|
"aboutYouStepName": "关于您",
|
||||||
"yourNeedsStepName": "您的需求",
|
"yourNeedsStepName": "您的需求",
|
||||||
"discoveryStepName": "发现",
|
"discoveryStepName": "发现",
|
||||||
|
|||||||
@@ -18,5 +18,10 @@ export const surveyApiService = {
|
|||||||
async getUserSurveyResponse(surveyId: string): Promise<IServerResponse<ISurveyResponse>> {
|
async getUserSurveyResponse(surveyId: string): Promise<IServerResponse<ISurveyResponse>> {
|
||||||
const response = await apiClient.get<IServerResponse<ISurveyResponse>>(`${API_BASE_URL}/surveys/responses/${surveyId}`);
|
const response = await apiClient.get<IServerResponse<ISurveyResponse>>(`${API_BASE_URL}/surveys/responses/${surveyId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkAccountSetupSurveyStatus(): Promise<IServerResponse<{ is_completed: boolean; completed_at?: string }>> {
|
||||||
|
const response = await apiClient.get<IServerResponse<{ is_completed: boolean; completed_at?: string }>>(`${API_BASE_URL}/surveys/account-setup/status`);
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -20,6 +20,7 @@ interface Props {
|
|||||||
styles: any;
|
styles: any;
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
token?: any;
|
token?: any;
|
||||||
|
isModal?: boolean; // New prop to indicate if used in modal context
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SurveyPageProps {
|
interface SurveyPageProps {
|
||||||
@@ -29,6 +30,7 @@ interface SurveyPageProps {
|
|||||||
surveyData: IAccountSetupSurveyData;
|
surveyData: IAccountSetupSurveyData;
|
||||||
handleSurveyDataChange: (field: keyof IAccountSetupSurveyData, value: any) => void;
|
handleSurveyDataChange: (field: keyof IAccountSetupSurveyData, value: any) => void;
|
||||||
handleUseCaseToggle?: (value: UseCase) => void;
|
handleUseCaseToggle?: (value: UseCase) => void;
|
||||||
|
isModal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page 1: About You
|
// Page 1: About You
|
||||||
@@ -235,7 +237,7 @@ const YourNeedsPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, h
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Page 3: Discovery
|
// Page 3: Discovery
|
||||||
const DiscoveryPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, handleSurveyDataChange }) => {
|
const DiscoveryPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, handleSurveyDataChange, isModal }) => {
|
||||||
const { t } = useTranslation('account-setup');
|
const { t } = useTranslation('account-setup');
|
||||||
|
|
||||||
const howHeardAboutOptions: { value: HowHeardAbout; label: string; icon: string }[] = [
|
const howHeardAboutOptions: { value: HowHeardAbout; label: string; icon: string }[] = [
|
||||||
@@ -291,14 +293,18 @@ const DiscoveryPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, h
|
|||||||
|
|
||||||
<div className="mt-12 p-1.5 rounded-lg text-center" style={{ backgroundColor: token?.colorSuccessBg, borderColor: token?.colorSuccessBorder, border: '1px solid' }}>
|
<div className="mt-12 p-1.5 rounded-lg text-center" style={{ backgroundColor: token?.colorSuccessBg, borderColor: token?.colorSuccessBorder, border: '1px solid' }}>
|
||||||
<div className="text-4xl mb-3">🎉</div>
|
<div className="text-4xl mb-3">🎉</div>
|
||||||
<Title level={4} style={{ color: token?.colorText, marginBottom: 8 }}>{t('allSetTitle')}</Title>
|
<Title level={4} style={{ color: token?.colorText, marginBottom: 8 }}>
|
||||||
<Paragraph style={{ color: token?.colorTextSecondary, marginBottom: 0 }}>{t('allSetDescription')}</Paragraph>
|
{isModal ? t('surveyCompleteTitle') : t('allSetTitle')}
|
||||||
|
</Title>
|
||||||
|
<Paragraph style={{ color: token?.colorTextSecondary, marginBottom: 0 }}>
|
||||||
|
{isModal ? t('surveyCompleteDescription') : t('allSetDescription')}
|
||||||
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token }) => {
|
export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token, isModal = false }) => {
|
||||||
const { t } = useTranslation('account-setup');
|
const { t } = useTranslation('account-setup');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { surveyData, surveySubStep } = useSelector((state: RootState) => state.accountSetupReducer);
|
const { surveyData, surveySubStep } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||||
@@ -339,9 +345,9 @@ export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token
|
|||||||
};
|
};
|
||||||
|
|
||||||
const surveyPages = [
|
const surveyPages = [
|
||||||
<AboutYouPage key="about-you" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} />,
|
<AboutYouPage key="about-you" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} isModal={isModal} />,
|
||||||
<YourNeedsPage key="your-needs" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} handleUseCaseToggle={handleUseCaseToggle} />,
|
<YourNeedsPage key="your-needs" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} handleUseCaseToggle={handleUseCaseToggle} isModal={isModal} />,
|
||||||
<DiscoveryPage key="discovery" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} />
|
<DiscoveryPage key="discovery" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} isModal={isModal} />
|
||||||
];
|
];
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
244
worklenz-frontend/src/components/survey/SurveyPromptModal.tsx
Normal file
244
worklenz-frontend/src/components/survey/SurveyPromptModal.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Modal, Button, Result, Spin, Flex } from '@/shared/antd-imports';
|
||||||
|
import { SurveyStep } from '@/components/account-setup/survey-step';
|
||||||
|
import { useSurveyStatus } from '@/hooks/useSurveyStatus';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { surveyApiService } from '@/api/survey/survey.api.service';
|
||||||
|
import { appMessage } from '@/shared/antd-imports';
|
||||||
|
import { ISurveySubmissionRequest } from '@/types/account-setup/survey.types';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { resetSurveyData, setSurveySubStep } from '@/features/account-setup/account-setup.slice';
|
||||||
|
|
||||||
|
interface SurveyPromptModalProps {
|
||||||
|
forceShow?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SurveyPromptModal: React.FC<SurveyPromptModalProps> = ({ forceShow = false, onClose }) => {
|
||||||
|
const { t } = useTranslation('survey');
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [surveyCompleted, setSurveyCompleted] = useState(false);
|
||||||
|
const [surveyInfo, setSurveyInfo] = useState<{ id: string; questions: any[] } | null>(null);
|
||||||
|
const { hasCompletedSurvey, loading, refetch } = useSurveyStatus();
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const surveyData = useAppSelector(state => state.accountSetupReducer.surveyData);
|
||||||
|
const surveySubStep = useAppSelector(state => state.accountSetupReducer.surveySubStep);
|
||||||
|
const isDarkMode = themeMode === 'dark';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (forceShow) {
|
||||||
|
setVisible(true);
|
||||||
|
dispatch(resetSurveyData());
|
||||||
|
dispatch(setSurveySubStep(0));
|
||||||
|
|
||||||
|
// Fetch survey info
|
||||||
|
const fetchSurvey = async () => {
|
||||||
|
try {
|
||||||
|
const response = await surveyApiService.getAccountSetupSurvey();
|
||||||
|
if (response.done && response.body) {
|
||||||
|
setSurveyInfo({
|
||||||
|
id: response.body.id,
|
||||||
|
questions: response.body.questions || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch survey', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSurvey();
|
||||||
|
} else if (!loading && hasCompletedSurvey === false) {
|
||||||
|
// Reset survey data when modal will be shown
|
||||||
|
dispatch(resetSurveyData());
|
||||||
|
dispatch(setSurveySubStep(0));
|
||||||
|
|
||||||
|
// Fetch survey info
|
||||||
|
const fetchSurvey = async () => {
|
||||||
|
try {
|
||||||
|
const response = await surveyApiService.getAccountSetupSurvey();
|
||||||
|
if (response.done && response.body) {
|
||||||
|
setSurveyInfo({
|
||||||
|
id: response.body.id,
|
||||||
|
questions: response.body.questions || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch survey', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSurvey();
|
||||||
|
|
||||||
|
// Show modal after a 5 second delay to not interrupt user immediately
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setVisible(true);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [loading, hasCompletedSurvey, dispatch, forceShow]);
|
||||||
|
|
||||||
|
const handleComplete = async () => {
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
if (!surveyData || !surveyInfo) {
|
||||||
|
throw new Error('Survey data not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of question keys to IDs
|
||||||
|
const questionMap = surveyInfo.questions.reduce((acc, q) => {
|
||||||
|
acc[q.question_key] = q.id;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
// Prepare submission data with actual question IDs
|
||||||
|
const submissionData: ISurveySubmissionRequest = {
|
||||||
|
survey_id: surveyInfo.id,
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
question_id: questionMap['organization_type'],
|
||||||
|
answer_text: surveyData.organization_type || ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: questionMap['user_role'],
|
||||||
|
answer_text: surveyData.user_role || ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: questionMap['main_use_cases'],
|
||||||
|
answer_json: surveyData.main_use_cases || []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: questionMap['previous_tools'],
|
||||||
|
answer_text: surveyData.previous_tools || ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_id: questionMap['how_heard_about'],
|
||||||
|
answer_text: surveyData.how_heard_about || ''
|
||||||
|
}
|
||||||
|
].filter(answer => answer.question_id) // Filter out any missing question IDs
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await surveyApiService.submitSurveyResponse(submissionData);
|
||||||
|
|
||||||
|
if (response.done) {
|
||||||
|
setSurveyCompleted(true);
|
||||||
|
appMessage.success('Thank you for completing the survey!');
|
||||||
|
|
||||||
|
// Wait a moment before closing
|
||||||
|
setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
refetch(); // Update the survey status
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to submit survey');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to submit survey', error);
|
||||||
|
appMessage.error('Failed to submit survey. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
setVisible(false);
|
||||||
|
// Optionally, you can set a flag in localStorage to not show again for some time
|
||||||
|
localStorage.setItem('survey_skipped_at', new Date().toISOString());
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentStepValid = () => {
|
||||||
|
switch (surveySubStep) {
|
||||||
|
case 0:
|
||||||
|
return surveyData.organization_type && surveyData.user_role;
|
||||||
|
case 1:
|
||||||
|
return surveyData.main_use_cases && surveyData.main_use_cases.length > 0;
|
||||||
|
case 2:
|
||||||
|
return surveyData.how_heard_about;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (surveySubStep < 2) {
|
||||||
|
dispatch(setSurveySubStep(surveySubStep + 1));
|
||||||
|
} else {
|
||||||
|
handleComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
if (surveySubStep > 0) {
|
||||||
|
dispatch(setSurveySubStep(surveySubStep - 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
title={surveyCompleted ? null : "Help Us Improve Your Experience"}
|
||||||
|
onCancel={handleSkip}
|
||||||
|
footer={
|
||||||
|
surveyCompleted ? null : (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleSkip}>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Flex gap={8}>
|
||||||
|
{surveySubStep > 0 && (
|
||||||
|
<Button onClick={handlePrevious}>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!isCurrentStepValid()}
|
||||||
|
loading={submitting && surveySubStep === 2}
|
||||||
|
>
|
||||||
|
{surveySubStep === 2 ? 'Complete Survey' : 'Next'}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
width={800}
|
||||||
|
maskClosable={false}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<p style={{ marginTop: 16 }}>Submitting your responses...</p>
|
||||||
|
</div>
|
||||||
|
) : surveyCompleted ? (
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title="Thank you!"
|
||||||
|
subTitle="Your feedback helps us improve Worklenz for everyone."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ maxHeight: '70vh', overflowY: 'auto' }}>
|
||||||
|
<SurveyStep
|
||||||
|
onEnter={() => {}} // Empty function since we handle navigation via buttons
|
||||||
|
styles={{}}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
isModal={true} // Pass true to indicate modal context
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, Button, Result, Alert } from '@/shared/antd-imports';
|
||||||
|
import { CheckCircleOutlined, FormOutlined } from '@/shared/antd-imports';
|
||||||
|
import { useSurveyStatus } from '@/hooks/useSurveyStatus';
|
||||||
|
import { SurveyPromptModal } from './SurveyPromptModal';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const SurveySettingsCard: React.FC = () => {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const { hasCompletedSurvey, loading } = useSurveyStatus();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card loading={true} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<span>
|
||||||
|
<FormOutlined style={{ marginRight: 8 }} />
|
||||||
|
Personalization Survey
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
hasCompletedSurvey && (
|
||||||
|
<Button type="link" onClick={() => setShowModal(true)}>
|
||||||
|
Update Responses
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{hasCompletedSurvey ? (
|
||||||
|
<Result
|
||||||
|
icon={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
||||||
|
title="Survey Completed"
|
||||||
|
subTitle="Thank you for completing the personalization survey. Your responses help us improve Worklenz."
|
||||||
|
extra={
|
||||||
|
<Button onClick={() => setShowModal(true)}>
|
||||||
|
Update Your Responses
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
message="Help us personalize your experience"
|
||||||
|
description="Take a quick survey to tell us about your organization and how you use Worklenz."
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Button type="primary" size="large" onClick={() => setShowModal(true)}>
|
||||||
|
Take Survey Now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{showModal && (
|
||||||
|
<SurveyPromptModal
|
||||||
|
forceShow={true}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
worklenz-frontend/src/components/survey/index.ts
Normal file
2
worklenz-frontend/src/components/survey/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { SurveyPromptModal } from './SurveyPromptModal';
|
||||||
|
export { SurveySettingsCard } from './SurveySettingsCard';
|
||||||
@@ -61,6 +61,10 @@ const accountSetupSlice = createSlice({
|
|||||||
setSurveySubStep: (state, action: PayloadAction<number>) => {
|
setSurveySubStep: (state, action: PayloadAction<number>) => {
|
||||||
state.surveySubStep = action.payload;
|
state.surveySubStep = action.payload;
|
||||||
},
|
},
|
||||||
|
resetSurveyData: (state) => {
|
||||||
|
state.surveyData = {};
|
||||||
|
state.surveySubStep = 0;
|
||||||
|
},
|
||||||
resetAccountSetup: () => initialState,
|
resetAccountSetup: () => initialState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -74,6 +78,7 @@ export const {
|
|||||||
setCurrentStep,
|
setCurrentStep,
|
||||||
setSurveyData,
|
setSurveyData,
|
||||||
setSurveySubStep,
|
setSurveySubStep,
|
||||||
|
resetSurveyData,
|
||||||
resetAccountSetup,
|
resetAccountSetup,
|
||||||
} = accountSetupSlice.actions;
|
} = accountSetupSlice.actions;
|
||||||
|
|
||||||
|
|||||||
49
worklenz-frontend/src/hooks/useSurveyStatus.ts
Normal file
49
worklenz-frontend/src/hooks/useSurveyStatus.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { surveyApiService } from '@/api/survey/survey.api.service';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
|
||||||
|
export interface UseSurveyStatusResult {
|
||||||
|
hasCompletedSurvey: boolean | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSurveyStatus = (): UseSurveyStatusResult => {
|
||||||
|
const [hasCompletedSurvey, setHasCompletedSurvey] = useState<boolean | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const checkSurveyStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await surveyApiService.checkAccountSetupSurveyStatus();
|
||||||
|
|
||||||
|
if (response.done) {
|
||||||
|
setHasCompletedSurvey(response.body.is_completed);
|
||||||
|
} else {
|
||||||
|
setHasCompletedSurvey(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to check survey status', err);
|
||||||
|
setError(err as Error);
|
||||||
|
// Assume not completed if there's an error
|
||||||
|
setHasCompletedSurvey(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkSurveyStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasCompletedSurvey,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: checkSurveyStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@ const SIDEBAR_MAX_WIDTH = 400;
|
|||||||
|
|
||||||
// Lazy load heavy components
|
// Lazy load heavy components
|
||||||
const TaskDrawer = React.lazy(() => import('@/components/task-drawer/task-drawer'));
|
const TaskDrawer = React.lazy(() => import('@/components/task-drawer/task-drawer'));
|
||||||
|
const SurveyPromptModal = React.lazy(() => import('@/components/survey/SurveyPromptModal').then(m => ({ default: m.SurveyPromptModal })));
|
||||||
|
|
||||||
const HomePage = memo(() => {
|
const HomePage = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -142,6 +143,11 @@ const HomePage = memo(() => {
|
|||||||
document.body,
|
document.body,
|
||||||
'project-drawer'
|
'project-drawer'
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Survey Modal - only shown to users who haven't completed it */}
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<SurveyPromptModal />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user