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:
chamikaJ
2025-07-28 14:54:54 +05:30
parent 2c860b0cc8
commit dd511b236f
17 changed files with 447 additions and 7 deletions

View File

@@ -8,6 +8,7 @@
"Bash(move:*)",
"Bash(mv:*)",
"Bash(grep:*)",
"Bash(rm:*)",
"Bash(rm:*)"
],
"deny": []

View File

@@ -164,4 +164,38 @@ export default class SurveyController extends WorklenzControllerBase {
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));
}
}

View File

@@ -8,6 +8,9 @@ const surveyApiRouter = express.Router();
// Get account setup survey with questions
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
surveyApiRouter.post("/responses", surveySubmissionValidator, safeControllerFunction(SurveyController.submitSurveyResponse));

View File

@@ -80,6 +80,8 @@
"discoveryQuestion": "Si dëgjove për ne?",
"allSetTitle": "Çdo gjë gati!",
"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",
"yourNeedsStepName": "Nevojat e tua",
"discoveryStepName": "Zbulimi",

View File

@@ -90,6 +90,8 @@
"discoveryQuestion": "Wie haben Sie von uns erfahren?",
"allSetTitle": "Sie sind bereit!",
"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",
"yourNeedsStepName": "Ihre Bedürfnisse",
"discoveryStepName": "Entdeckung",

View File

@@ -88,6 +88,8 @@
"discoveryQuestion": "How did you hear about us?",
"allSetTitle": "You're all set!",
"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",
"yourNeedsStepName": "Your Needs",
"discoveryStepName": "Discovery",

View File

@@ -91,6 +91,8 @@
"discoveryQuestion": "¿Cómo te enteraste de nosotros?",
"allSetTitle": "¡Ya estás listo!",
"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",
"yourNeedsStepName": "Tus necesidades",
"discoveryStepName": "Descubrimiento",

View File

@@ -91,6 +91,8 @@
"discoveryQuestion": "Como você soube sobre nós?",
"allSetTitle": "Você está pronto!",
"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ê",
"yourNeedsStepName": "Suas necessidades",
"discoveryStepName": "Descoberta",

View File

@@ -89,6 +89,8 @@
"discoveryQuestion": "您是如何听说我们的?",
"allSetTitle": "一切就绪!",
"allSetDescription": "让我们创建您的第一个项目并开始使用 Worklenz 吧",
"surveyCompleteTitle": "谢谢!",
"surveyCompleteDescription": "您的反馈有助于我们为所有人改进 Worklenz",
"aboutYouStepName": "关于您",
"yourNeedsStepName": "您的需求",
"discoveryStepName": "发现",

View File

@@ -18,5 +18,10 @@ export const surveyApiService = {
async getUserSurveyResponse(surveyId: string): Promise<IServerResponse<ISurveyResponse>> {
const response = await apiClient.get<IServerResponse<ISurveyResponse>>(`${API_BASE_URL}/surveys/responses/${surveyId}`);
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;
}
};

View File

@@ -20,6 +20,7 @@ interface Props {
styles: any;
isDarkMode: boolean;
token?: any;
isModal?: boolean; // New prop to indicate if used in modal context
}
interface SurveyPageProps {
@@ -29,6 +30,7 @@ interface SurveyPageProps {
surveyData: IAccountSetupSurveyData;
handleSurveyDataChange: (field: keyof IAccountSetupSurveyData, value: any) => void;
handleUseCaseToggle?: (value: UseCase) => void;
isModal?: boolean;
}
// Page 1: About You
@@ -235,7 +237,7 @@ const YourNeedsPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, h
};
// 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 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="text-4xl mb-3">🎉</div>
<Title level={4} style={{ color: token?.colorText, marginBottom: 8 }}>{t('allSetTitle')}</Title>
<Paragraph style={{ color: token?.colorTextSecondary, marginBottom: 0 }}>{t('allSetDescription')}</Paragraph>
<Title level={4} style={{ color: token?.colorText, marginBottom: 8 }}>
{isModal ? t('surveyCompleteTitle') : t('allSetTitle')}
</Title>
<Paragraph style={{ color: token?.colorTextSecondary, marginBottom: 0 }}>
{isModal ? t('surveyCompleteDescription') : t('allSetDescription')}
</Paragraph>
</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 dispatch = useDispatch();
const { surveyData, surveySubStep } = useSelector((state: RootState) => state.accountSetupReducer);
@@ -339,9 +345,9 @@ export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token
};
const surveyPages = [
<AboutYouPage key="about-you" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} />,
<YourNeedsPage key="your-needs" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} handleUseCaseToggle={handleUseCaseToggle} />,
<DiscoveryPage key="discovery" 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} isModal={isModal} />,
<DiscoveryPage key="discovery" styles={styles} isDarkMode={isDarkMode} token={token} surveyData={surveyData} handleSurveyDataChange={handleSurveyDataChange} isModal={isModal} />
];
React.useEffect(() => {

View 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>
);
};

View File

@@ -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)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,2 @@
export { SurveyPromptModal } from './SurveyPromptModal';
export { SurveySettingsCard } from './SurveySettingsCard';

View File

@@ -61,6 +61,10 @@ const accountSetupSlice = createSlice({
setSurveySubStep: (state, action: PayloadAction<number>) => {
state.surveySubStep = action.payload;
},
resetSurveyData: (state) => {
state.surveyData = {};
state.surveySubStep = 0;
},
resetAccountSetup: () => initialState,
},
});
@@ -74,6 +78,7 @@ export const {
setCurrentStep,
setSurveyData,
setSurveySubStep,
resetSurveyData,
resetAccountSetup,
} = accountSetupSlice.actions;

View 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
};
};

View File

@@ -27,6 +27,7 @@ const SIDEBAR_MAX_WIDTH = 400;
// Lazy load heavy components
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 dispatch = useAppDispatch();
@@ -142,6 +143,11 @@ const HomePage = memo(() => {
document.body,
'project-drawer'
)}
{/* Survey Modal - only shown to users who haven't completed it */}
<Suspense fallback={null}>
<SurveyPromptModal />
</Suspense>
</div>
);
});