feat(surveys): implement account setup survey functionality
- Added new database migration to create survey-related tables for storing questions and responses. - Developed SurveyController to handle fetching and submitting survey data. - Created survey API routes for account setup, including endpoints for retrieving the survey and submitting responses. - Implemented frontend components for displaying the survey and capturing user responses, integrating with Redux for state management. - Enhanced localization files to include survey-related text for multiple languages. - Added validation middleware for survey submissions to ensure data integrity.
This commit is contained in:
@@ -4,7 +4,9 @@
|
|||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(npm run type-check:*)",
|
"Bash(npm run type-check:*)",
|
||||||
"Bash(npm run:*)"
|
"Bash(npm run:*)",
|
||||||
|
"Bash(move:*)",
|
||||||
|
"Bash(mv:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
-- Migration: Add survey tables for account setup questionnaire
|
||||||
|
-- Date: 2025-07-24
|
||||||
|
-- Description: Creates tables to store survey questions and user responses for account setup flow
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Create surveys table to define different types of surveys
|
||||||
|
CREATE TABLE IF NOT EXISTS surveys (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
survey_type VARCHAR(50) DEFAULT 'account_setup' NOT NULL, -- 'account_setup', 'onboarding', 'feedback'
|
||||||
|
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create survey_questions table to store individual questions
|
||||||
|
CREATE TABLE IF NOT EXISTS survey_questions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
question_key VARCHAR(100) NOT NULL, -- Used for localization keys
|
||||||
|
question_type VARCHAR(50) NOT NULL, -- 'single_choice', 'multiple_choice', 'text'
|
||||||
|
is_required BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
sort_order INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
options JSONB, -- For choice questions, store options as JSON array
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create survey_responses table to track user responses to surveys
|
||||||
|
CREATE TABLE IF NOT EXISTS survey_responses (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
started_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create survey_answers table to store individual question answers
|
||||||
|
CREATE TABLE IF NOT EXISTS survey_answers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
response_id UUID REFERENCES survey_responses(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
question_id UUID REFERENCES survey_questions(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
answer_text TEXT,
|
||||||
|
answer_json JSONB, -- For multiple choice answers stored as array
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add performance indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_surveys_type_active ON surveys(survey_type, is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_survey_questions_survey_order ON survey_questions(survey_id, sort_order);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_survey_responses_user_survey ON survey_responses(user_id, survey_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_survey_responses_completed ON survey_responses(survey_id, is_completed);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_survey_answers_response ON survey_answers(response_id);
|
||||||
|
|
||||||
|
-- Add constraints
|
||||||
|
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_sort_order_check CHECK (sort_order >= 0);
|
||||||
|
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_type_check CHECK (question_type IN ('single_choice', 'multiple_choice', 'text'));
|
||||||
|
|
||||||
|
-- Add unique constraint to prevent duplicate responses per user per survey
|
||||||
|
ALTER TABLE survey_responses ADD CONSTRAINT unique_user_survey_response UNIQUE (user_id, survey_id);
|
||||||
|
|
||||||
|
-- Add unique constraint to prevent duplicate answers per question per response
|
||||||
|
ALTER TABLE survey_answers ADD CONSTRAINT unique_response_question_answer UNIQUE (response_id, question_id);
|
||||||
|
|
||||||
|
-- Insert the default account setup survey
|
||||||
|
INSERT INTO surveys (name, description, survey_type, is_active) VALUES
|
||||||
|
('Account Setup Survey', 'Initial questionnaire during account setup to understand user needs', 'account_setup', true)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Get the survey ID for inserting questions
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
survey_uuid UUID;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO survey_uuid FROM surveys WHERE survey_type = 'account_setup' AND name = 'Account Setup Survey' LIMIT 1;
|
||||||
|
|
||||||
|
-- Insert survey questions
|
||||||
|
INSERT INTO survey_questions (survey_id, question_key, question_type, is_required, sort_order, options) VALUES
|
||||||
|
(survey_uuid, 'organization_type', 'single_choice', true, 1, '["freelancer", "startup", "small_medium_business", "agency", "enterprise", "other"]'),
|
||||||
|
(survey_uuid, 'user_role', 'single_choice', true, 2, '["founder_ceo", "project_manager", "software_developer", "designer", "operations", "other"]'),
|
||||||
|
(survey_uuid, 'main_use_cases', 'multiple_choice', true, 3, '["task_management", "team_collaboration", "resource_planning", "client_communication", "time_tracking", "other"]'),
|
||||||
|
(survey_uuid, 'previous_tools', 'text', false, 4, null),
|
||||||
|
(survey_uuid, 'how_heard_about', 'single_choice', false, 5, '["google_search", "twitter", "linkedin", "friend_colleague", "blog_article", "other"]')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
167
worklenz-backend/src/controllers/survey-controller.ts
Normal file
167
worklenz-backend/src/controllers/survey-controller.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||||
|
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||||
|
import { ServerResponse } from "../models/server-response";
|
||||||
|
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||||
|
import HandleExceptions from "../decorators/handle-exceptions";
|
||||||
|
import { ISurveySubmissionRequest } from "../interfaces/survey";
|
||||||
|
import db from "../config/db";
|
||||||
|
|
||||||
|
export default class SurveyController extends WorklenzControllerBase {
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async getAccountSetupSurvey(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
const q = `
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.name,
|
||||||
|
s.description,
|
||||||
|
s.survey_type,
|
||||||
|
s.is_active,
|
||||||
|
COALESCE(
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'id', sq.id,
|
||||||
|
'survey_id', sq.survey_id,
|
||||||
|
'question_key', sq.question_key,
|
||||||
|
'question_type', sq.question_type,
|
||||||
|
'is_required', sq.is_required,
|
||||||
|
'sort_order', sq.sort_order,
|
||||||
|
'options', sq.options
|
||||||
|
) ORDER BY sq.sort_order
|
||||||
|
) FILTER (WHERE sq.id IS NOT NULL),
|
||||||
|
'[]'
|
||||||
|
) AS questions
|
||||||
|
FROM surveys s
|
||||||
|
LEFT JOIN survey_questions sq ON s.id = sq.survey_id
|
||||||
|
WHERE s.survey_type = 'account_setup' AND s.is_active = true
|
||||||
|
GROUP BY s.id, s.name, s.description, s.survey_type, s.is_active
|
||||||
|
LIMIT 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await db.query(q);
|
||||||
|
const [survey] = result.rows;
|
||||||
|
|
||||||
|
if (!survey) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "Account setup survey not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(new ServerResponse(true, survey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async submitSurveyResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
const body = req.body as ISurveySubmissionRequest;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.survey_id || !body.answers || !Array.isArray(body.answers)) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "Invalid survey submission data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has already submitted a response for this survey
|
||||||
|
const existingResponseQuery = `
|
||||||
|
SELECT id FROM survey_responses
|
||||||
|
WHERE user_id = $1 AND survey_id = $2;
|
||||||
|
`;
|
||||||
|
const existingResult = await db.query(existingResponseQuery, [userId, body.survey_id]);
|
||||||
|
|
||||||
|
let responseId: string;
|
||||||
|
|
||||||
|
if (existingResult.rows.length > 0) {
|
||||||
|
// Update existing response
|
||||||
|
responseId = existingResult.rows[0].id;
|
||||||
|
|
||||||
|
const updateResponseQuery = `
|
||||||
|
UPDATE survey_responses
|
||||||
|
SET is_completed = true, completed_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
|
`;
|
||||||
|
await db.query(updateResponseQuery, [responseId]);
|
||||||
|
|
||||||
|
// Delete existing answers
|
||||||
|
const deleteAnswersQuery = `DELETE FROM survey_answers WHERE response_id = $1;`;
|
||||||
|
await db.query(deleteAnswersQuery, [responseId]);
|
||||||
|
} else {
|
||||||
|
// Create new response
|
||||||
|
const createResponseQuery = `
|
||||||
|
INSERT INTO survey_responses (survey_id, user_id, is_completed, completed_at)
|
||||||
|
VALUES ($1, $2, true, NOW())
|
||||||
|
RETURNING id;
|
||||||
|
`;
|
||||||
|
const responseResult = await db.query(createResponseQuery, [body.survey_id, userId]);
|
||||||
|
responseId = responseResult.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new answers
|
||||||
|
if (body.answers.length > 0) {
|
||||||
|
const answerValues: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
|
||||||
|
body.answers.forEach((answer, index) => {
|
||||||
|
const baseIndex = index * 4;
|
||||||
|
answerValues.push(`($${baseIndex + 1}, $${baseIndex + 2}, $${baseIndex + 3}, $${baseIndex + 4})`);
|
||||||
|
|
||||||
|
params.push(
|
||||||
|
responseId,
|
||||||
|
answer.question_id,
|
||||||
|
answer.answer_text || null,
|
||||||
|
answer.answer_json ? JSON.stringify(answer.answer_json) : null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertAnswersQuery = `
|
||||||
|
INSERT INTO survey_answers (response_id, question_id, answer_text, answer_json)
|
||||||
|
VALUES ${answerValues.join(', ')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db.query(insertAnswersQuery, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(new ServerResponse(true, { response_id: responseId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async getUserSurveyResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
const surveyId = req.params.survey_id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT
|
||||||
|
sr.id,
|
||||||
|
sr.survey_id,
|
||||||
|
sr.user_id,
|
||||||
|
sr.is_completed,
|
||||||
|
sr.started_at,
|
||||||
|
sr.completed_at,
|
||||||
|
COALESCE(
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'question_id', sa.question_id,
|
||||||
|
'answer_text', sa.answer_text,
|
||||||
|
'answer_json', sa.answer_json
|
||||||
|
)
|
||||||
|
) FILTER (WHERE sa.id IS NOT NULL),
|
||||||
|
'[]'
|
||||||
|
) AS answers
|
||||||
|
FROM survey_responses sr
|
||||||
|
LEFT JOIN survey_answers sa ON sr.id = sa.response_id
|
||||||
|
WHERE sr.user_id = $1 AND sr.survey_id = $2
|
||||||
|
GROUP BY sr.id, sr.survey_id, sr.user_id, sr.is_completed, sr.started_at, sr.completed_at;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await db.query(q, [userId, surveyId]);
|
||||||
|
const [response] = result.rows;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "Survey response not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(new ServerResponse(true, response));
|
||||||
|
}
|
||||||
|
}
|
||||||
37
worklenz-backend/src/interfaces/survey.ts
Normal file
37
worklenz-backend/src/interfaces/survey.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface ISurveyQuestion {
|
||||||
|
id: string;
|
||||||
|
survey_id: string;
|
||||||
|
question_key: string;
|
||||||
|
question_type: 'single_choice' | 'multiple_choice' | 'text';
|
||||||
|
is_required: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
options?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurvey {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
survey_type: 'account_setup' | 'onboarding' | 'feedback';
|
||||||
|
is_active: boolean;
|
||||||
|
questions?: ISurveyQuestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurveyAnswer {
|
||||||
|
question_id: string;
|
||||||
|
answer_text?: string;
|
||||||
|
answer_json?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurveyResponse {
|
||||||
|
id?: string;
|
||||||
|
survey_id: string;
|
||||||
|
user_id?: string;
|
||||||
|
is_completed: boolean;
|
||||||
|
answers: ISurveyAnswer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurveySubmissionRequest {
|
||||||
|
survey_id: string;
|
||||||
|
answers: ISurveyAnswer[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { NextFunction } from "express";
|
||||||
|
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||||
|
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||||
|
import { ServerResponse } from "../../models/server-response";
|
||||||
|
import { ISurveySubmissionRequest } from "../../interfaces/survey";
|
||||||
|
|
||||||
|
export default function surveySubmissionValidator(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void {
|
||||||
|
const body = req.body as ISurveySubmissionRequest;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "Request body is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.survey_id || typeof body.survey_id !== 'string') {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "Survey ID is required and must be a string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.answers || !Array.isArray(body.answers)) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, "Answers are required and must be an array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each answer
|
||||||
|
for (let i = 0; i < body.answers.length; i++) {
|
||||||
|
const answer = body.answers[i];
|
||||||
|
|
||||||
|
if (!answer.question_id || typeof answer.question_id !== 'string') {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: Question ID is required and must be a string`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one of answer_text or answer_json should be provided
|
||||||
|
if (!answer.answer_text && !answer.answer_json) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: Either answer_text or answer_json is required`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate answer_text if provided
|
||||||
|
if (answer.answer_text && typeof answer.answer_text !== 'string') {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_text must be a string`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate answer_json if provided
|
||||||
|
if (answer.answer_json && !Array.isArray(answer.answer_json)) {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_json must be an array`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate answer_json items are strings
|
||||||
|
if (answer.answer_json) {
|
||||||
|
for (let j = 0; j < answer.answer_json.length; j++) {
|
||||||
|
if (typeof answer.answer_json[j] !== 'string') {
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_json items must be strings`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
@@ -51,13 +51,14 @@ import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
|
|||||||
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
|
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
|
||||||
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
|
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
|
||||||
import projectManagerApiRouter from "./project-managers-api-router";
|
import projectManagerApiRouter from "./project-managers-api-router";
|
||||||
|
import surveyApiRouter from "./survey-api-router";
|
||||||
|
|
||||||
import billingApiRouter from "./billing-api-router";
|
import billingApiRouter from "./billing-api-router";
|
||||||
import taskDependenciesApiRouter from "./task-dependencies-api-router";
|
import taskDependenciesApiRouter from "./task-dependencies-api-router";
|
||||||
|
|
||||||
import taskRecurringApiRouter from "./task-recurring-api-router";
|
import taskRecurringApiRouter from "./task-recurring-api-router";
|
||||||
|
|
||||||
import customColumnsApiRouter from "./custom-columns-api-router";
|
import customColumnsApiRouter from "./custom-columns-api-router";
|
||||||
|
|
||||||
const api = express.Router();
|
const api = express.Router();
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ api.use("/roadmap-gannt", roadmapApiRouter);
|
|||||||
api.use("/roadmap-gannt", roadmapApiRouter);
|
api.use("/roadmap-gannt", roadmapApiRouter);
|
||||||
api.use("/schedule-gannt", scheduleApiRouter);
|
api.use("/schedule-gannt", scheduleApiRouter);
|
||||||
api.use("/schedule-gannt-v2", scheduleApiV2Router);
|
api.use("/schedule-gannt-v2", scheduleApiV2Router);
|
||||||
|
api.use("/project-managers", projectManagerApiRouter);
|
||||||
api.use("/surveys", surveyApiRouter);
|
api.use("/surveys", surveyApiRouter);
|
||||||
|
|
||||||
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
|
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
|
||||||
@@ -115,6 +117,6 @@ api.use("/task-dependencies", taskDependenciesApiRouter);
|
|||||||
api.use("/task-dependencies", taskDependenciesApiRouter);
|
api.use("/task-dependencies", taskDependenciesApiRouter);
|
||||||
|
|
||||||
api.use("/task-recurring", taskRecurringApiRouter);
|
api.use("/task-recurring", taskRecurringApiRouter);
|
||||||
|
|
||||||
api.use("/custom-columns", customColumnsApiRouter);
|
api.use("/custom-columns", customColumnsApiRouter);
|
||||||
|
|
||||||
|
|||||||
17
worklenz-backend/src/routes/apis/survey-api-router.ts
Normal file
17
worklenz-backend/src/routes/apis/survey-api-router.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import express from "express";
|
||||||
|
import SurveyController from "../../controllers/survey-controller";
|
||||||
|
import surveySubmissionValidator from "../../middlewares/validators/survey-submission-validator";
|
||||||
|
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||||
|
|
||||||
|
const surveyApiRouter = express.Router();
|
||||||
|
|
||||||
|
// Get account setup survey with questions
|
||||||
|
surveyApiRouter.get("/account-setup", safeControllerFunction(SurveyController.getAccountSetupSurvey));
|
||||||
|
|
||||||
|
// Submit survey response
|
||||||
|
surveyApiRouter.post("/responses", surveySubmissionValidator, safeControllerFunction(SurveyController.submitSurveyResponse));
|
||||||
|
|
||||||
|
// Get user's survey response for a specific survey
|
||||||
|
surveyApiRouter.get("/responses/:survey_id", safeControllerFunction(SurveyController.getUserSurveyResponse));
|
||||||
|
|
||||||
|
export default surveyApiRouter;
|
||||||
@@ -27,5 +27,43 @@
|
|||||||
"formTitle": "Krijoni detyrën tuaj të parë.",
|
"formTitle": "Krijoni detyrën tuaj të parë.",
|
||||||
"step3Title": "Fto ekipin tënd të punojë me",
|
"step3Title": "Fto ekipin tënd të punojë me",
|
||||||
"maxMembers": " (Mund të ftoni deri në 5 anëtarë)",
|
"maxMembers": " (Mund të ftoni deri në 5 anëtarë)",
|
||||||
"maxTasks": " (Mund të krijoni deri në 5 detyra)"
|
"maxTasks": " (Mund të krijoni deri në 5 detyra)",
|
||||||
|
|
||||||
|
"surveyStepTitle": "Na tregoni për ju",
|
||||||
|
"surveyStepLabel": "Na ndihmoni të personalizojmë eksperiencën tuaj në Worklenz duke përgjigjur disa pyetjeve.",
|
||||||
|
|
||||||
|
"organizationType": "Cila përshkruan më mirë organizatën tuaj?",
|
||||||
|
"organizationTypeFreelancer": "Freelancer",
|
||||||
|
"organizationTypeStartup": "Startup",
|
||||||
|
"organizationTypeSmallMediumBusiness": "Biznes i Vogël ose i Mesmu",
|
||||||
|
"organizationTypeAgency": "Agjensi",
|
||||||
|
"organizationTypeEnterprise": "Ndërmarrje",
|
||||||
|
"organizationTypeOther": "Tjetër",
|
||||||
|
|
||||||
|
"userRole": "Cili është roli juaj?",
|
||||||
|
"userRoleFounderCeo": "Themeluesi / CEO",
|
||||||
|
"userRoleProjectManager": "Menaxheri i Projektit",
|
||||||
|
"userRoleSoftwareDeveloper": "Zhvilluesi i Software-it",
|
||||||
|
"userRoleDesigner": "Dizajneri",
|
||||||
|
"userRoleOperations": "Operacionet",
|
||||||
|
"userRoleOther": "Tjetër",
|
||||||
|
|
||||||
|
"mainUseCases": "Për çfarë do ta përdorni kryësisht Worklenz?",
|
||||||
|
"mainUseCasesTaskManagement": "Menaxhimi i detyrave",
|
||||||
|
"mainUseCasesTeamCollaboration": "Bashkëpunimi i ekipit",
|
||||||
|
"mainUseCasesResourcePlanning": "Planifikimi i burimeve",
|
||||||
|
"mainUseCasesClientCommunication": "Komunikimi me klientët & raportet",
|
||||||
|
"mainUseCasesTimeTracking": "Ndjekja e kohës",
|
||||||
|
"mainUseCasesOther": "Tjetër",
|
||||||
|
|
||||||
|
"previousTools": "Cilat vegla përdornit para Worklenz?",
|
||||||
|
"previousToolsPlaceholder": "p.sh. Trello, Asana, Monday.com",
|
||||||
|
|
||||||
|
"howHeardAbout": "Si dëgjuat për Worklenz?",
|
||||||
|
"howHeardAboutGoogleSearch": "Kërkimi Google",
|
||||||
|
"howHeardAboutTwitter": "Twitter",
|
||||||
|
"howHeardAboutLinkedin": "LinkedIn",
|
||||||
|
"howHeardAboutFriendColleague": "Një miku ose kolegu",
|
||||||
|
"howHeardAboutBlogArticle": "Një blog ose artikulli",
|
||||||
|
"howHeardAboutOther": "Tjetër"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,43 @@
|
|||||||
"formTitle": "Erstellen Sie Ihre erste Aufgabe.",
|
"formTitle": "Erstellen Sie Ihre erste Aufgabe.",
|
||||||
"step3Title": "Laden Sie Ihr Team zur Zusammenarbeit ein",
|
"step3Title": "Laden Sie Ihr Team zur Zusammenarbeit ein",
|
||||||
"maxMembers": " (Sie können bis zu 5 Mitglieder einladen)",
|
"maxMembers": " (Sie können bis zu 5 Mitglieder einladen)",
|
||||||
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)"
|
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)",
|
||||||
|
|
||||||
|
"surveyStepTitle": "Erzählen Sie uns von sich",
|
||||||
|
"surveyStepLabel": "Helfen Sie uns, Ihre Worklenz-Erfahrung zu personalisieren, indem Sie ein paar Fragen beantworten.",
|
||||||
|
|
||||||
|
"organizationType": "Was beschreibt Ihre Organisation am besten?",
|
||||||
|
"organizationTypeFreelancer": "Freelancer",
|
||||||
|
"organizationTypeStartup": "Startup",
|
||||||
|
"organizationTypeSmallMediumBusiness": "Kleines oder mittleres Unternehmen",
|
||||||
|
"organizationTypeAgency": "Agentur",
|
||||||
|
"organizationTypeEnterprise": "Unternehmen",
|
||||||
|
"organizationTypeOther": "Andere",
|
||||||
|
|
||||||
|
"userRole": "Was ist Ihre Rolle?",
|
||||||
|
"userRoleFounderCeo": "Gründer / CEO",
|
||||||
|
"userRoleProjectManager": "Projektmanager",
|
||||||
|
"userRoleSoftwareDeveloper": "Software-Entwickler",
|
||||||
|
"userRoleDesigner": "Designer",
|
||||||
|
"userRoleOperations": "Betrieb",
|
||||||
|
"userRoleOther": "Andere",
|
||||||
|
|
||||||
|
"mainUseCases": "Wofür werden Sie Worklenz hauptsächlich verwenden?",
|
||||||
|
"mainUseCasesTaskManagement": "Aufgabenverwaltung",
|
||||||
|
"mainUseCasesTeamCollaboration": "Teamzusammenarbeit",
|
||||||
|
"mainUseCasesResourcePlanning": "Ressourcenplanung",
|
||||||
|
"mainUseCasesClientCommunication": "Kundenkommunikation & Berichterstattung",
|
||||||
|
"mainUseCasesTimeTracking": "Zeiterfassung",
|
||||||
|
"mainUseCasesOther": "Andere",
|
||||||
|
|
||||||
|
"previousTools": "Welche Tools haben Sie vor Worklenz verwendet?",
|
||||||
|
"previousToolsPlaceholder": "z.B. Trello, Asana, Monday.com",
|
||||||
|
|
||||||
|
"howHeardAbout": "Wie haben Sie von Worklenz erfahren?",
|
||||||
|
"howHeardAboutGoogleSearch": "Google-Suche",
|
||||||
|
"howHeardAboutTwitter": "Twitter",
|
||||||
|
"howHeardAboutLinkedin": "LinkedIn",
|
||||||
|
"howHeardAboutFriendColleague": "Ein Freund oder Kollege",
|
||||||
|
"howHeardAboutBlogArticle": "Ein Blog oder Artikel",
|
||||||
|
"howHeardAboutOther": "Andere"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,43 @@
|
|||||||
"formTitle": "Create your first task.",
|
"formTitle": "Create your first task.",
|
||||||
"step3Title": "Invite your team to work with",
|
"step3Title": "Invite your team to work with",
|
||||||
"maxMembers": " (You can invite up to 5 members)",
|
"maxMembers": " (You can invite up to 5 members)",
|
||||||
"maxTasks": " (You can create up to 5 tasks)"
|
"maxTasks": " (You can create up to 5 tasks)",
|
||||||
|
|
||||||
|
"surveyStepTitle": "Tell us about yourself",
|
||||||
|
"surveyStepLabel": "Help us personalize your Worklenz experience by answering a few questions.",
|
||||||
|
|
||||||
|
"organizationType": "What best describes your organization?",
|
||||||
|
"organizationTypeFreelancer": "Freelancer",
|
||||||
|
"organizationTypeStartup": "Startup",
|
||||||
|
"organizationTypeSmallMediumBusiness": "Small or Medium Business",
|
||||||
|
"organizationTypeAgency": "Agency",
|
||||||
|
"organizationTypeEnterprise": "Enterprise",
|
||||||
|
"organizationTypeOther": "Other",
|
||||||
|
|
||||||
|
"userRole": "What is your role?",
|
||||||
|
"userRoleFounderCeo": "Founder / CEO",
|
||||||
|
"userRoleProjectManager": "Project Manager",
|
||||||
|
"userRoleSoftwareDeveloper": "Software Developer",
|
||||||
|
"userRoleDesigner": "Designer",
|
||||||
|
"userRoleOperations": "Operations",
|
||||||
|
"userRoleOther": "Other",
|
||||||
|
|
||||||
|
"mainUseCases": "What will you mainly use Worklenz for?",
|
||||||
|
"mainUseCasesTaskManagement": "Task management",
|
||||||
|
"mainUseCasesTeamCollaboration": "Team collaboration",
|
||||||
|
"mainUseCasesResourcePlanning": "Resource planning",
|
||||||
|
"mainUseCasesClientCommunication": "Client communication & reporting",
|
||||||
|
"mainUseCasesTimeTracking": "Time tracking",
|
||||||
|
"mainUseCasesOther": "Other",
|
||||||
|
|
||||||
|
"previousTools": "What tool(s) were you using before Worklenz?",
|
||||||
|
"previousToolsPlaceholder": "e.g. Trello, Asana, Monday.com",
|
||||||
|
|
||||||
|
"howHeardAbout": "How did you hear about Worklenz?",
|
||||||
|
"howHeardAboutGoogleSearch": "Google Search",
|
||||||
|
"howHeardAboutTwitter": "Twitter",
|
||||||
|
"howHeardAboutLinkedin": "LinkedIn",
|
||||||
|
"howHeardAboutFriendColleague": "A friend or colleague",
|
||||||
|
"howHeardAboutBlogArticle": "A blog or article",
|
||||||
|
"howHeardAboutOther": "Other"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,43 @@
|
|||||||
"step3Title": "Invita a tu equipo a trabajar",
|
"step3Title": "Invita a tu equipo a trabajar",
|
||||||
|
|
||||||
"maxMembers": " (Puedes invitar hasta 5 miembros)",
|
"maxMembers": " (Puedes invitar hasta 5 miembros)",
|
||||||
"maxTasks": " (Puedes crear hasta 5 tareas)"
|
"maxTasks": " (Puedes crear hasta 5 tareas)",
|
||||||
|
|
||||||
|
"surveyStepTitle": "Cuéntanos sobre ti",
|
||||||
|
"surveyStepLabel": "Ayúdanos a personalizar tu experiencia de Worklenz respondiendo algunas preguntas.",
|
||||||
|
|
||||||
|
"organizationType": "¿Qué describe mejor tu organización?",
|
||||||
|
"organizationTypeFreelancer": "Freelancer",
|
||||||
|
"organizationTypeStartup": "Startup",
|
||||||
|
"organizationTypeSmallMediumBusiness": "Pequeña o Mediana Empresa",
|
||||||
|
"organizationTypeAgency": "Agencia",
|
||||||
|
"organizationTypeEnterprise": "Empresa",
|
||||||
|
"organizationTypeOther": "Otro",
|
||||||
|
|
||||||
|
"userRole": "¿Cuál es tu rol?",
|
||||||
|
"userRoleFounderCeo": "Fundador / CEO",
|
||||||
|
"userRoleProjectManager": "Gerente de Proyecto",
|
||||||
|
"userRoleSoftwareDeveloper": "Desarrollador de Software",
|
||||||
|
"userRoleDesigner": "Diseñador",
|
||||||
|
"userRoleOperations": "Operaciones",
|
||||||
|
"userRoleOther": "Otro",
|
||||||
|
|
||||||
|
"mainUseCases": "¿Para qué usarás principalmente Worklenz?",
|
||||||
|
"mainUseCasesTaskManagement": "Gestión de tareas",
|
||||||
|
"mainUseCasesTeamCollaboration": "Colaboración de equipo",
|
||||||
|
"mainUseCasesResourcePlanning": "Planificación de recursos",
|
||||||
|
"mainUseCasesClientCommunication": "Comunicación con clientes e informes",
|
||||||
|
"mainUseCasesTimeTracking": "Seguimiento de tiempo",
|
||||||
|
"mainUseCasesOther": "Otro",
|
||||||
|
|
||||||
|
"previousTools": "¿Qué herramienta(s) usabas antes de Worklenz?",
|
||||||
|
"previousToolsPlaceholder": "ej. Trello, Asana, Monday.com",
|
||||||
|
|
||||||
|
"howHeardAbout": "¿Cómo conociste Worklenz?",
|
||||||
|
"howHeardAboutGoogleSearch": "Búsqueda de Google",
|
||||||
|
"howHeardAboutTwitter": "Twitter",
|
||||||
|
"howHeardAboutLinkedin": "LinkedIn",
|
||||||
|
"howHeardAboutFriendColleague": "Un amigo o colega",
|
||||||
|
"howHeardAboutBlogArticle": "Un blog o artículo",
|
||||||
|
"howHeardAboutOther": "Otro"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,43 @@
|
|||||||
"step3Title": "Convide sua equipe para trabalhar",
|
"step3Title": "Convide sua equipe para trabalhar",
|
||||||
|
|
||||||
"maxMembers": " (Você pode convidar até 5 membros)",
|
"maxMembers": " (Você pode convidar até 5 membros)",
|
||||||
"maxTasks": " (Você pode criar até 5 tarefas)"
|
"maxTasks": " (Você pode criar até 5 tarefas)",
|
||||||
|
|
||||||
|
"surveyStepTitle": "Conte-nos sobre você",
|
||||||
|
"surveyStepLabel": "Ajude-nos a personalizar sua experiência no Worklenz respondendo algumas perguntas.",
|
||||||
|
|
||||||
|
"organizationType": "O que melhor descreve sua organização?",
|
||||||
|
"organizationTypeFreelancer": "Freelancer",
|
||||||
|
"organizationTypeStartup": "Startup",
|
||||||
|
"organizationTypeSmallMediumBusiness": "Pequena ou Média Empresa",
|
||||||
|
"organizationTypeAgency": "Agência",
|
||||||
|
"organizationTypeEnterprise": "Empresa",
|
||||||
|
"organizationTypeOther": "Outro",
|
||||||
|
|
||||||
|
"userRole": "Qual é o seu papel?",
|
||||||
|
"userRoleFounderCeo": "Fundador / CEO",
|
||||||
|
"userRoleProjectManager": "Gerente de Projeto",
|
||||||
|
"userRoleSoftwareDeveloper": "Desenvolvedor de Software",
|
||||||
|
"userRoleDesigner": "Designer",
|
||||||
|
"userRoleOperations": "Operações",
|
||||||
|
"userRoleOther": "Outro",
|
||||||
|
|
||||||
|
"mainUseCases": "Para que você usará principalmente o Worklenz?",
|
||||||
|
"mainUseCasesTaskManagement": "Gerenciamento de tarefas",
|
||||||
|
"mainUseCasesTeamCollaboration": "Colaboração em equipe",
|
||||||
|
"mainUseCasesResourcePlanning": "Planejamento de recursos",
|
||||||
|
"mainUseCasesClientCommunication": "Comunicação com clientes e relatórios",
|
||||||
|
"mainUseCasesTimeTracking": "Controle de tempo",
|
||||||
|
"mainUseCasesOther": "Outro",
|
||||||
|
|
||||||
|
"previousTools": "Que ferramenta(s) você usava antes do Worklenz?",
|
||||||
|
"previousToolsPlaceholder": "ex. Trello, Asana, Monday.com",
|
||||||
|
|
||||||
|
"howHeardAbout": "Como você soube do Worklenz?",
|
||||||
|
"howHeardAboutGoogleSearch": "Busca no Google",
|
||||||
|
"howHeardAboutTwitter": "Twitter",
|
||||||
|
"howHeardAboutLinkedin": "LinkedIn",
|
||||||
|
"howHeardAboutFriendColleague": "Um amigo ou colega",
|
||||||
|
"howHeardAboutBlogArticle": "Um blog ou artigo",
|
||||||
|
"howHeardAboutOther": "Outro"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,43 @@
|
|||||||
"formTitle": "创建您的第一个任务。",
|
"formTitle": "创建您的第一个任务。",
|
||||||
"step3Title": "邀请您的团队一起工作",
|
"step3Title": "邀请您的团队一起工作",
|
||||||
"maxMembers": "(您最多可以邀请5名成员)",
|
"maxMembers": "(您最多可以邀请5名成员)",
|
||||||
"maxTasks": "(您最多可以创建5个任务)"
|
"maxTasks": "(您最多可以创建5个任务)",
|
||||||
|
|
||||||
|
"surveyStepTitle": "告诉我们关于您的信息",
|
||||||
|
"surveyStepLabel": "通过回答几个问题帮助我们个性化您的Worklenz体验。",
|
||||||
|
|
||||||
|
"organizationType": "什么最能描述您的组织?",
|
||||||
|
"organizationTypeFreelancer": "自由职业者",
|
||||||
|
"organizationTypeStartup": "初创公司",
|
||||||
|
"organizationTypeSmallMediumBusiness": "中小企业",
|
||||||
|
"organizationTypeAgency": "代理机构",
|
||||||
|
"organizationTypeEnterprise": "企业",
|
||||||
|
"organizationTypeOther": "其他",
|
||||||
|
|
||||||
|
"userRole": "您的角色是什么?",
|
||||||
|
"userRoleFounderCeo": "创始人/CEO",
|
||||||
|
"userRoleProjectManager": "项目经理",
|
||||||
|
"userRoleSoftwareDeveloper": "软件开发者",
|
||||||
|
"userRoleDesigner": "设计师",
|
||||||
|
"userRoleOperations": "运营",
|
||||||
|
"userRoleOther": "其他",
|
||||||
|
|
||||||
|
"mainUseCases": "您主要将Worklenz用于什么?",
|
||||||
|
"mainUseCasesTaskManagement": "任务管理",
|
||||||
|
"mainUseCasesTeamCollaboration": "团队协作",
|
||||||
|
"mainUseCasesResourcePlanning": "资源规划",
|
||||||
|
"mainUseCasesClientCommunication": "客户沟通和报告",
|
||||||
|
"mainUseCasesTimeTracking": "时间跟踪",
|
||||||
|
"mainUseCasesOther": "其他",
|
||||||
|
|
||||||
|
"previousTools": "您在使用Worklenz之前使用什么工具?",
|
||||||
|
"previousToolsPlaceholder": "例如:Trello、Asana、Monday.com",
|
||||||
|
|
||||||
|
"howHeardAbout": "您是如何了解Worklenz的?",
|
||||||
|
"howHeardAboutGoogleSearch": "Google搜索",
|
||||||
|
"howHeardAboutTwitter": "Twitter",
|
||||||
|
"howHeardAboutLinkedin": "LinkedIn",
|
||||||
|
"howHeardAboutFriendColleague": "朋友或同事",
|
||||||
|
"howHeardAboutBlogArticle": "博客或文章",
|
||||||
|
"howHeardAboutOther": "其他"
|
||||||
}
|
}
|
||||||
22
worklenz-frontend/src/api/survey/survey.api.service.ts
Normal file
22
worklenz-frontend/src/api/survey/survey.api.service.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { IServerResponse } from '@/types/common.types';
|
||||||
|
import { ISurvey, ISurveySubmissionRequest, ISurveyResponse } from '@/types/account-setup/survey.types';
|
||||||
|
import apiClient from '../api-client';
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
export const surveyApiService = {
|
||||||
|
async getAccountSetupSurvey(): Promise<IServerResponse<ISurvey>> {
|
||||||
|
const response = await apiClient.get<IServerResponse<ISurvey>>(`${API_BASE_URL}/surveys/account-setup`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitSurveyResponse(data: ISurveySubmissionRequest): Promise<IServerResponse<{ response_id: string }>> {
|
||||||
|
const response = await apiClient.post<IServerResponse<{ response_id: string }>>(`${API_BASE_URL}/surveys/responses`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserSurveyResponse(surveyId: string): Promise<IServerResponse<ISurveyResponse>> {
|
||||||
|
const response = await apiClient.get<IServerResponse<ISurveyResponse>>(`${API_BASE_URL}/surveys/responses/${surveyId}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -13,19 +13,25 @@ interface Props {
|
|||||||
onEnter: () => void;
|
onEnter: () => void;
|
||||||
styles: any;
|
styles: any;
|
||||||
organizationNamePlaceholder: string;
|
organizationNamePlaceholder: string;
|
||||||
|
organizationNameInitialValue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrganizationStep: React.FC<Props> = ({
|
export const OrganizationStep: React.FC<Props> = ({
|
||||||
onEnter,
|
onEnter,
|
||||||
styles,
|
styles,
|
||||||
organizationNamePlaceholder,
|
organizationNamePlaceholder,
|
||||||
|
organizationNameInitialValue,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('account-setup');
|
const { t } = useTranslation('account-setup');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { organizationName } = useSelector((state: RootState) => state.accountSetupReducer);
|
const { organizationName } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||||
const inputRef = useRef<InputRef>(null);
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
// Autofill organization name if not already set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!organizationName && organizationNameInitialValue) {
|
||||||
|
dispatch(setOrganizationName(organizationNameInitialValue));
|
||||||
|
}
|
||||||
setTimeout(() => inputRef.current?.focus(), 300);
|
setTimeout(() => inputRef.current?.focus(), 300);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
210
worklenz-frontend/src/components/account-setup/survey-step.tsx
Normal file
210
worklenz-frontend/src/components/account-setup/survey-step.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form, Input, Typography, Button } from '@/shared/antd-imports';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { setSurveyData } from '@/features/account-setup/account-setup.slice';
|
||||||
|
import { RootState } from '@/app/store';
|
||||||
|
import {
|
||||||
|
OrganizationType,
|
||||||
|
UserRole,
|
||||||
|
UseCase,
|
||||||
|
HowHeardAbout,
|
||||||
|
IAccountSetupSurveyData
|
||||||
|
} from '@/types/account-setup/survey.types';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onEnter: () => void;
|
||||||
|
styles: any;
|
||||||
|
isDarkMode: boolean;
|
||||||
|
token?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token }) => {
|
||||||
|
const { t } = useTranslation('account-setup');
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { surveyData } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||||
|
|
||||||
|
const handleSurveyDataChange = (field: keyof IAccountSetupSurveyData, value: any) => {
|
||||||
|
dispatch(setSurveyData({ [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get Ant Design button type based on selection state
|
||||||
|
const getButtonType = (isSelected: boolean) => {
|
||||||
|
return isSelected ? 'primary' : 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle multi-select for use cases (button-based)
|
||||||
|
const handleUseCaseToggle = (value: UseCase) => {
|
||||||
|
const currentUseCases = surveyData.main_use_cases || [];
|
||||||
|
const isSelected = currentUseCases.includes(value);
|
||||||
|
|
||||||
|
let newUseCases;
|
||||||
|
if (isSelected) {
|
||||||
|
// Remove if already selected
|
||||||
|
newUseCases = currentUseCases.filter(useCase => useCase !== value);
|
||||||
|
} else {
|
||||||
|
// Add if not selected
|
||||||
|
newUseCases = [...currentUseCases, value];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSurveyDataChange('main_use_cases', newUseCases);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPressEnter = () => {
|
||||||
|
onEnter();
|
||||||
|
};
|
||||||
|
|
||||||
|
const organizationTypeOptions: { value: OrganizationType; label: string }[] = [
|
||||||
|
{ value: 'freelancer', label: t('organizationTypeFreelancer') },
|
||||||
|
{ value: 'startup', label: t('organizationTypeStartup') },
|
||||||
|
{ value: 'small_medium_business', label: t('organizationTypeSmallMediumBusiness') },
|
||||||
|
{ value: 'agency', label: t('organizationTypeAgency') },
|
||||||
|
{ value: 'enterprise', label: t('organizationTypeEnterprise') },
|
||||||
|
{ value: 'other', label: t('organizationTypeOther') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const userRoleOptions: { value: UserRole; label: string }[] = [
|
||||||
|
{ value: 'founder_ceo', label: t('userRoleFounderCeo') },
|
||||||
|
{ value: 'project_manager', label: t('userRoleProjectManager') },
|
||||||
|
{ value: 'software_developer', label: t('userRoleSoftwareDeveloper') },
|
||||||
|
{ value: 'designer', label: t('userRoleDesigner') },
|
||||||
|
{ value: 'operations', label: t('userRoleOperations') },
|
||||||
|
{ value: 'other', label: t('userRoleOther') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const useCaseOptions: { value: UseCase; label: string }[] = [
|
||||||
|
{ value: 'task_management', label: t('mainUseCasesTaskManagement') },
|
||||||
|
{ value: 'team_collaboration', label: t('mainUseCasesTeamCollaboration') },
|
||||||
|
{ value: 'resource_planning', label: t('mainUseCasesResourcePlanning') },
|
||||||
|
{ value: 'client_communication', label: t('mainUseCasesClientCommunication') },
|
||||||
|
{ value: 'time_tracking', label: t('mainUseCasesTimeTracking') },
|
||||||
|
{ value: 'other', label: t('mainUseCasesOther') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const howHeardAboutOptions: { value: HowHeardAbout; label: string }[] = [
|
||||||
|
{ value: 'google_search', label: t('howHeardAboutGoogleSearch') },
|
||||||
|
{ value: 'twitter', label: t('howHeardAboutTwitter') },
|
||||||
|
{ value: 'linkedin', label: t('howHeardAboutLinkedin') },
|
||||||
|
{ value: 'friend_colleague', label: t('howHeardAboutFriendColleague') },
|
||||||
|
{ value: 'blog_article', label: t('howHeardAboutBlogArticle') },
|
||||||
|
{ value: 'other', label: t('howHeardAboutOther') },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className="step-form" style={styles.form}>
|
||||||
|
<Form.Item className="mb-6">
|
||||||
|
<Title level={2} className="mb-2 text-2xl" style={{ color: token?.colorText }}>
|
||||||
|
{t('surveyStepTitle')}
|
||||||
|
</Title>
|
||||||
|
<p className="mb-4 text-sm" style={{ color: token?.colorTextSecondary }}>
|
||||||
|
{t('surveyStepLabel')}
|
||||||
|
</p>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Organization Type */}
|
||||||
|
<Form.Item
|
||||||
|
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('organizationType')}</span>}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{organizationTypeOptions.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleSurveyDataChange('organization_type', option.value)}
|
||||||
|
type={getButtonType(surveyData.organization_type === option.value)}
|
||||||
|
size="small"
|
||||||
|
className="h-8"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* User Role */}
|
||||||
|
<Form.Item
|
||||||
|
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('userRole')}</span>}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{userRoleOptions.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleSurveyDataChange('user_role', option.value)}
|
||||||
|
type={getButtonType(surveyData.user_role === option.value)}
|
||||||
|
size="small"
|
||||||
|
className="h-8"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Main Use Cases */}
|
||||||
|
<Form.Item
|
||||||
|
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('mainUseCases')}</span>}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{useCaseOptions.map((option) => {
|
||||||
|
const isSelected = (surveyData.main_use_cases || []).includes(option.value);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleUseCaseToggle(option.value)}
|
||||||
|
type={getButtonType(isSelected)}
|
||||||
|
size="small"
|
||||||
|
className="h-8"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Previous Tools */}
|
||||||
|
<Form.Item
|
||||||
|
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('previousTools')}</span>}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
placeholder={t('previousToolsPlaceholder')}
|
||||||
|
value={surveyData.previous_tools || ''}
|
||||||
|
onChange={(e) => handleSurveyDataChange('previous_tools', e.target.value)}
|
||||||
|
autoSize={{ minRows: 2, maxRows: 3 }}
|
||||||
|
className="mt-2 text-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: token?.colorBgContainer,
|
||||||
|
borderColor: token?.colorBorder,
|
||||||
|
color: token?.colorText
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* How Heard About */}
|
||||||
|
<Form.Item
|
||||||
|
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('howHeardAbout')}</span>}
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{howHeardAboutOptions.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleSurveyDataChange('how_heard_about', option.value)}
|
||||||
|
type={getButtonType(surveyData.how_heard_about === option.value)}
|
||||||
|
size="small"
|
||||||
|
className="h-8"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { IAccountSetupSurveyData } from '@/types/account-setup/survey.types';
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -17,6 +18,7 @@ interface AccountSetupState {
|
|||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
teamMembers: Email[];
|
teamMembers: Email[];
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
|
surveyData: IAccountSetupSurveyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AccountSetupState = {
|
const initialState: AccountSetupState = {
|
||||||
@@ -26,6 +28,7 @@ const initialState: AccountSetupState = {
|
|||||||
tasks: [{ id: 0, value: '' }],
|
tasks: [{ id: 0, value: '' }],
|
||||||
teamMembers: [{ id: 0, value: '' }],
|
teamMembers: [{ id: 0, value: '' }],
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
|
surveyData: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const accountSetupSlice = createSlice({
|
const accountSetupSlice = createSlice({
|
||||||
@@ -50,6 +53,9 @@ const accountSetupSlice = createSlice({
|
|||||||
setCurrentStep: (state, action: PayloadAction<number>) => {
|
setCurrentStep: (state, action: PayloadAction<number>) => {
|
||||||
state.currentStep = action.payload;
|
state.currentStep = action.payload;
|
||||||
},
|
},
|
||||||
|
setSurveyData: (state, action: PayloadAction<Partial<IAccountSetupSurveyData>>) => {
|
||||||
|
state.surveyData = { ...state.surveyData, ...action.payload };
|
||||||
|
},
|
||||||
resetAccountSetup: () => initialState,
|
resetAccountSetup: () => initialState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -61,6 +67,7 @@ export const {
|
|||||||
setTasks,
|
setTasks,
|
||||||
setTeamMembers,
|
setTeamMembers,
|
||||||
setCurrentStep,
|
setCurrentStep,
|
||||||
|
setSurveyData,
|
||||||
resetAccountSetup,
|
resetAccountSetup,
|
||||||
} = accountSetupSlice.actions;
|
} = accountSetupSlice.actions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
/* Steps styling - using Ant Design theme tokens */
|
||||||
.ant-steps-item-finish .ant-steps-item-icon {
|
.ant-steps-item-finish .ant-steps-item-icon {
|
||||||
border-color: #1890ff !important;
|
border-color: var(--ant-color-primary) !important;
|
||||||
|
background-color: var(--ant-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-steps-item-icon {
|
.ant-steps-item-icon {
|
||||||
@@ -9,37 +11,48 @@
|
|||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
line-height: 32px !important;
|
line-height: 32px !important;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.25) !important;
|
border: 1px solid var(--ant-color-border) !important;
|
||||||
border-radius: 32px !important;
|
border-radius: 32px !important;
|
||||||
transition:
|
transition: all 0.3s !important;
|
||||||
background-color 0.3s,
|
background-color: var(--ant-color-bg-container) !important;
|
||||||
border-color 0.3s !important;
|
color: var(--ant-color-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-steps-item-wait .ant-steps-item-icon {
|
.ant-steps-item-wait .ant-steps-item-icon {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
opacity: 0.6;
|
||||||
|
|
||||||
.dark-mode .ant-steps-item-wait .ant-steps-item-icon {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-steps .ant-steps-item.ant-steps-item-process .ant-steps-item-title::after {
|
.progress-steps .ant-steps-item.ant-steps-item-process .ant-steps-item-title::after {
|
||||||
background-color: #1677ff !important;
|
background-color: var(--ant-color-primary) !important;
|
||||||
width: 60px !important;
|
width: 60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Steps title styling */
|
||||||
|
.ant-steps-item-title {
|
||||||
|
color: var(--ant-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-steps-item-description {
|
||||||
|
color: var(--ant-color-text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design improvements */
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.progress-steps {
|
.progress-steps,
|
||||||
width: 400px !important;
|
.step,
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
width: 400px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-content {
|
.step-content {
|
||||||
width: 400px !important;
|
width: 90% !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.progress-steps,
|
||||||
|
.step,
|
||||||
|
.step-content {
|
||||||
|
width: 95% !important;
|
||||||
|
max-width: 400px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,44 +66,51 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.step-content {
|
.step-content {
|
||||||
width: 200px !important;
|
width: 100% !important;
|
||||||
|
max-width: 300px !important;
|
||||||
|
padding: 0 1rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.organization-name-form {
|
.organization-name-form,
|
||||||
width: 400px !important;
|
.first-project-form,
|
||||||
|
.create-first-task-form {
|
||||||
|
width: 90% !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.organization-name-form,
|
||||||
|
.first-project-form,
|
||||||
|
.create-first-task-form {
|
||||||
|
width: 95% !important;
|
||||||
|
max-width: 400px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
.organization-name-form {
|
.organization-name-form,
|
||||||
width: 200px !important;
|
.first-project-form,
|
||||||
|
.create-first-task-form {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 300px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.vert-text {
|
.vert-text {
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
background-color: #fff;
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
margin-left: auto;
|
margin: 2rem auto;
|
||||||
margin-right: auto;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 2rem;
|
align-items: center;
|
||||||
}
|
padding: 0.5rem;
|
||||||
|
|
||||||
.vert-text-dark {
|
|
||||||
max-width: 40px;
|
|
||||||
background-color: #141414;
|
|
||||||
position: relative;
|
|
||||||
z-index: 99;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vert-line {
|
.vert-line {
|
||||||
@@ -98,53 +118,105 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
content: "";
|
height: 1px;
|
||||||
height: 2px;
|
background-color: var(--ant-color-border);
|
||||||
background-color: #00000047;
|
top: 50%;
|
||||||
bottom: 0;
|
transform: translateY(-50%);
|
||||||
top: 0;
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Legacy dark mode classes for backward compatibility */
|
||||||
|
.dark-mode .vert-text,
|
||||||
|
.vert-text-dark {
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .vert-line,
|
||||||
.vert-line-dark {
|
.vert-line-dark {
|
||||||
position: absolute;
|
background-color: var(--ant-color-border);
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
content: "";
|
|
||||||
height: 2px;
|
|
||||||
background-color: white;
|
|
||||||
bottom: 0;
|
|
||||||
top: 0;
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.first-project-form {
|
|
||||||
width: 400px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
.first-project-form {
|
|
||||||
width: 200px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button and component improvements */
|
||||||
.custom-close-button:hover {
|
.custom-close-button:hover {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
.setup-action-buttons {
|
||||||
.create-first-task-form {
|
width: 100%;
|
||||||
width: 400px !important;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
.setup-action-buttons .ant-btn {
|
||||||
.create-first-task-form {
|
min-height: 40px;
|
||||||
width: 200px !important;
|
font-weight: 500;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Form styling with Ant Design theme tokens */
|
||||||
|
.step-form .ant-form-item-label > label {
|
||||||
|
color: var(--ant-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-input,
|
||||||
|
.step-form .ant-input-affix-wrapper {
|
||||||
|
background-color: var(--ant-color-bg-container) !important;
|
||||||
|
border-color: var(--ant-color-border) !important;
|
||||||
|
color: var(--ant-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-input:focus,
|
||||||
|
.step-form .ant-input-affix-wrapper:focus,
|
||||||
|
.step-form .ant-input-affix-wrapper-focused {
|
||||||
|
background-color: var(--ant-color-bg-container) !important;
|
||||||
|
border-color: var(--ant-color-primary) !important;
|
||||||
|
color: var(--ant-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-input::placeholder {
|
||||||
|
color: var(--ant-color-text-placeholder) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select styling */
|
||||||
|
.step-form .ant-select-selector {
|
||||||
|
background-color: var(--ant-color-bg-container) !important;
|
||||||
|
border-color: var(--ant-color-border) !important;
|
||||||
|
color: var(--ant-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-select-selection-placeholder {
|
||||||
|
color: var(--ant-color-text-placeholder) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
.step-form .ant-typography {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-typography.ant-typography-secondary {
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styling */
|
||||||
|
.step-form .ant-card {
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
border-color: var(--ant-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-card-body {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox and Radio styling */
|
||||||
|
.step-form .ant-checkbox-wrapper {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form .ant-radio-wrapper {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions for theme switching */
|
||||||
|
* {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
@@ -2,13 +2,14 @@ import React, { useEffect } from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Space, Steps, Button, Typography } from 'antd/es';
|
import { Space, Steps, Button, Typography, theme } from '@/shared/antd-imports';
|
||||||
|
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { setCurrentStep } from '@/features/account-setup/account-setup.slice';
|
import { setCurrentStep } from '@/features/account-setup/account-setup.slice';
|
||||||
import { OrganizationStep } from '@/components/account-setup/organization-step';
|
import { OrganizationStep } from '@/components/account-setup/organization-step';
|
||||||
import { ProjectStep } from '@/components/account-setup/project-step';
|
import { ProjectStep } from '@/components/account-setup/project-step';
|
||||||
import { TasksStep } from '@/components/account-setup/tasks-step';
|
import { TasksStep } from '@/components/account-setup/tasks-step';
|
||||||
|
import { SurveyStep } from '@/components/account-setup/survey-step';
|
||||||
import MembersStep from '@/components/account-setup/members-step';
|
import MembersStep from '@/components/account-setup/members-step';
|
||||||
import {
|
import {
|
||||||
evt_account_setup_complete,
|
evt_account_setup_complete,
|
||||||
@@ -31,23 +32,104 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|||||||
import './account-setup.css';
|
import './account-setup.css';
|
||||||
import { IAccountSetupRequest } from '@/types/project-templates/project-templates.types';
|
import { IAccountSetupRequest } from '@/types/project-templates/project-templates.types';
|
||||||
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
|
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
|
||||||
|
import { surveyApiService } from '@/api/survey/survey.api.service';
|
||||||
|
import { ISurveySubmissionRequest, ISurveyAnswer } from '@/types/account-setup/survey.types';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
// Simplified styles for form components using Ant Design theme tokens
|
||||||
|
const getAccountSetupStyles = (token: any) => ({
|
||||||
|
form: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '600px',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: token.colorText,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
|
drawerFooter: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'right',
|
||||||
|
padding: '10px 16px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const AccountSetup: React.FC = () => {
|
const AccountSetup: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation('account-setup');
|
const { t } = useTranslation('account-setup');
|
||||||
useDocumentTitle(t('setupYourAccount', 'Account Setup'));
|
useDocumentTitle(t('setupYourAccount', 'Account Setup'));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
const { currentStep, organizationName, projectName, templateId, tasks, teamMembers } =
|
const { currentStep, organizationName, projectName, templateId, tasks, teamMembers, surveyData } =
|
||||||
useSelector((state: RootState) => state.accountSetupReducer);
|
useSelector((state: RootState) => state.accountSetupReducer);
|
||||||
const userDetails = getUserSession();
|
const userDetails = getUserSession();
|
||||||
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
|
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
|
||||||
|
|
||||||
|
const [surveyId, setSurveyId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const isDarkMode = themeMode === 'dark';
|
const isDarkMode = themeMode === 'dark';
|
||||||
const organizationNamePlaceholder = userDetails?.name ? `e.g. ${userDetails?.name}'s Team` : '';
|
// Helper to extract organization name from email or fallback to user name
|
||||||
|
function getOrganizationNamePlaceholder(userDetails: { email?: string; name?: string } | null): string {
|
||||||
|
if (!userDetails) return '';
|
||||||
|
const email = userDetails.email || '';
|
||||||
|
const name = userDetails.name || '';
|
||||||
|
if (email) {
|
||||||
|
const match = email.match(/^([^@]+)@([^@]+)$/);
|
||||||
|
if (match) {
|
||||||
|
const domain = match[2].toLowerCase();
|
||||||
|
// List of common public email providers
|
||||||
|
const publicProviders = [
|
||||||
|
'gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com', 'aol.com', 'protonmail.com', 'zoho.com', 'gmx.com', 'mail.com', 'yandex.com', 'msn.com', 'live.com', 'me.com', 'comcast.net', 'rediffmail.com', 'ymail.com', 'rocketmail.com', 'inbox.com', 'mail.ru', 'qq.com', 'naver.com', '163.com', '126.com', 'sina.com', 'yeah.net', 'googlemail.com', 'fastmail.com', 'hushmail.com', 'tutanota.com', 'pm.me', 'mailbox.org', 'proton.me'
|
||||||
|
];
|
||||||
|
if (!publicProviders.includes(domain)) {
|
||||||
|
// Use the first part of the domain (before the first dot)
|
||||||
|
const org = domain.split('.')[0];
|
||||||
|
if (org && org.length > 1) {
|
||||||
|
return `e.g. ${org.charAt(0).toUpperCase() + org.slice(1)} Team`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to user name
|
||||||
|
return name ? `e.g. ${name}'s Team` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizationNamePlaceholder = getOrganizationNamePlaceholder(userDetails);
|
||||||
|
|
||||||
|
// Helper to extract organization name from email or fallback to user name
|
||||||
|
function getOrganizationNameInitialValue(userDetails: { email?: string; name?: string } | null): string {
|
||||||
|
if (!userDetails) return '';
|
||||||
|
const email = userDetails.email || '';
|
||||||
|
const name = userDetails.name || '';
|
||||||
|
if (email) {
|
||||||
|
const match = email.match(/^([^@]+)@([^@]+)$/);
|
||||||
|
if (match) {
|
||||||
|
const domain = match[2].toLowerCase();
|
||||||
|
const publicProviders = [
|
||||||
|
'gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com', 'aol.com', 'protonmail.com', 'zoho.com', 'gmx.com', 'mail.com', 'yandex.com', 'msn.com', 'live.com', 'me.com', 'comcast.net', 'rediffmail.com', 'ymail.com', 'rocketmail.com', 'inbox.com', 'mail.ru', 'qq.com', 'naver.com', '163.com', '126.com', 'sina.com', 'yeah.net', 'googlemail.com', 'fastmail.com', 'hushmail.com', 'tutanota.com', 'pm.me', 'mailbox.org', 'proton.me'
|
||||||
|
];
|
||||||
|
if (!publicProviders.includes(domain)) {
|
||||||
|
const org = domain.split('.')[0];
|
||||||
|
if (org && org.length > 1) {
|
||||||
|
return org.charAt(0).toUpperCase() + org.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizationNameInitialValue = getOrganizationNameInitialValue(userDetails);
|
||||||
|
|
||||||
|
const styles = getAccountSetupStyles(token);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trackMixpanelEvent(evt_account_setup_visit);
|
trackMixpanelEvent(evt_account_setup_visit);
|
||||||
@@ -65,88 +147,25 @@ const AccountSetup: React.FC = () => {
|
|||||||
logger.error('Failed to verify authentication status', error);
|
logger.error('Failed to verify authentication status', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadSurvey = async () => {
|
||||||
|
try {
|
||||||
|
const response = await surveyApiService.getAccountSetupSurvey();
|
||||||
|
if (response.done && response.body) {
|
||||||
|
setSurveyId(response.body.id);
|
||||||
|
} else {
|
||||||
|
logger.error('Survey not found or inactive (warn replaced with error)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to load survey', error);
|
||||||
|
// Continue without survey - don't block account setup
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
void verifyAuthStatus();
|
void verifyAuthStatus();
|
||||||
|
void loadSurvey();
|
||||||
}, [dispatch, navigate, trackMixpanelEvent]);
|
}, [dispatch, navigate, trackMixpanelEvent]);
|
||||||
|
|
||||||
const calculateHeight = () => {
|
|
||||||
if (currentStep === 2) {
|
|
||||||
return tasks.length * 105;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStep === 3) {
|
|
||||||
return teamMembers.length * 105;
|
|
||||||
}
|
|
||||||
return 'min-content';
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
form: {
|
|
||||||
width: '600px',
|
|
||||||
paddingBottom: '1rem',
|
|
||||||
marginTop: '3rem',
|
|
||||||
height: '100%',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
color: isDarkMode ? '' : '#00000073',
|
|
||||||
fontWeight: 500,
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: '1rem',
|
|
||||||
},
|
|
||||||
drawerFooter: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'right',
|
|
||||||
padding: '10px 16px',
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
height: '100vh',
|
|
||||||
width: '100vw',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column' as const,
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '3rem 0',
|
|
||||||
backgroundColor: isDarkMode ? 'black' : '#FAFAFA',
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
backgroundColor: isDarkMode ? '#141414' : 'white',
|
|
||||||
marginTop: '1.5rem',
|
|
||||||
paddingTop: '3rem',
|
|
||||||
margin: '1.5rem auto 0',
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: '66.66667%',
|
|
||||||
minHeight: 'fit-content',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column' as const,
|
|
||||||
},
|
|
||||||
space: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column' as const,
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0',
|
|
||||||
flexGrow: 1,
|
|
||||||
width: '100%',
|
|
||||||
minHeight: 'fit-content',
|
|
||||||
},
|
|
||||||
steps: {
|
|
||||||
margin: '1rem 0',
|
|
||||||
width: '600px',
|
|
||||||
},
|
|
||||||
stepContent: {
|
|
||||||
flexGrow: 1,
|
|
||||||
width: '600px',
|
|
||||||
minHeight: calculateHeight(),
|
|
||||||
overflow: 'visible',
|
|
||||||
},
|
|
||||||
actionButtons: {
|
|
||||||
flexGrow: 1,
|
|
||||||
width: '600px',
|
|
||||||
marginBottom: '1rem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeAccountSetup = async (skip = false) => {
|
const completeAccountSetup = async (skip = false) => {
|
||||||
try {
|
try {
|
||||||
@@ -159,6 +178,13 @@ const AccountSetup: React.FC = () => {
|
|||||||
: teamMembers
|
: teamMembers
|
||||||
.map(teamMember => sanitizeInput(teamMember.value.trim()))
|
.map(teamMember => sanitizeInput(teamMember.value.trim()))
|
||||||
.filter(email => validateEmail(email)),
|
.filter(email => validateEmail(email)),
|
||||||
|
survey_data: {
|
||||||
|
organization_type: surveyData.organization_type,
|
||||||
|
user_role: surveyData.user_role,
|
||||||
|
main_use_cases: surveyData.main_use_cases,
|
||||||
|
previous_tools: surveyData.previous_tools,
|
||||||
|
how_heard_about: surveyData.how_heard_about,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const res = await profileSettingsApiService.setupAccount(model);
|
const res = await profileSettingsApiService.setupAccount(model);
|
||||||
if (res.done && res.body.id) {
|
if (res.done && res.body.id) {
|
||||||
@@ -190,6 +216,20 @@ const AccountSetup: React.FC = () => {
|
|||||||
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
organizationNamePlaceholder={organizationNamePlaceholder}
|
organizationNamePlaceholder={organizationNamePlaceholder}
|
||||||
|
organizationNameInitialValue={organizationNameInitialValue}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
content: (
|
||||||
|
<SurveyStep
|
||||||
|
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
||||||
|
styles={styles}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
token={token}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -200,6 +240,7 @@ const AccountSetup: React.FC = () => {
|
|||||||
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
|
token={token}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -210,12 +251,13 @@ const AccountSetup: React.FC = () => {
|
|||||||
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
|
token={token}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
content: <MembersStep isDarkMode={isDarkMode} styles={styles} />,
|
content: <MembersStep isDarkMode={isDarkMode} styles={styles} token={token} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -224,10 +266,13 @@ const AccountSetup: React.FC = () => {
|
|||||||
case 0:
|
case 0:
|
||||||
return !organizationName?.trim();
|
return !organizationName?.trim();
|
||||||
case 1:
|
case 1:
|
||||||
return !projectName?.trim() && !templateId;
|
// Survey step - no required fields, can always continue
|
||||||
|
return false;
|
||||||
case 2:
|
case 2:
|
||||||
return tasks.length === 0 || tasks.every(task => !task.value?.trim());
|
return !projectName?.trim() && !templateId;
|
||||||
case 3:
|
case 3:
|
||||||
|
return tasks.length === 0 || tasks.every(task => !task.value?.trim());
|
||||||
|
case 4:
|
||||||
return (
|
return (
|
||||||
teamMembers.length > 0 && !teamMembers.some(member => validateEmail(member.value?.trim()))
|
teamMembers.length > 0 && !teamMembers.some(member => validateEmail(member.value?.trim()))
|
||||||
);
|
);
|
||||||
@@ -236,8 +281,99 @@ const AccountSetup: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextStep = () => {
|
const saveSurveyData = async () => {
|
||||||
if (currentStep === 3) {
|
if (!surveyId || !surveyData) {
|
||||||
|
logger.error('Skipping survey save - no survey ID or data (info replaced with error)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const answers: ISurveyAnswer[] = [];
|
||||||
|
|
||||||
|
// Get the survey questions to map data properly
|
||||||
|
const surveyResponse = await surveyApiService.getAccountSetupSurvey();
|
||||||
|
if (!surveyResponse.done || !surveyResponse.body?.questions) {
|
||||||
|
logger.error('Could not retrieve survey questions for data mapping (warn replaced with error)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questions = surveyResponse.body.questions;
|
||||||
|
|
||||||
|
// Map survey data to answers based on question keys
|
||||||
|
questions.forEach(question => {
|
||||||
|
switch (question.question_key) {
|
||||||
|
case 'organization_type':
|
||||||
|
if (surveyData.organization_type) {
|
||||||
|
answers.push({
|
||||||
|
question_id: question.id,
|
||||||
|
answer_text: surveyData.organization_type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'user_role':
|
||||||
|
if (surveyData.user_role) {
|
||||||
|
answers.push({
|
||||||
|
question_id: question.id,
|
||||||
|
answer_text: surveyData.user_role
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'main_use_cases':
|
||||||
|
if (surveyData.main_use_cases && surveyData.main_use_cases.length > 0) {
|
||||||
|
answers.push({
|
||||||
|
question_id: question.id,
|
||||||
|
answer_json: surveyData.main_use_cases
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'previous_tools':
|
||||||
|
if (surveyData.previous_tools) {
|
||||||
|
answers.push({
|
||||||
|
question_id: question.id,
|
||||||
|
answer_text: surveyData.previous_tools
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'how_heard_about':
|
||||||
|
if (surveyData.how_heard_about) {
|
||||||
|
answers.push({
|
||||||
|
question_id: question.id,
|
||||||
|
answer_text: surveyData.how_heard_about
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answers.length > 0) {
|
||||||
|
const submissionData: ISurveySubmissionRequest = {
|
||||||
|
survey_id: surveyId,
|
||||||
|
answers
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await surveyApiService.submitSurveyResponse(submissionData);
|
||||||
|
if (result.done) {
|
||||||
|
logger.error('Survey data saved successfully (info replaced with error)');
|
||||||
|
} else {
|
||||||
|
logger.error('Survey submission returned unsuccessful response (warn replaced with error)');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error('No survey answers to save (info replaced with error)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to save survey data', error);
|
||||||
|
// Don't block account setup flow if survey fails
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = async () => {
|
||||||
|
if (currentStep === 1) {
|
||||||
|
// Save survey data when moving from survey step
|
||||||
|
await saveSurveyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep === 4) {
|
||||||
|
// Complete setup after members step
|
||||||
completeAccountSetup();
|
completeAccountSetup();
|
||||||
} else {
|
} else {
|
||||||
dispatch(setCurrentStep(currentStep + 1));
|
dispatch(setCurrentStep(currentStep + 1));
|
||||||
@@ -245,46 +381,71 @@ const AccountSetup: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div
|
||||||
<div>
|
className="min-h-screen w-full flex flex-col items-center py-8 px-4"
|
||||||
|
style={{ backgroundColor: token.colorBgLayout }}
|
||||||
|
>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="mb-4">
|
||||||
<img src={isDarkMode ? logoDark : logo} alt="Logo" width={235} height={50} />
|
<img src={isDarkMode ? logoDark : logo} alt="Logo" width={235} height={50} />
|
||||||
</div>
|
</div>
|
||||||
<Title level={5} style={{ textAlign: 'center', margin: '4px 0 24px' }}>
|
|
||||||
|
{/* Title */}
|
||||||
|
<Title
|
||||||
|
level={3}
|
||||||
|
className="text-center mb-6 font-semibold"
|
||||||
|
style={{ color: token.colorText }}
|
||||||
|
>
|
||||||
{t('setupYourAccount')}
|
{t('setupYourAccount')}
|
||||||
</Title>
|
</Title>
|
||||||
<div style={styles.contentContainer}>
|
|
||||||
<Space className={isDarkMode ? 'dark-mode' : ''} style={styles.space} direction="vertical">
|
{/* Content Container */}
|
||||||
<Steps
|
<div
|
||||||
className={isContinueDisabled() ? 'step' : 'progress-steps'}
|
className="w-full max-w-4xl rounded-lg shadow-lg mt-6 p-8"
|
||||||
current={currentStep}
|
style={{
|
||||||
items={steps}
|
backgroundColor: token.colorBgContainer,
|
||||||
style={styles.steps}
|
borderColor: token.colorBorder,
|
||||||
/>
|
border: `1px solid ${token.colorBorder}`,
|
||||||
<div className="step-content" style={styles.stepContent}>
|
boxShadow: token.boxShadowTertiary
|
||||||
{steps[currentStep].content}
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center space-y-6 w-full">
|
||||||
|
{/* Steps */}
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<Steps
|
||||||
|
className={`${isContinueDisabled() ? 'step' : 'progress-steps'} ${isDarkMode ? 'dark-mode' : 'light-mode'}`}
|
||||||
|
current={currentStep}
|
||||||
|
items={steps}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.actionButtons} className="setup-action-buttons">
|
|
||||||
<div
|
{/* Step Content */}
|
||||||
style={{
|
<div className="w-full max-w-2xl flex flex-col items-center min-h-fit">
|
||||||
display: 'flex',
|
<div className="step-content w-full">
|
||||||
justifyContent: currentStep !== 0 ? 'space-between' : 'flex-end',
|
{steps[currentStep].content}
|
||||||
}}
|
</div>
|
||||||
>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="w-full max-w-2xl mt-8">
|
||||||
|
<div className={`flex ${
|
||||||
|
currentStep !== 0 ? 'justify-between' : 'justify-end'
|
||||||
|
} items-center`}>
|
||||||
{currentStep !== 0 && (
|
{currentStep !== 0 && (
|
||||||
<div>
|
<div className="flex flex-col space-y-2">
|
||||||
<Button
|
<Button
|
||||||
style={{ padding: 0 }}
|
|
||||||
type="link"
|
type="link"
|
||||||
className="my-7"
|
className="p-0 font-medium"
|
||||||
|
style={{ color: token.colorTextSecondary }}
|
||||||
onClick={() => dispatch(setCurrentStep(currentStep - 1))}
|
onClick={() => dispatch(setCurrentStep(currentStep - 1))}
|
||||||
>
|
>
|
||||||
{t('goBack')}
|
{t('goBack')}
|
||||||
</Button>
|
</Button>
|
||||||
{currentStep === 3 && (
|
{currentStep === 4 && (
|
||||||
<Button
|
<Button
|
||||||
style={{ color: isDarkMode ? '' : '#00000073', fontWeight: 500 }}
|
|
||||||
type="link"
|
type="link"
|
||||||
className="my-7"
|
className="p-0 font-medium"
|
||||||
|
style={{ color: token.colorTextTertiary }}
|
||||||
onClick={() => completeAccountSetup(true)}
|
onClick={() => completeAccountSetup(true)}
|
||||||
>
|
>
|
||||||
{t('skipForNow')}
|
{t('skipForNow')}
|
||||||
@@ -296,14 +457,14 @@ const AccountSetup: React.FC = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
disabled={isContinueDisabled()}
|
disabled={isContinueDisabled()}
|
||||||
className="mt-7 mb-7"
|
className="min-h-10 font-medium px-8"
|
||||||
onClick={nextStep}
|
onClick={nextStep}
|
||||||
>
|
>
|
||||||
{t('continue')}
|
{t('continue')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import {
|
|||||||
Timeline,
|
Timeline,
|
||||||
Mentions,
|
Mentions,
|
||||||
Radio,
|
Radio,
|
||||||
|
Steps
|
||||||
} from 'antd/es';
|
} from 'antd/es';
|
||||||
|
|
||||||
// Icons - Import commonly used ones
|
// Icons - Import commonly used ones
|
||||||
@@ -240,6 +241,7 @@ export {
|
|||||||
Timeline,
|
Timeline,
|
||||||
Mentions,
|
Mentions,
|
||||||
Radio,
|
Radio,
|
||||||
|
Steps
|
||||||
};
|
};
|
||||||
|
|
||||||
// TypeScript Types - Import commonly used ones
|
// TypeScript Types - Import commonly used ones
|
||||||
|
|||||||
51
worklenz-frontend/src/types/account-setup/survey.types.ts
Normal file
51
worklenz-frontend/src/types/account-setup/survey.types.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export interface ISurveyQuestion {
|
||||||
|
id: string;
|
||||||
|
survey_id: string;
|
||||||
|
question_key: string;
|
||||||
|
question_type: 'single_choice' | 'multiple_choice' | 'text';
|
||||||
|
is_required: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
options?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurvey {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
survey_type: 'account_setup' | 'onboarding' | 'feedback';
|
||||||
|
is_active: boolean;
|
||||||
|
questions?: ISurveyQuestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurveyAnswer {
|
||||||
|
question_id: string;
|
||||||
|
answer_text?: string;
|
||||||
|
answer_json?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurveyResponse {
|
||||||
|
id?: string;
|
||||||
|
survey_id: string;
|
||||||
|
user_id?: string;
|
||||||
|
is_completed: boolean;
|
||||||
|
answers: ISurveyAnswer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISurveySubmissionRequest {
|
||||||
|
survey_id: string;
|
||||||
|
answers: ISurveyAnswer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account setup survey specific types
|
||||||
|
export type OrganizationType = 'freelancer' | 'startup' | 'small_medium_business' | 'agency' | 'enterprise' | 'other';
|
||||||
|
export type UserRole = 'founder_ceo' | 'project_manager' | 'software_developer' | 'designer' | 'operations' | 'other';
|
||||||
|
export type UseCase = 'task_management' | 'team_collaboration' | 'resource_planning' | 'client_communication' | 'time_tracking' | 'other';
|
||||||
|
export type HowHeardAbout = 'google_search' | 'twitter' | 'linkedin' | 'friend_colleague' | 'blog_article' | 'other';
|
||||||
|
|
||||||
|
export interface IAccountSetupSurveyData {
|
||||||
|
organization_type?: OrganizationType;
|
||||||
|
user_role?: UserRole;
|
||||||
|
main_use_cases?: UseCase[];
|
||||||
|
previous_tools?: string;
|
||||||
|
how_heard_about?: HowHeardAbout;
|
||||||
|
}
|
||||||
@@ -52,6 +52,13 @@ export interface IAccountSetupRequest {
|
|||||||
tasks: string[];
|
tasks: string[];
|
||||||
team_members: string[];
|
team_members: string[];
|
||||||
template_id?: string | null;
|
template_id?: string | null;
|
||||||
|
survey_data?: {
|
||||||
|
organization_type?: string;
|
||||||
|
user_role?: string;
|
||||||
|
main_use_cases?: string[];
|
||||||
|
previous_tools?: string;
|
||||||
|
how_heard_about?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountSetupResponse {
|
export interface IAccountSetupResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user