diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b6f137d5..fc3b4e38 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,6 +5,8 @@ "Bash(npm run build:*)", "Bash(npm run type-check:*)", "Bash(npm run:*)", + "Bash(move:*)", + "Bash(mv:*)", "Bash(grep:*)", "Bash(rm:*)" ], diff --git a/worklenz-backend/database/migrations/release-v2.1.4/20250724000000-add-survey-tables.sql b/worklenz-backend/database/migrations/release-v2.1.4/20250724000000-add-survey-tables.sql new file mode 100644 index 00000000..ad779ae3 --- /dev/null +++ b/worklenz-backend/database/migrations/release-v2.1.4/20250724000000-add-survey-tables.sql @@ -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; \ No newline at end of file diff --git a/worklenz-backend/src/controllers/survey-controller.ts b/worklenz-backend/src/controllers/survey-controller.ts new file mode 100644 index 00000000..10bcc29e --- /dev/null +++ b/worklenz-backend/src/controllers/survey-controller.ts @@ -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 { + 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 { + 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 { + 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)); + } +} \ No newline at end of file diff --git a/worklenz-backend/src/interfaces/survey.ts b/worklenz-backend/src/interfaces/survey.ts new file mode 100644 index 00000000..8cb3f5a9 --- /dev/null +++ b/worklenz-backend/src/interfaces/survey.ts @@ -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[]; +} \ No newline at end of file diff --git a/worklenz-backend/src/middlewares/validators/survey-submission-validator.ts b/worklenz-backend/src/middlewares/validators/survey-submission-validator.ts new file mode 100644 index 00000000..a9f75bdb --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/survey-submission-validator.ts @@ -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(); +} \ No newline at end of file diff --git a/worklenz-backend/src/public/locales/en/account-setup.json b/worklenz-backend/src/public/locales/en/account-setup.json index 5e71ca40..4310e0c6 100644 --- a/worklenz-backend/src/public/locales/en/account-setup.json +++ b/worklenz-backend/src/public/locales/en/account-setup.json @@ -1,7 +1,7 @@ { "continue": "Continue", - "setupYourAccount": "Setup Your Worklenz Account.", + "setupYourAccount": "Setup Your Account.", "organizationStepTitle": "Name Your Organization", "organizationStepLabel": "Pick a name for your Worklenz account.", diff --git a/worklenz-backend/src/routes/apis/index.ts b/worklenz-backend/src/routes/apis/index.ts index 5a2019c8..7bd13eec 100644 --- a/worklenz-backend/src/routes/apis/index.ts +++ b/worklenz-backend/src/routes/apis/index.ts @@ -51,13 +51,14 @@ import roadmapApiRouter from "./gannt-apis/roadmap-api-router"; import scheduleApiRouter from "./gannt-apis/schedule-api-router"; import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router"; import projectManagerApiRouter from "./project-managers-api-router"; +import surveyApiRouter from "./survey-api-router"; import billingApiRouter from "./billing-api-router"; import taskDependenciesApiRouter from "./task-dependencies-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(); api.use("/projects", projectsApiRouter); @@ -103,6 +104,7 @@ api.use("/roadmap-gannt", roadmapApiRouter); api.use("/schedule-gannt", scheduleApiRouter); api.use("/schedule-gannt-v2", scheduleApiV2Router); api.use("/project-managers", projectManagerApiRouter); +api.use("/surveys", surveyApiRouter); api.get("/overview/:id", safeControllerFunction(OverviewController.getById)); api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get)); @@ -115,6 +117,6 @@ api.use("/task-dependencies", taskDependenciesApiRouter); api.use("/task-recurring", taskRecurringApiRouter); -api.use("/custom-columns", customColumnsApiRouter); - +api.use("/custom-columns", customColumnsApiRouter); + export default api; diff --git a/worklenz-backend/src/routes/apis/survey-api-router.ts b/worklenz-backend/src/routes/apis/survey-api-router.ts new file mode 100644 index 00000000..dbcde5a4 --- /dev/null +++ b/worklenz-backend/src/routes/apis/survey-api-router.ts @@ -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; \ No newline at end of file diff --git a/worklenz-frontend/public/locales/alb/account-setup.json b/worklenz-frontend/public/locales/alb/account-setup.json index d5f624b3..2f811092 100644 --- a/worklenz-frontend/public/locales/alb/account-setup.json +++ b/worklenz-frontend/public/locales/alb/account-setup.json @@ -1,31 +1,191 @@ { "continue": "Vazhdo", - "setupYourAccount": "Konfiguro Llogarinë Tënde në Worklenz.", - "organizationStepTitle": "Emërtoni Organizatën Tuaj", - "organizationStepLabel": "Zgjidhni një emër për llogarinë tuaj në Worklenz.", - - "projectStepTitle": "Krijoni projektin tuaj të parë", - "projectStepLabel": "Në cilin projekt po punoni aktualisht?", + "setupYourAccount": "Konfiguro llogarinë tënde.", + "organizationStepTitle": "Emërto organizatën tënde", + "organizationStepLabel": "Zgjidh një emër për llogarinë tënde në Worklenz.", + "organizationStepWelcome": "Konfiguro llogarinë tënde në Worklenz.", + "organizationStepDescription": "Le të fillojmë duke konfiguruar organizatën tënde. Kjo do të jetë hapësira kryesore e punës për ekipin tënd.", + "organizationStepTooltip": "Ky emër do të shfaqet në hapësirën tënde të punës dhe mund të ndryshohet më vonë në cilësime.", + "organizationStepNeedIdeas": "Keni nevojë për ide?", + "organizationStepUseDetected": "Përdorimi i zbuluar:", + "organizationStepCharacters": "karaktere", + "organizationStepGoodLength": "Gjatësi e mirë", + "organizationStepTooShort": "Shumë i shkurtër", + "organizationStepNamingTips": "Këshilla për emërtimin", + "organizationStepTip1": "Mbaje të thjeshtë dhe të lehtë për t'u mbajtur mend", + "organizationStepTip2": "Përfaqëso industrinë ose vlerat e tua", + "organizationStepTip3": "Mendo për rritjen në të ardhmen", + "organizationStepTip4": "Bëje unik dhe të përshtatshëm për markë", + "organizationStepSuggestionsTitle": "Sugjerime për emra", + "organizationStepCategory1": "Kompani Teknologjie", + "organizationStepCategory2": "Agjenci Kreative", + "organizationStepCategory3": "Konsulencë", + "organizationStepCategory4": "Startupe", + "organizationStepSuggestionsNote": "Këto janë vetëm shembuj për të të ndihmuar të fillosh. Zgjidh diçka që përfaqëson organizatën tënde.", + "organizationStepPrivacyNote": "Emri i organizatës tënde është privat dhe i dukshëm vetëm për anëtarët e ekipit.", + "projectStepTitle": "Krijo projektin tënd të parë", + "projectStepLabel": "Në cilin projekt po punon tani?", "projectStepPlaceholder": "p.sh. Plani i Marketingut", - - "tasksStepTitle": "Krijoni detyrat tuaja të para", - "tasksStepLabel": "Shkruani disa detyra që do të kryeni në", + "tasksStepTitle": "Krijo detyrat e tua të para", + "tasksStepLabel": "Shkruaj disa detyra që do të kryesh në", "tasksStepAddAnother": "Shto një tjetër", - - "emailPlaceholder": "Adresa email", - "invalidEmail": "Ju lutemi vendosni një adresë email të vlefshme", + "emailPlaceholder": "Adresa e emailit", + "invalidEmail": "Ju lutem vendosni një adresë emaili të vlefshme", "or": "ose", "templateButton": "Importo nga shablloni", - "goBack": "Kthehu Mbrapa", + "goBack": "Kthehu mbrapa", "cancel": "Anulo", "create": "Krijo", "templateDrawerTitle": "Zgjidh nga shabllonet", "step3InputLabel": "Fto me email", "addAnother": "Shto një tjetër", - "skipForNow": "Kalo tani për tani", - "formTitle": "Krijoni detyrën tuaj të parë.", - "step3Title": "Fto ekipin tënd të punojë me", + "skipForNow": "Kalo për tani", + "formTitle": "Krijo detyrën tënde të parë.", + "step3Title": "Fto ekipin tënd për të punuar së bashku", "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)", + "membersStepTitle": "Fto ekipin tënd", + "membersStepDescription": "Shto anëtarë ekipi në \"{{organizationName}}\" dhe filloni bashkëpunimin", + "memberPlaceholder": "Anëtari i ekipit {{index}} - Shkruani adresën e emailit", + "validEmailAddress": "Adresë emaili e vlefshme", + "addAnotherTeamMember": "Shto një anëtar tjetër të ekipit ({{current}}/{{max}})", + "canInviteLater": "Gjithmonë mund të ftoni anëtarë të ekipit më vonë", + "skipStepDescription": "Nuk i keni adresat e emailit gati? Asnjë problem! Mund ta kaloni këtë hap dhe të ftoni anëtarë nga paneli i projektit më vonë.", + "orgCategoryTech": "Kompani Teknologjie", + "orgCategoryCreative": "Agjenci Kreative", + "orgCategoryConsulting": "Konsulencë", + "orgCategoryStartups": "Startupe", + "namingTip1": "Mbaje të thjeshtë dhe të lehtë për t'u mbajtur mend", + "namingTip2": "Përfaqëso industrinë ose vlerat e tua", + "namingTip3": "Mendo për rritjen në të ardhmen", + "namingTip4": "Bëje unik dhe të përshtatshëm për markë", + "aboutYouTitle": "Na trego për veten tënde", + "aboutYouDescription": "Na ndihmo të personalizojmë përvojën tënde", + "orgTypeQuestion": "Cila përshkruan më mirë organizatën tënde?", + "userRoleQuestion": "Cili është roli yt?", + "yourNeedsTitle": "Cilat janë nevojat e tua kryesore?", + "yourNeedsDescription": "Zgjidh të gjitha që aplikohen për të na ndihmuar të konfigurojmë hapësirën tënde të punës", + "yourNeedsQuestion": "Si do ta përdorësh kryesisht Worklenz?", + "useCaseTaskOrg": "Organizo dhe ndiq detyrat", + "useCaseTeamCollab": "Puno së bashku pa pengesa", + "useCaseResourceMgmt": "Menaxho kohën dhe burimet", + "useCaseClientComm": "Qëndro i lidhur me klientët", + "useCaseTimeTrack": "Monitoro orët e projektit", + "useCaseOther": "Diçka tjetër", + "selectedText": "zgjedhur", + "previousToolsQuestion": "Çfarë mjetesh ke përdorur më parë? (Opsionale)", + "discoveryTitle": "Edhe një gjë e fundit...", + "discoveryDescription": "Na ndihmo të kuptojmë si e zbulove Worklenz", + "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", + "aboutYouStepName": "Rreth teje", + "yourNeedsStepName": "Nevojat e tua", + "discoveryStepName": "Zbulimi", + "stepProgress": "Hapi {step} nga 3: {title}", + "projectStepHeader": "Le të krijojmë projektin tënd të parë", + "projectStepSubheader": "Fillo nga e para ose përdor një shabllon për të filluar më shpejt", + "startFromScratch": "Fillo nga e para", + "templateSelected": "Shablloni i zgjedhur më poshtë", + "quickSuggestions": "Sugjerime të shpejta:", + "orText": "OSE", + "startWithTemplate": "Fillo me një shabllon", + "clearToSelectTemplate": "Pastro emrin e projektit më sipër për të zgjedhur një shabllon", + "templateHeadStart": "Fillo më shpejt me struktura të gatshme projekti", + "browseAllTemplates": "Shfleto të gjitha shabllonet", + "templatesAvailable": "15+ shabllone të specializuara sipas industrisë në dispozicion", + "chooseTemplate": "Zgjidh një shabllon që i përshtatet llojit të projektit tënd", + "createProject": "Krijo projekt", + "templateSoftwareDev": "Zhvillim Softueri", + "templateSoftwareDesc": "Sprint-e agile, ndjekje gabimesh, lëshime", + "templateMarketing": "Fushatë Marketingu", + "templateMarketingDesc": "Planifikim fushate, kalendar përmbajtjesh", + "templateConstruction": "Projekt Ndërtimi", + "templateConstructionDesc": "Faza, leje, kontraktorë", + "templateStartup": "Lansim Startup-i", + "templateStartupDesc": "Zhvillim MVP, financim, rritje", + "tasksStepDescription": "Ndaji \"{{projectName}}\" në detyra të veprueshme për të filluar", + "taskPlaceholder": "Detyra {{index}} - p.sh., Çfarë duhet bërë?", + "addAnotherTask": "Shto një detyrë tjetër ({{current}}/{{max}})", + "surveyStepTitle": "Na trego për veten tënde", + "surveyStepLabel": "Na ndihmo të personalizojmë përvojën tënde në Worklenz duke iu përgjigjur disa pyetjeve.", + "organizationType": "Cila përshkruan më mirë organizatën tënde?", + "organizationTypeFreelancer": "Freelancer", + "organizationTypeStartup": "Startup", + "organizationTypeSmallMediumBusiness": "Biznes i Vogël ose i Mesëm", + "organizationTypeAgency": "Agjenci", + "organizationTypeEnterprise": "Ndërmarrje", + "organizationTypeOther": "Tjetër", + "userRole": "Cili është roli yt?", + "userRoleFounderCeo": "Themelues / CEO", + "userRoleProjectManager": "Menaxher Projekti", + "userRoleSoftwareDeveloper": "Zhvillues Softueri", + "userRoleDesigner": "Dizajner", + "userRoleOperations": "Operacionet", + "userRoleOther": "Tjetër", + "mainUseCases": "Për çfarë do ta përdorësh kryesisht Worklenz?", + "mainUseCasesTaskManagement": "Menaxhim detyrash", + "mainUseCasesTeamCollaboration": "Bashkëpunim ekipi", + "mainUseCasesResourcePlanning": "Planifikim burimesh", + "mainUseCasesClientCommunication": "Komunikim & raportim me klientët", + "mainUseCasesTimeTracking": "Ndjekje kohe", + "mainUseCasesOther": "Tjetër", + "previousTools": "Çfarë mjetesh ke përdorur para Worklenz?", + "previousToolsPlaceholder": "p.sh. Trello, Asana, Monday.com", + "howHeardAbout": "Si dëgjove për Worklenz?", + "howHeardAboutGoogleSearch": "Kërkim në Google", + "howHeardAboutTwitter": "Twitter", + "howHeardAboutLinkedin": "LinkedIn", + "howHeardAboutFriendColleague": "Një mik ose koleg", + "howHeardAboutBlogArticle": "Një blog ose artikull", + "howHeardAboutOther": "Tjetër", + + "aboutYouStepTitle": "Na trego për veten", + "aboutYouStepDescription": "Na ndihmo të personalizojmë përvojën tënde", + "yourNeedsStepTitle": "Cilat janë nevojat e tua kryesore?", + "yourNeedsStepDescription": "Zgjidh të gjitha që aplikohen për të na ndihmuar të konfigurojmë hapësirën tënde të punës", + "selected": "zgjedhur", + "previousToolsLabel": "Çfarë mjetesh ke përdorur më parë? (Opsionale)", + + "roleSuggestions": { + "designer": "UI/UX, Grafikë, Kreativ", + "developer": "Frontend, Backend, Full-stack", + "projectManager": "Planifikim, Koordinim", + "marketing": "Përmbajtje, Media Sociale, Rritje", + "sales": "Zhvillim Biznesi, Marrëdhënie me Klientë", + "operations": "Administratë, HR, Financa" + }, + + "languages": { + "en": "Anglisht", + "es": "Spanjisht", + "pt": "Portugalisht", + "de": "Gjermanisht", + "alb": "Shqip", + "zh": "Kinezçe" + }, + + "orgSuggestions": { + "tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"], + "creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"], + "consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"], + "startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"] + }, + + "projectSuggestions": { + "freelancer": ["Projekti i Klientit", "Përditësim Portfolio", "Markë Personale"], + "startup": ["Zhvillim MVP", "Lansim Produkti", "Kërkim Tregu"], + "agency": ["Fushatë Klienti", "Strategji Markë", "Ridizajnim Website"], + "enterprise": ["Migrim Sistemi", "Optimizim Procesesh", "Trajnim Ekipi"] + }, + + "useCaseDescriptions": { + "taskManagement": "Organizoj dhe ndjek detyrat", + "teamCollaboration": "Punojmë së bashku pa probleme", + "resourcePlanning": "Menaxhoj kohën dhe burimet", + "clientCommunication": "Qëndroj i lidhur me klientët", + "timeTracking": "Monitoroj orët e projektit", + "other": "Diçka tjetër" + } } diff --git a/worklenz-frontend/public/locales/de/account-setup.json b/worklenz-frontend/public/locales/de/account-setup.json index ddfb7b80..9890bcc0 100644 --- a/worklenz-frontend/public/locales/de/account-setup.json +++ b/worklenz-frontend/public/locales/de/account-setup.json @@ -3,7 +3,28 @@ "setupYourAccount": "Richten Sie Ihr Worklenz-Konto ein.", "organizationStepTitle": "Organisation benennen", - "organizationStepLabel": "Wählen Sie einen Namen für Ihr Worklenz-Konto.", + "organizationStepWelcome": "Willkommen bei Worklenz!", + "organizationStepDescription": "Beginnen wir mit der Einrichtung Ihrer Organisation. Dies wird der Hauptarbeitsplatz für Ihr Team.", + "organizationStepLabel": "Organisationsname", + "organizationStepPlaceholder": "z.B. Acme Corporation", + "organizationStepTooltip": "Dieser Name wird in Ihrem Arbeitsbereich angezeigt und kann später in den Einstellungen geändert werden.", + "organizationStepNeedIdeas": "Brauchen Sie Ideen?", + "organizationStepUseDetected": "Erkannt verwenden:", + "organizationStepCharacters": "Zeichen", + "organizationStepGoodLength": "Gute Länge", + "organizationStepTooShort": "Zu kurz", + "organizationStepNamingTips": "Namensgebungstipps", + "organizationStepTip1": "Halten Sie es einfach und einprägsam", + "organizationStepTip2": "Spiegeln Sie Ihre Branche oder Werte wider", + "organizationStepTip3": "Denken Sie an zukünftiges Wachstum", + "organizationStepTip4": "Machen Sie es einzigartig und markenfähig", + "organizationStepSuggestionsTitle": "Namensvorschläge", + "organizationStepCategory1": "Tech-Unternehmen", + "organizationStepCategory2": "Kreativagenturen", + "organizationStepCategory3": "Beratung", + "organizationStepCategory4": "Startups", + "organizationStepSuggestionsNote": "Dies sind nur Beispiele für den Einstieg. Wählen Sie etwas, das Ihre Organisation repräsentiert.", + "organizationStepPrivacyNote": "Ihr Organisationsname ist privat und nur für Ihre Teammitglieder sichtbar.", "projectStepTitle": "Erstellen Sie Ihr erstes Projekt", "projectStepLabel": "An welchem Projekt arbeiten Sie gerade?", @@ -27,5 +48,164 @@ "formTitle": "Erstellen Sie Ihre erste Aufgabe.", "step3Title": "Laden Sie Ihr Team zur Zusammenarbeit ein", "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)", + + "membersStepTitle": "Laden Sie Ihr Team ein", + "membersStepDescription": "Teammitglieder zu \"{{organizationName}}\" hinzufügen und mit der Zusammenarbeit beginnen", + "memberPlaceholder": "Teammitglied {{index}} - E-Mail-Adresse eingeben", + "validEmailAddress": "Gültige E-Mail-Adresse", + "addAnotherTeamMember": "Weiteres Teammitglied hinzufügen ({{current}}/{{max}})", + "canInviteLater": "Sie können Teammitglieder jederzeit später einladen", + "skipStepDescription": "Haben Sie keine E-Mail-Adressen bereit? Kein Problem! Sie können diesen Schritt überspringen und Teammitglieder später über Ihr Projekt-Dashboard einladen.", + + "orgCategoryTech": "Technologieunternehmen", + "orgCategoryCreative": "Kreativagenturen", + "orgCategoryConsulting": "Beratung", + "orgCategoryStartups": "Startups", + "namingTip1": "Halten Sie es einfach und einprägsam", + "namingTip2": "Spiegeln Sie Ihre Branche oder Werte wider", + "namingTip3": "Denken Sie an zukünftiges Wachstum", + "namingTip4": "Machen Sie es einzigartig und markenfähig", + + "aboutYouTitle": "Erzählen Sie uns von sich", + "aboutYouDescription": "Helfen Sie uns, Ihre Erfahrung zu personalisieren", + "orgTypeQuestion": "Was beschreibt Ihre Organisation am besten?", + "userRoleQuestion": "Was ist Ihre Rolle?", + + "yourNeedsTitle": "Was sind Ihre Hauptbedürfnisse?", + "yourNeedsDescription": "Wählen Sie alle zutreffenden aus, um uns bei der Einrichtung Ihres Arbeitsbereichs zu helfen", + "yourNeedsQuestion": "Wie werden Sie Worklenz hauptsächlich nutzen?", + "useCaseTaskOrg": "Aufgaben organisieren und verfolgen", + "useCaseTeamCollab": "Nahtlos zusammenarbeiten", + "useCaseResourceMgmt": "Zeit und Ressourcen verwalten", + "useCaseClientComm": "Mit Kunden in Verbindung bleiben", + "useCaseTimeTrack": "Projektstunden überwachen", + "useCaseOther": "Etwas anderes", + "selectedText": "ausgewählt", + "previousToolsQuestion": "Welche Tools haben Sie zuvor verwendet? (Optional)", + "previousToolsPlaceholder": "z.B. Asana, Trello, Jira, Monday.com, etc.", + + "discoveryTitle": "Eine letzte Sache...", + "discoveryDescription": "Helfen Sie uns zu verstehen, wie Sie Worklenz entdeckt haben", + "discoveryQuestion": "Wie haben Sie von uns erfahren?", + "allSetTitle": "Sie sind bereit!", + "allSetDescription": "Lassen Sie uns Ihr erstes Projekt erstellen und mit Worklenz beginnen", + "aboutYouStepName": "Über Sie", + "yourNeedsStepName": "Ihre Bedürfnisse", + "discoveryStepName": "Entdeckung", + "stepProgress": "Schritt {step} von 3: {title}", + + "projectStepHeader": "Lassen Sie uns Ihr erstes Projekt erstellen", + "projectStepSubheader": "Von Grund auf beginnen oder eine Vorlage verwenden, um schneller voranzukommen", + "startFromScratch": "Von Grund auf beginnen", + "templateSelected": "Vorlage unten ausgewählt", + "quickSuggestions": "Schnelle Vorschläge:", + "orText": "ODER", + "startWithTemplate": "Mit einer Vorlage beginnen", + "clearToSelectTemplate": "Projektname oben löschen, um eine Vorlage auszuwählen", + "templateHeadStart": "Verschaffen Sie sich einen Vorsprung mit vorgefertigten Projektstrukturen", + "browseAllTemplates": "Alle Vorlagen durchsuchen", + "templatesAvailable": "15+ branchenspezifische Vorlagen verfügbar", + "chooseTemplate": "Wählen Sie eine Vorlage, die zu Ihrem Projekttyp passt", + "createProject": "Projekt erstellen", + + "templateSoftwareDev": "Softwareentwicklung", + "templateSoftwareDesc": "Agile Sprints, Fehlerverfolgung, Releases", + "templateMarketing": "Marketing-Kampagne", + "templateMarketingDesc": "Kampagnenplanung, Content-Kalender", + "templateConstruction": "Bauprojekt", + "templateConstructionDesc": "Phasen, Genehmigungen, Auftragnehmer", + "templateStartup": "Startup-Launch", + "templateStartupDesc": "MVP-Entwicklung, Finanzierung, Wachstum", + + "tasksStepTitle": "Fügen Sie Ihre ersten Aufgaben hinzu", + "tasksStepDescription": "Unterteilen Sie \"{{projectName}}\" in umsetzbare Aufgaben, um zu beginnen", + "taskPlaceholder": "Aufgabe {{index}} - z.B., Was muss getan werden?", + "addAnotherTask": "Weitere Aufgabe hinzufügen ({{current}}/{{max}})", + + "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", + + "aboutYouStepTitle": "Erzählen Sie uns von sich", + "aboutYouStepDescription": "Helfen Sie uns, Ihre Erfahrung zu personalisieren", + "yourNeedsStepTitle": "Was sind Ihre Hauptbedürfnisse?", + "yourNeedsStepDescription": "Wählen Sie alle zutreffenden aus, um uns bei der Einrichtung Ihres Arbeitsbereichs zu helfen", + "selected": "ausgewählt", + "previousToolsLabel": "Welche Tools haben Sie zuvor verwendet? (Optional)", + + "roleSuggestions": { + "designer": "UI/UX, Grafiken, Kreativ", + "developer": "Frontend, Backend, Full-stack", + "projectManager": "Planung, Koordination", + "marketing": "Inhalt, Social Media, Wachstum", + "sales": "Geschäftsentwicklung, Kundenbeziehungen", + "operations": "Admin, HR, Finanzen" + }, + + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "de": "Deutsch", + "alb": "Shqip", + "zh": "简体中文" + }, + + "orgSuggestions": { + "tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"], + "creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"], + "consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"], + "startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"] + }, + + "projectSuggestions": { + "freelancer": ["Kundenprojekt", "Portfolio-Update", "Persönliche Marke"], + "startup": ["MVP-Entwicklung", "Produktlaunch", "Marktforschung"], + "agency": ["Kundenkampagne", "Markenstrategie", "Website-Redesign"], + "enterprise": ["Systemumstellung", "Prozessoptimierung", "Teamschulung"] + }, + + "useCaseDescriptions": { + "taskManagement": "Aufgaben organisieren und verfolgen", + "teamCollaboration": "Nahtlos zusammenarbeiten", + "resourcePlanning": "Zeit und Ressourcen verwalten", + "clientCommunication": "Mit Kunden in Verbindung bleiben", + "timeTracking": "Projektstunden überwachen", + "other": "Etwas anderes" + } } diff --git a/worklenz-frontend/public/locales/de/admin-center/configuration.json b/worklenz-frontend/public/locales/de/admin-center/configuration.json new file mode 100644 index 00000000..79d33f27 --- /dev/null +++ b/worklenz-frontend/public/locales/de/admin-center/configuration.json @@ -0,0 +1,26 @@ +{ + "billingDetails": "Abrechnungsdetails", + "name": "Name", + "namePlaceholder": "Name", + "emailAddress": "E-Mail-Adresse", + "emailPlaceholder": "E-Mail-Adresse", + "contactNumber": "Telefonnummer", + "phoneNumberPlaceholder": "Telefonnummer", + "phoneValidationError": "Telefonnummer muss genau 10 Ziffern haben", + "companyDetails": "Firmendetails", + "companyName": "Firmenname", + "companyNamePlaceholder": "Firmenname", + "addressLine01": "Adresszeile 01", + "addressLine01Placeholder": "Adresszeile 01", + "addressLine02": "Adresszeile 02", + "addressLine02Placeholder": "Adresszeile 02", + "country": "Land", + "countryPlaceholder": "Land", + "city": "Stadt", + "cityPlaceholder": "Stadt", + "state": "Bundesland", + "statePlaceholder": "Bundesland", + "postalCode": "Postleitzahl", + "postalCodePlaceholder": "Postleitzahl", + "save": "Speichern" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/en/account-setup.json b/worklenz-frontend/public/locales/en/account-setup.json index 5e71ca40..22681256 100644 --- a/worklenz-frontend/public/locales/en/account-setup.json +++ b/worklenz-frontend/public/locales/en/account-setup.json @@ -1,15 +1,35 @@ { "continue": "Continue", - "setupYourAccount": "Setup Your Worklenz Account.", + "setupYourAccount": "Setup Your Account.", "organizationStepTitle": "Name Your Organization", - "organizationStepLabel": "Pick a name for your Worklenz account.", + "organizationStepWelcome": "Welcome to Worklenz!", + "organizationStepDescription": "Let's start by setting up your organization. This will be the main workspace for your team.", + "organizationStepLabel": "Organization name", + "organizationStepPlaceholder": "e.g. Acme Corporation", + "organizationStepTooltip": "This name will appear in your workspace and can be changed later in settings.", + "organizationStepNeedIdeas": "Need ideas?", + "organizationStepUseDetected": "Use detected:", + "organizationStepCharacters": "characters", + "organizationStepGoodLength": "Good length", + "organizationStepTooShort": "Too short", + "organizationStepNamingTips": "Naming Tips", + "organizationStepTip1": "Keep it simple and memorable", + "organizationStepTip2": "Reflect your industry or values", + "organizationStepTip3": "Think about future growth", + "organizationStepTip4": "Make it unique and brandable", + "organizationStepSuggestionsTitle": "Name Suggestions", + "organizationStepCategory1": "Tech Companies", + "organizationStepCategory2": "Creative Agencies", + "organizationStepCategory3": "Consulting", + "organizationStepCategory4": "Startups", + "organizationStepSuggestionsNote": "These are just examples to get you started. Choose something that represents your organization.", + "organizationStepPrivacyNote": "Your organization name is private and only visible to your team members.", "projectStepTitle": "Create your first project", "projectStepLabel": "What project are you working on right now?", "projectStepPlaceholder": "e.g. Marketing Plan", - "tasksStepTitle": "Create your first tasks", "tasksStepLabel": "Type a few tasks that you are going to do in", "tasksStepAddAnother": "Add another", @@ -27,5 +47,163 @@ "formTitle": "Create your first task.", "step3Title": "Invite your team to work with", "maxMembers": " (You can invite up to 5 members)", - "maxTasks": " (You can create up to 5 tasks)" + "maxTasks": " (You can create up to 5 tasks)", + + "membersStepTitle": "Invite your team", + "membersStepDescription": "Add team members to \"{{organizationName}}\" and start collaborating", + "memberPlaceholder": "Team member {{index}} - Enter email address", + "validEmailAddress": "Valid email address", + "addAnotherTeamMember": "Add another team member ({{current}}/{{max}})", + "canInviteLater": "You can always invite team members later", + "skipStepDescription": "Don't have email addresses ready? No problem! You can skip this step and invite team members from your project dashboard later.", + + "orgCategoryTech": "Tech Companies", + "orgCategoryCreative": "Creative Agencies", + "orgCategoryConsulting": "Consulting", + "orgCategoryStartups": "Startups", + "namingTip1": "Keep it simple and memorable", + "namingTip2": "Reflect your industry or values", + "namingTip3": "Think about future growth", + "namingTip4": "Make it unique and brandable", + + "aboutYouTitle": "Tell us about yourself", + "aboutYouDescription": "Help us personalize your experience", + "orgTypeQuestion": "What best describes your organization?", + "userRoleQuestion": "What's your role?", + + "yourNeedsTitle": "What are your main needs?", + "yourNeedsDescription": "Select all that apply to help us set up your workspace", + "yourNeedsQuestion": "How will you primarily use Worklenz?", + "useCaseTaskOrg": "Organize and track tasks", + "useCaseTeamCollab": "Work together seamlessly", + "useCaseResourceMgmt": "Manage time and resources", + "useCaseClientComm": "Stay connected with clients", + "useCaseTimeTrack": "Monitor project hours", + "useCaseOther": "Something else", + "selectedText": "selected", + "previousToolsQuestion": "What tools have you used before? (Optional)", + + "discoveryTitle": "One last thing...", + "discoveryDescription": "Help us understand how you discovered Worklenz", + "discoveryQuestion": "How did you hear about us?", + "allSetTitle": "You're all set!", + "allSetDescription": "Let's create your first project and get started with Worklenz", + "aboutYouStepName": "About You", + "yourNeedsStepName": "Your Needs", + "discoveryStepName": "Discovery", + "stepProgress": "Step {step} of 3: {title}", + + "projectStepHeader": "Let's create your first project", + "projectStepSubheader": "Start from scratch or use a template to get going faster", + "startFromScratch": "Start from scratch", + "templateSelected": "Template selected below", + "quickSuggestions": "Quick suggestions:", + "orText": "OR", + "startWithTemplate": "Start with a template", + "clearToSelectTemplate": "Clear project name above to select a template", + "templateHeadStart": "Get a head start with pre-built project structures", + "browseAllTemplates": "Browse All Templates", + "templatesAvailable": "15+ industry-specific templates available", + "chooseTemplate": "Choose a template that matches your project type", + "createProject": "Create Project", + + "templateSoftwareDev": "Software Development", + "templateSoftwareDesc": "Agile sprints, bug tracking, releases", + "templateMarketing": "Marketing Campaign", + "templateMarketingDesc": "Campaign planning, content calendar", + "templateConstruction": "Construction Project", + "templateConstructionDesc": "Phases, permits, contractors", + "templateStartup": "Startup Launch", + "templateStartupDesc": "MVP development, funding, growth", + + "tasksStepTitle": "Add your first tasks", + "tasksStepDescription": "Break down \"{{projectName}}\" into actionable tasks to get started", + "taskPlaceholder": "Task {{index}} - e.g., What needs to be done?", + "addAnotherTask": "Add another task ({{current}}/{{max}})", + + "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", + + "aboutYouStepTitle": "Tell us about yourself", + "aboutYouStepDescription": "Help us personalize your experience", + "yourNeedsStepTitle": "What are your main needs?", + "yourNeedsStepDescription": "Select all that apply to help us set up your workspace", + "selected": "selected", + "previousToolsLabel": "What tools have you used before? (Optional)", + + "roleSuggestions": { + "designer": "UI/UX, Graphics, Creative", + "developer": "Frontend, Backend, Full-stack", + "projectManager": "Planning, Coordination", + "marketing": "Content, Social Media, Growth", + "sales": "Business Development, Client Relations", + "operations": "Admin, HR, Finance" + }, + + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "de": "Deutsch", + "alb": "Shqip", + "zh": "简体中文" + }, + + "orgSuggestions": { + "tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"], + "creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"], + "consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"], + "startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"] + }, + + "projectSuggestions": { + "freelancer": ["Client Project", "Portfolio Update", "Personal Brand"], + "startup": ["MVP Development", "Product Launch", "Market Research"], + "agency": ["Client Campaign", "Brand Strategy", "Website Redesign"], + "enterprise": ["System Migration", "Process Optimization", "Team Training"] + }, + + "useCaseDescriptions": { + "taskManagement": "Organize and track tasks", + "teamCollaboration": "Work together seamlessly", + "resourcePlanning": "Manage time and resources", + "clientCommunication": "Stay connected with clients", + "timeTracking": "Monitor project hours", + "other": "Something else" + } } diff --git a/worklenz-frontend/public/locales/en/admin-center/configuration.json b/worklenz-frontend/public/locales/en/admin-center/configuration.json new file mode 100644 index 00000000..78293239 --- /dev/null +++ b/worklenz-frontend/public/locales/en/admin-center/configuration.json @@ -0,0 +1,26 @@ +{ + "billingDetails": "Billing Details", + "name": "Name", + "namePlaceholder": "Name", + "emailAddress": "Email Address", + "emailPlaceholder": "Email Address", + "contactNumber": "Contact Number", + "phoneNumberPlaceholder": "Phone Number", + "phoneValidationError": "Phone number must be exactly 10 digits", + "companyDetails": "Company Details", + "companyName": "Company Name", + "companyNamePlaceholder": "Company Name", + "addressLine01": "Address Line 01", + "addressLine01Placeholder": "Address Line 01", + "addressLine02": "Address Line 02", + "addressLine02Placeholder": "Address Line 02", + "country": "Country", + "countryPlaceholder": "Country", + "city": "City", + "cityPlaceholder": "City", + "state": "State", + "statePlaceholder": "State", + "postalCode": "Postal Code", + "postalCodePlaceholder": "Postal Code", + "save": "Save" +} \ No newline at end of file diff --git a/worklenz-frontend/public/locales/en/admin-center/current-bill.json b/worklenz-frontend/public/locales/en/admin-center/current-bill.json index fe840789..9ff09401 100644 --- a/worklenz-frontend/public/locales/en/admin-center/current-bill.json +++ b/worklenz-frontend/public/locales/en/admin-center/current-bill.json @@ -117,5 +117,26 @@ "currentSeatsText": "You currently have {{seats}} seats available.", "selectSeatsText": "Please select the number of additional seats to purchase.", "purchase": "Purchase", - "contactSales": "Contact sales" + "contactSales": "Contact sales", + "submitSuccess": "Code redeemed successfully!", + "submitSuccessDescription": "Your account has been updated with the new credits.", + "percentUsed": "% Used", + "sizeUnits": { + "bytes": "Bytes", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" + }, + "seatPerMonth": "seat / month", + "totalPrice": "Total $", + "tryForFree": "Try for free", + "subscriptionUpdateSuccess": "Subscription updated successfully!", + "paymentProcessorError": "Failed to load payment processor", + "seatsLabel": "Seats:", + "requiredField": "*", + "purchaseSeatsTextSingle": "To continue, you'll need to purchase an additional seat.", + "singleUserNote": "You currently have 1 seat available.", + "selectSeatsTextSingle": "Please select the number of additional seats to purchase.", + "phoneNumberPattern": "07xxxxxxxx" } diff --git a/worklenz-frontend/public/locales/en/admin-center/overview.json b/worklenz-frontend/public/locales/en/admin-center/overview.json index efc42855..3ec4329b 100644 --- a/worklenz-frontend/public/locales/en/admin-center/overview.json +++ b/worklenz-frontend/public/locales/en/admin-center/overview.json @@ -4,5 +4,8 @@ "owner": "Organization Owner", "admins": "Organization Admins", "contactNumber": "Add Contact Number", - "edit": "Edit" + "edit": "Edit", + "emailAddress": "Email Address", + "enterOrganizationName": "Enter organization name", + "ownerSuffix": " (Owner)" } diff --git a/worklenz-frontend/public/locales/en/admin-center/users.json b/worklenz-frontend/public/locales/en/admin-center/users.json index 7e462ef6..db2641a4 100644 --- a/worklenz-frontend/public/locales/en/admin-center/users.json +++ b/worklenz-frontend/public/locales/en/admin-center/users.json @@ -5,5 +5,6 @@ "user": "User", "email": "Email", "lastActivity": "Last Activity", - "refresh": "Refresh users" + "refresh": "Refresh users", + "name": "Name" } diff --git a/worklenz-frontend/public/locales/es/account-setup.json b/worklenz-frontend/public/locales/es/account-setup.json index 3f7b013e..0910f64c 100644 --- a/worklenz-frontend/public/locales/es/account-setup.json +++ b/worklenz-frontend/public/locales/es/account-setup.json @@ -3,7 +3,28 @@ "setupYourAccount": "Configura tu cuenta.", "organizationStepTitle": "Nombra tu organización", - "organizationStepLabel": "Elige un nombre para tu cuenta de Worklenz.", + "organizationStepWelcome": "¡Bienvenido a Worklenz!", + "organizationStepDescription": "Comencemos configurando tu organización. Este será el espacio de trabajo principal para tu equipo.", + "organizationStepLabel": "Nombre de la organización", + "organizationStepPlaceholder": "ej. Corporación Acme", + "organizationStepTooltip": "Este nombre aparecerá en tu espacio de trabajo y se puede cambiar más tarde en la configuración.", + "organizationStepNeedIdeas": "¿Necesitas ideas?", + "organizationStepUseDetected": "Usar detectado:", + "organizationStepCharacters": "caracteres", + "organizationStepGoodLength": "Buena longitud", + "organizationStepTooShort": "Demasiado corto", + "organizationStepNamingTips": "Consejos para nombrar", + "organizationStepTip1": "Manténlo simple y memorable", + "organizationStepTip2": "Refleja tu industria o valores", + "organizationStepTip3": "Piensa en el crecimiento futuro", + "organizationStepTip4": "Hazlo único y reconocible", + "organizationStepSuggestionsTitle": "Sugerencias de nombres", + "organizationStepCategory1": "Empresas tecnológicas", + "organizationStepCategory2": "Agencias creativas", + "organizationStepCategory3": "Consultoría", + "organizationStepCategory4": "Startups", + "organizationStepSuggestionsNote": "Estos son solo ejemplos para empezar. Elige algo que represente a tu organización.", + "organizationStepPrivacyNote": "El nombre de tu organización es privado y solo visible para los miembros de tu equipo.", "projectStepTitle": "Crea tu primer proyecto", "projectStepLabel": "¿En qué proyecto estás trabajando ahora?", @@ -28,5 +49,164 @@ "step3Title": "Invita a tu equipo a trabajar", "maxMembers": " (Puedes invitar hasta 5 miembros)", - "maxTasks": " (Puedes crear hasta 5 tareas)" + "maxTasks": " (Puedes crear hasta 5 tareas)", + + "membersStepTitle": "Invita a tu equipo", + "membersStepDescription": "Añade miembros del equipo a \"{{organizationName}}\" y comienza a colaborar", + "memberPlaceholder": "Miembro del equipo {{index}} - Ingresa dirección de correo", + "validEmailAddress": "Dirección de correo válida", + "addAnotherTeamMember": "Añadir otro miembro del equipo ({{current}}/{{max}})", + "canInviteLater": "Siempre puedes invitar miembros del equipo más tarde", + "skipStepDescription": "¿No tienes direcciones de correo listas? ¡No hay problema! Puedes omitir este paso e invitar miembros del equipo desde tu panel de proyecto más tarde.", + + "orgCategoryTech": "Empresas Tecnológicas", + "orgCategoryCreative": "Agencias Creativas", + "orgCategoryConsulting": "Consultoría", + "orgCategoryStartups": "Startups", + "namingTip1": "Manténlo simple y memorable", + "namingTip2": "Refleja tu industria o valores", + "namingTip3": "Piensa en el crecimiento futuro", + "namingTip4": "Hazlo único y reconocible", + + "aboutYouTitle": "Cuéntanos sobre ti", + "aboutYouDescription": "Ayúdanos a personalizar tu experiencia", + "orgTypeQuestion": "¿Qué describe mejor tu organización?", + "userRoleQuestion": "¿Cuál es tu rol?", + + "yourNeedsTitle": "¿Cuáles son tus principales necesidades?", + "yourNeedsDescription": "Selecciona todas las que apliquen para ayudarnos a configurar tu espacio de trabajo", + "yourNeedsQuestion": "¿Cómo usarás principalmente Worklenz?", + "useCaseTaskOrg": "Organizar y hacer seguimiento de tareas", + "useCaseTeamCollab": "Trabajar juntos sin problemas", + "useCaseResourceMgmt": "Gestionar tiempo y recursos", + "useCaseClientComm": "Mantenerse conectado con clientes", + "useCaseTimeTrack": "Monitorear horas de proyecto", + "useCaseOther": "Algo más", + "selectedText": "seleccionado", + "previousToolsQuestion": "¿Qué herramientas has usado antes? (Opcional)", + "previousToolsPlaceholder": "ej., Asana, Trello, Jira, Monday.com, etc.", + + "discoveryTitle": "Una última cosa...", + "discoveryDescription": "Ayúdanos a entender cómo descubriste Worklenz", + "discoveryQuestion": "¿Cómo te enteraste de nosotros?", + "allSetTitle": "¡Ya estás listo!", + "allSetDescription": "Vamos a crear tu primer proyecto y comenzar con Worklenz", + "aboutYouStepName": "Sobre ti", + "yourNeedsStepName": "Tus necesidades", + "discoveryStepName": "Descubrimiento", + "stepProgress": "Paso {step} de 3: {title}", + + "projectStepHeader": "Vamos a crear tu primer proyecto", + "projectStepSubheader": "Empieza desde cero o usa una plantilla para ir más rápido", + "startFromScratch": "Empezar desde cero", + "templateSelected": "Plantilla seleccionada abajo", + "quickSuggestions": "Sugerencias rápidas:", + "orText": "O", + "startWithTemplate": "Comenzar con una plantilla", + "clearToSelectTemplate": "Borra el nombre del proyecto arriba para seleccionar una plantilla", + "templateHeadStart": "Obtén una ventaja inicial con estructuras de proyecto pre-construidas", + "browseAllTemplates": "Explorar todas las plantillas", + "templatesAvailable": "15+ plantillas específicas de industria disponibles", + "chooseTemplate": "Elige una plantilla que coincida con tu tipo de proyecto", + "createProject": "Crear proyecto", + + "templateSoftwareDev": "Desarrollo de Software", + "templateSoftwareDesc": "Sprints ágiles, seguimiento de errores, lanzamientos", + "templateMarketing": "Campaña de Marketing", + "templateMarketingDesc": "Planificación de campaña, calendario de contenido", + "templateConstruction": "Proyecto de Construcción", + "templateConstructionDesc": "Fases, permisos, contratistas", + "templateStartup": "Lanzamiento de Startup", + "templateStartupDesc": "Desarrollo MVP, financiación, crecimiento", + + "tasksStepTitle": "Añade tus primeras tareas", + "tasksStepDescription": "Desglosa \"{{projectName}}\" en tareas accionables para comenzar", + "taskPlaceholder": "Tarea {{index}} - ej., ¿Qué necesita hacerse?", + "addAnotherTask": "Añadir otra tarea ({{current}}/{{max}})", + + "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", + + "aboutYouStepTitle": "Cuéntanos sobre ti", + "aboutYouStepDescription": "Ayúdanos a personalizar tu experiencia", + "yourNeedsStepTitle": "¿Cuáles son tus principales necesidades?", + "yourNeedsStepDescription": "Selecciona todas las que apliquen para ayudarnos a configurar tu espacio de trabajo", + "selected": "seleccionado", + "previousToolsLabel": "¿Qué herramientas has usado antes? (Opcional)", + + "roleSuggestions": { + "designer": "UI/UX, Gráficos, Creativo", + "developer": "Frontend, Backend, Full-stack", + "projectManager": "Planificación, Coordinación", + "marketing": "Contenido, Redes Sociales, Crecimiento", + "sales": "Desarrollo de Negocios, Relaciones con Clientes", + "operations": "Administración, RRHH, Finanzas" + }, + + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "de": "Deutsch", + "alb": "Shqip", + "zh": "简体中文" + }, + + "orgSuggestions": { + "tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"], + "creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"], + "consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"], + "startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"] + }, + + "projectSuggestions": { + "freelancer": ["Proyecto Cliente", "Actualización Portfolio", "Marca Personal"], + "startup": ["Desarrollo MVP", "Lanzamiento Producto", "Investigación Mercado"], + "agency": ["Campaña Cliente", "Estrategia Marca", "Rediseño Website"], + "enterprise": ["Migración Sistema", "Optimización Procesos", "Capacitación Equipo"] + }, + + "useCaseDescriptions": { + "taskManagement": "Organizar y rastrear tareas", + "teamCollaboration": "Trabajar juntos sin problemas", + "resourcePlanning": "Gestionar tiempo y recursos", + "clientCommunication": "Mantenerse conectado con clientes", + "timeTracking": "Monitorear horas de proyecto", + "other": "Algo más" + } } diff --git a/worklenz-frontend/public/locales/pt/account-setup.json b/worklenz-frontend/public/locales/pt/account-setup.json index 1d8a8cba..68cdd224 100644 --- a/worklenz-frontend/public/locales/pt/account-setup.json +++ b/worklenz-frontend/public/locales/pt/account-setup.json @@ -3,7 +3,28 @@ "setupYourAccount": "Configure sua conta.", "organizationStepTitle": "Nomeie sua organização", - "organizationStepLabel": "Escolha um nome para sua conta Worklenz.", + "organizationStepWelcome": "Bem-vindo ao Worklenz!", + "organizationStepDescription": "Vamos começar configurando sua organização. Este será o espaço de trabalho principal para sua equipe.", + "organizationStepLabel": "Nome da organização", + "organizationStepPlaceholder": "ex. Corporação Acme", + "organizationStepTooltip": "Este nome aparecerá em seu espaço de trabalho e pode ser alterado posteriormente nas configurações.", + "organizationStepNeedIdeas": "Precisa de ideias?", + "organizationStepUseDetected": "Usar detectado:", + "organizationStepCharacters": "caracteres", + "organizationStepGoodLength": "Bom comprimento", + "organizationStepTooShort": "Muito curto", + "organizationStepNamingTips": "Dicas de nomenclatura", + "organizationStepTip1": "Mantenha simples e memorável", + "organizationStepTip2": "Reflita sua indústria ou valores", + "organizationStepTip3": "Pense no crescimento futuro", + "organizationStepTip4": "Torne único e marcante", + "organizationStepSuggestionsTitle": "Sugestões de nomes", + "organizationStepCategory1": "Empresas de tecnologia", + "organizationStepCategory2": "Agências criativas", + "organizationStepCategory3": "Consultoria", + "organizationStepCategory4": "Startups", + "organizationStepSuggestionsNote": "Estes são apenas exemplos para começar. Escolha algo que represente sua organização.", + "organizationStepPrivacyNote": "O nome da sua organização é privado e visível apenas para os membros da sua equipe.", "projectStepTitle": "Crie seu primeiro projeto", "projectStepLabel": "Em qual projeto você está trabalhando agora?", @@ -28,5 +49,164 @@ "step3Title": "Convide sua equipe para trabalhar", "maxMembers": " (Você pode convidar até 5 membros)", - "maxTasks": " (Você pode criar até 5 tarefas)" + "maxTasks": " (Você pode criar até 5 tarefas)", + + "membersStepTitle": "Convide sua equipe", + "membersStepDescription": "Adicione membros da equipe ao \"{{organizationName}}\" e comece a colaborar", + "memberPlaceholder": "Membro da equipe {{index}} - Digite o endereço de email", + "validEmailAddress": "Endereço de email válido", + "addAnotherTeamMember": "Adicionar outro membro da equipe ({{current}}/{{max}})", + "canInviteLater": "Você sempre pode convidar membros da equipe mais tarde", + "skipStepDescription": "Não tem endereços de email prontos? Sem problema! Você pode pular esta etapa e convidar membros da equipe do seu painel de projeto mais tarde.", + + "orgCategoryTech": "Empresas de Tecnologia", + "orgCategoryCreative": "Agências Criativas", + "orgCategoryConsulting": "Consultoria", + "orgCategoryStartups": "Startups", + "namingTip1": "Mantenha simples e memorável", + "namingTip2": "Reflita sua indústria ou valores", + "namingTip3": "Pense no crescimento futuro", + "namingTip4": "Torne único e marcante", + + "aboutYouTitle": "Conte-nos sobre você", + "aboutYouDescription": "Ajude-nos a personalizar sua experiência", + "orgTypeQuestion": "O que melhor descreve sua organização?", + "userRoleQuestion": "Qual é seu papel?", + + "yourNeedsTitle": "Quais são suas principais necessidades?", + "yourNeedsDescription": "Selecione todas que se aplicam para nos ajudar a configurar seu espaço de trabalho", + "yourNeedsQuestion": "Como você usará principalmente o Worklenz?", + "useCaseTaskOrg": "Organizar e acompanhar tarefas", + "useCaseTeamCollab": "Trabalhar juntos perfeitamente", + "useCaseResourceMgmt": "Gerenciar tempo e recursos", + "useCaseClientComm": "Manter-se conectado com clientes", + "useCaseTimeTrack": "Monitorar horas do projeto", + "useCaseOther": "Algo mais", + "selectedText": "selecionado", + "previousToolsQuestion": "Que ferramentas você usou antes? (Opcional)", + "previousToolsPlaceholder": "ex., Asana, Trello, Jira, Monday.com, etc.", + + "discoveryTitle": "Uma última coisa...", + "discoveryDescription": "Ajude-nos a entender como você descobriu o Worklenz", + "discoveryQuestion": "Como você soube sobre nós?", + "allSetTitle": "Você está pronto!", + "allSetDescription": "Vamos criar seu primeiro projeto e começar com o Worklenz", + "aboutYouStepName": "Sobre você", + "yourNeedsStepName": "Suas necessidades", + "discoveryStepName": "Descoberta", + "stepProgress": "Passo {step} de 3: {title}", + + "projectStepHeader": "Vamos criar seu primeiro projeto", + "projectStepSubheader": "Comece do zero ou use um modelo para ir mais rápido", + "startFromScratch": "Começar do zero", + "templateSelected": "Modelo selecionado abaixo", + "quickSuggestions": "Sugestões rápidas:", + "orText": "OU", + "startWithTemplate": "Começar com um modelo", + "clearToSelectTemplate": "Limpe o nome do projeto acima para selecionar um modelo", + "templateHeadStart": "Obtenha uma vantagem inicial com estruturas de projeto pré-construídas", + "browseAllTemplates": "Navegar por todos os modelos", + "templatesAvailable": "15+ modelos específicos da indústria disponíveis", + "chooseTemplate": "Escolha um modelo que corresponda ao seu tipo de projeto", + "createProject": "Criar projeto", + + "templateSoftwareDev": "Desenvolvimento de Software", + "templateSoftwareDesc": "Sprints ágeis, rastreamento de bugs, lançamentos", + "templateMarketing": "Campanha de Marketing", + "templateMarketingDesc": "Planejamento de campanha, calendário de conteúdo", + "templateConstruction": "Projeto de Construção", + "templateConstructionDesc": "Fases, licenças, empreiteiros", + "templateStartup": "Lançamento de Startup", + "templateStartupDesc": "Desenvolvimento MVP, financiamento, crescimento", + + "tasksStepTitle": "Adicione suas primeiras tarefas", + "tasksStepDescription": "Divida \"{{projectName}}\" em tarefas acionáveis para começar", + "taskPlaceholder": "Tarefa {{index}} - ex., O que precisa ser feito?", + "addAnotherTask": "Adicionar outra tarefa ({{current}}/{{max}})", + + "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", + + "aboutYouStepTitle": "Conte-nos sobre você", + "aboutYouStepDescription": "Ajude-nos a personalizar sua experiência", + "yourNeedsStepTitle": "Quais são suas principais necessidades?", + "yourNeedsStepDescription": "Selecione todas que se aplicam para nos ajudar a configurar seu espaço de trabalho", + "selected": "selecionado", + "previousToolsLabel": "Que ferramentas você usou antes? (Opcional)", + + "roleSuggestions": { + "designer": "UI/UX, Gráficos, Criativo", + "developer": "Frontend, Backend, Full-stack", + "projectManager": "Planejamento, Coordenação", + "marketing": "Conteúdo, Mídias Sociais, Crescimento", + "sales": "Desenvolvimento de Negócios, Relacionamento com Clientes", + "operations": "Administração, RH, Finanças" + }, + + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "de": "Deutsch", + "alb": "Shqip", + "zh": "简体中文" + }, + + "orgSuggestions": { + "tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"], + "creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"], + "consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"], + "startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"] + }, + + "projectSuggestions": { + "freelancer": ["Projeto Cliente", "Atualização Portfolio", "Marca Pessoal"], + "startup": ["Desenvolvimento MVP", "Lançamento Produto", "Pesquisa Mercado"], + "agency": ["Campanha Cliente", "Estratégia Marca", "Redesign Website"], + "enterprise": ["Migração Sistema", "Otimização Processos", "Treinamento Equipe"] + }, + + "useCaseDescriptions": { + "taskManagement": "Organizar e rastrear tarefas", + "teamCollaboration": "Trabalhar juntos perfeitamente", + "resourcePlanning": "Gerenciar tempo e recursos", + "clientCommunication": "Manter-se conectado com clientes", + "timeTracking": "Monitorar horas do projeto", + "other": "Algo mais" + } } diff --git a/worklenz-frontend/public/locales/zh/account-setup.json b/worklenz-frontend/public/locales/zh/account-setup.json index 51cac1eb..57b8aa25 100644 --- a/worklenz-frontend/public/locales/zh/account-setup.json +++ b/worklenz-frontend/public/locales/zh/account-setup.json @@ -1,14 +1,38 @@ { "continue": "继续", - "setupYourAccount": "设置您的Worklenz账户。", + "setupYourAccount": "设置您的 Worklenz 账户。", "organizationStepTitle": "命名您的组织", - "organizationStepLabel": "为您的Worklenz账户选择一个名称。", + "organizationStepWelcome": "欢迎使用 Worklenz!", + "organizationStepDescription": "让我们从设置您的组织开始。这将是您团队的主要工作空间。", + "organizationStepLabel": "组织名称", + "organizationStepPlaceholder": "例如:Acme 公司", + "organizationStepTooltip": "此名称将显示在您的工作区,并可在设置中更改。", + "organizationStepNeedIdeas": "需要灵感?", + "organizationStepUseDetected": "检测到使用:", + "organizationStepCharacters": "字符", + "organizationStepGoodLength": "长度合适", + "organizationStepTooShort": "太短", + "organizationStepNamingTips": "命名建议", + "organizationStepTip1": "保持简单且易记", + "organizationStepTip2": "体现您的行业或价值观", + "organizationStepTip3": "考虑未来发展", + "organizationStepTip4": "使其独特且有品牌感", + "organizationStepSuggestionsTitle": "名称建议", + "organizationStepCategory1": "科技公司", + "organizationStepCategory2": "创意机构", + "organizationStepCategory3": "咨询公司", + "organizationStepCategory4": "初创企业", + "organizationStepSuggestionsNote": "这些只是帮助您入门的示例。请选择能代表您组织的名称。", + "organizationStepPrivacyNote": "您的组织名称是私有的,仅团队成员可见。", + "projectStepTitle": "创建您的第一个项目", "projectStepLabel": "您现在正在做什么项目?", "projectStepPlaceholder": "例如:营销计划", + "tasksStepTitle": "创建您的第一个任务", "tasksStepLabel": "输入您将在其中完成的几个任务", "tasksStepAddAnother": "添加另一个", + "emailPlaceholder": "电子邮件地址", "invalidEmail": "请输入有效的电子邮件地址", "or": "或", @@ -22,6 +46,165 @@ "skipForNow": "暂时跳过", "formTitle": "创建您的第一个任务。", "step3Title": "邀请您的团队一起工作", - "maxMembers": "(您最多可以邀请5名成员)", - "maxTasks": "(您最多可以创建5个任务)" -} \ No newline at end of file + "maxMembers": "(您最多可以邀请 5 名成员)", + "maxTasks": "(您最多可以创建 5 个任务)", + + "membersStepTitle": "邀请您的团队", + "membersStepDescription": "将团队成员添加到 \"{{organizationName}}\" 并开始协作", + "memberPlaceholder": "团队成员 {{index}} - 输入电子邮件地址", + "validEmailAddress": "有效的电子邮件地址", + "addAnotherTeamMember": "添加另一个团队成员 ({{current}}/{{max}})", + "canInviteLater": "您可以稍后邀请团队成员", + "skipStepDescription": "没有准备好电子邮件地址?没关系!您可以跳过此步骤,稍后从项目面板邀请团队成员。", + + "orgCategoryTech": "科技公司", + "orgCategoryCreative": "创意机构", + "orgCategoryConsulting": "咨询公司", + "orgCategoryStartups": "初创企业", + "namingTip1": "保持简单且易记", + "namingTip2": "体现您的行业或价值观", + "namingTip3": "考虑未来发展", + "namingTip4": "使其独特且有品牌感", + + "aboutYouTitle": "告诉我们关于您的信息", + "aboutYouDescription": "帮助我们个性化您的体验", + "orgTypeQuestion": "哪项最能描述您的组织?", + "userRoleQuestion": "您的角色是什么?", + + "yourNeedsTitle": "您的主要需求是什么?", + "yourNeedsDescription": "请选择所有适用项,帮助我们设置您的工作区", + "yourNeedsQuestion": "您主要如何使用 Worklenz?", + "useCaseTaskOrg": "组织和跟踪任务", + "useCaseTeamCollab": "团队协作", + "useCaseResourceMgmt": "管理时间和资源", + "useCaseClientComm": "与客户保持联系", + "useCaseTimeTrack": "监控项目工时", + "useCaseOther": "其他", + "selectedText": "已选择", + "previousToolsQuestion": "您之前用过哪些工具?(可选)", + "previousToolsPlaceholder": "例如:Asana、Trello、Jira、Monday.com 等", + + "discoveryTitle": "最后一个问题……", + "discoveryDescription": "帮助我们了解您是如何发现 Worklenz 的", + "discoveryQuestion": "您是如何听说我们的?", + "allSetTitle": "一切就绪!", + "allSetDescription": "让我们创建您的第一个项目并开始使用 Worklenz 吧", + "aboutYouStepName": "关于您", + "yourNeedsStepName": "您的需求", + "discoveryStepName": "发现", + "stepProgress": "第 {step} 步,共 3 步:{title}", + + "projectStepHeader": "让我们创建您的第一个项目", + "projectStepSubheader": "从头开始或使用模板更快上手", + "startFromScratch": "从头开始", + "templateSelected": "已选择模板如下", + "quickSuggestions": "快速建议:", + "orText": "或", + "startWithTemplate": "从模板开始", + "clearToSelectTemplate": "请先清空上方项目名称以选择模板", + "templateHeadStart": "使用预设项目结构快速开始", + "browseAllTemplates": "浏览所有模板", + "templatesAvailable": "15+ 行业专用模板可用", + "chooseTemplate": "选择与您的项目类型匹配的模板", + "createProject": "创建项目", + + "templateSoftwareDev": "软件开发", + "templateSoftwareDesc": "敏捷冲刺、缺陷跟踪、版本发布", + "templateMarketing": "市场营销活动", + "templateMarketingDesc": "活动策划、内容日历", + "templateConstruction": "建设项目", + "templateConstructionDesc": "阶段、许可、承包商", + "templateStartup": "初创启动", + "templateStartupDesc": "MVP 开发、融资、增长", + + "tasksStepTitle": "添加您的第一个任务", + "tasksStepDescription": "将 \"{{projectName}}\" 拆分为可执行任务以开始", + "taskPlaceholder": "任务 {{index}} - 例如:需要做什么?", + "addAnotherTask": "添加另一个任务 ({{current}}/{{max}})", + + "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": "其他", + + "aboutYouStepTitle": "告诉我们关于您的信息", + "aboutYouStepDescription": "帮助我们个性化您的体验", + "yourNeedsStepTitle": "您的主要需求是什么?", + "yourNeedsStepDescription": "选择所有适用的选项,帮助我们设置您的工作空间", + "selected": "已选择", + "previousToolsLabel": "您之前使用过哪些工具?(可选)", + + "roleSuggestions": { + "designer": "UI/UX、图形、创意", + "developer": "前端、后端、全栈", + "projectManager": "规划、协调", + "marketing": "内容、社交媒体、增长", + "sales": "业务发展、客户关系", + "operations": "行政、人力资源、财务" + }, + + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "de": "Deutsch", + "alb": "Shqip", + "zh": "简体中文" + }, + + "orgSuggestions": { + "tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"], + "creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"], + "consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"], + "startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"] + }, + + "projectSuggestions": { + "freelancer": ["客户项目", "作品集更新", "个人品牌"], + "startup": ["MVP开发", "产品发布", "市场调研"], + "agency": ["客户活动", "品牌策略", "网站重设计"], + "enterprise": ["系统迁移", "流程优化", "团队培训"] + }, + + "useCaseDescriptions": { + "taskManagement": "组织和跟踪任务", + "teamCollaboration": "无缝协作", + "resourcePlanning": "管理时间和资源", + "clientCommunication": "与客户保持联系", + "timeTracking": "监控项目时间", + "other": "其他" + } +} diff --git a/worklenz-frontend/src/api/survey/survey.api.service.ts b/worklenz-frontend/src/api/survey/survey.api.service.ts new file mode 100644 index 00000000..f4c29509 --- /dev/null +++ b/worklenz-frontend/src/api/survey/survey.api.service.ts @@ -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/v1'; + +export const surveyApiService = { + async getAccountSetupSurvey(): Promise> { + const response = await apiClient.get>(`${API_BASE_URL}/surveys/account-setup`); + return response.data; + }, + + async submitSurveyResponse(data: ISurveySubmissionRequest): Promise> { + const response = await apiClient.post>(`${API_BASE_URL}/surveys/responses`, data); + return response.data; + }, + + async getUserSurveyResponse(surveyId: string): Promise> { + const response = await apiClient.get>(`${API_BASE_URL}/surveys/responses/${surveyId}`); + return response.data; + } +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/account-setup/admin-center-common.css b/worklenz-frontend/src/components/account-setup/admin-center-common.css deleted file mode 100644 index f4c9c421..00000000 --- a/worklenz-frontend/src/components/account-setup/admin-center-common.css +++ /dev/null @@ -1,19 +0,0 @@ -@media (max-width: 1000px) { - .step-content, - .step-form, - .create-first-task-form, - .setup-action-buttons, - .invite-members-form { - width: 400px !important; - } -} - -@media (max-width: 500px) { - .step-content, - .step-form, - .create-first-task-form, - .setup-action-buttons, - .invite-members-form { - width: 200px !important; - } -} diff --git a/worklenz-frontend/src/components/account-setup/members-step.tsx b/worklenz-frontend/src/components/account-setup/members-step.tsx index d3feedc1..a9759f33 100644 --- a/worklenz-frontend/src/components/account-setup/members-step.tsx +++ b/worklenz-frontend/src/components/account-setup/members-step.tsx @@ -1,16 +1,15 @@ -import React, { useEffect, useRef } from 'react'; -import { Form, Input, Button, List, Alert, message, InputRef } from '@/shared/antd-imports'; -import { CloseCircleOutlined, MailOutlined, PlusOutlined } from '@/shared/antd-imports'; +import React, { useEffect, useRef, useState } from 'react'; +import { Form, Input, Button, Typography, Card, Avatar, Tag, Alert, Space, Dropdown, MenuProps } from '@/shared/antd-imports'; +import { CloseCircleOutlined, MailOutlined, PlusOutlined, UserOutlined, CheckCircleOutlined, ExclamationCircleOutlined, GlobalOutlined } from '@/shared/antd-imports'; import { useTranslation } from 'react-i18next'; -import { Typography } from '@/shared/antd-imports'; -import { setTeamMembers, setTasks } from '@/features/account-setup/account-setup.slice'; +import { setTeamMembers } from '@/features/account-setup/account-setup.slice'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/app/store'; import { validateEmail } from '@/utils/validateEmail'; import { sanitizeInput } from '@/utils/sanitizeInput'; -import { Rule } from 'antd/es/form'; +import { setLanguage } from '@/features/i18n/localesSlice'; -const { Title } = Typography; +const { Title, Paragraph, Text } = Typography; interface Email { id: number; @@ -20,163 +19,233 @@ interface Email { interface MembersStepProps { isDarkMode: boolean; styles: any; + token?: any; } -const MembersStep: React.FC = ({ isDarkMode, styles }) => { - const { t } = useTranslation('account-setup'); +const getEmailSuggestions = (orgName?: string) => { + if (!orgName) return []; + const cleanOrgName = orgName.toLowerCase().replace(/[^a-z0-9]/g, ''); + return [`info@${cleanOrgName}.com`, `team@${cleanOrgName}.com`, `hello@${cleanOrgName}.com`, `contact@${cleanOrgName}.com`]; +}; + +const getRoleSuggestions = (t: any) => [ + { role: 'Designer', icon: '🎨', description: t('roleSuggestions.designer') }, + { role: 'Developer', icon: '💻', description: t('roleSuggestions.developer') }, + { role: 'Project Manager', icon: '📊', description: t('roleSuggestions.projectManager') }, + { role: 'Marketing', icon: '📢', description: t('roleSuggestions.marketing') }, + { role: 'Sales', icon: '💼', description: t('roleSuggestions.sales') }, + { role: 'Operations', icon: '⚙️', description: t('roleSuggestions.operations') } +]; + +const MembersStep: React.FC = ({ isDarkMode, styles, token }) => { + const { t, i18n } = useTranslation('account-setup'); const { teamMembers, organizationName } = useSelector( (state: RootState) => state.accountSetupReducer ); - const inputRefs = useRef<(InputRef | null)[]>([]); + const { language } = useSelector((state: RootState) => state.localesReducer); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const dispatch = useDispatch(); - const [form] = Form.useForm(); + const [focusedIndex, setFocusedIndex] = useState(null); + const [showSuggestions, setShowSuggestions] = useState(false); + const [validatedEmails, setValidatedEmails] = useState>(new Set()); + + const emailSuggestions = getEmailSuggestions(organizationName); const addEmail = () => { - if (teamMembers.length == 5) return; - + if (teamMembers.length >= 5) return; const newId = teamMembers.length > 0 ? Math.max(...teamMembers.map(t => t.id)) + 1 : 0; dispatch(setTeamMembers([...teamMembers, { id: newId, value: '' }])); - setTimeout(() => { - inputRefs.current[newId]?.focus(); - }, 0); + setTimeout(() => inputRefs.current[teamMembers.length]?.focus(), 100); }; const removeEmail = (id: number) => { - if (teamMembers.length > 1) { - dispatch(setTeamMembers(teamMembers.filter(teamMember => teamMember.id !== id))); - } + if (teamMembers.length > 1) dispatch(setTeamMembers(teamMembers.filter(teamMember => teamMember.id !== id))); }; const updateEmail = (id: number, value: string) => { const sanitizedValue = sanitizeInput(value); - dispatch( - setTeamMembers( - teamMembers.map(teamMember => - teamMember.id === id ? { ...teamMember, value: sanitizedValue } : teamMember - ) - ) - ); + dispatch(setTeamMembers(teamMembers.map(teamMember => teamMember.id === id ? { ...teamMember, value: sanitizedValue } : teamMember))); }; - const handleKeyPress = (e: React.KeyboardEvent) => { - const input = e.currentTarget as HTMLInputElement; - if (!input.value.trim()) return; - e.preventDefault(); - addEmail(); + const handleKeyPress = (e: React.KeyboardEvent, index: number) => { + if (e.key === 'Enter') { + const input = e.currentTarget as HTMLInputElement; + if (input.value.trim() && validateEmail(input.value.trim())) { + e.preventDefault(); + if (index === teamMembers.length - 1 && teamMembers.length < 5) addEmail(); + else if (index < teamMembers.length - 1) inputRefs.current[index + 1]?.focus(); + } + } }; - // Function to set ref that doesn't return anything (void) - const setInputRef = (index: number) => (el: InputRef | null) => { - inputRefs.current[index] = el; + const handleSuggestionClick = (suggestion: string) => { + const emptyEmailIndex = teamMembers.findIndex(member => !member.value.trim()); + if (emptyEmailIndex !== -1) { + updateEmail(teamMembers[emptyEmailIndex].id, suggestion); + } else if (teamMembers.length < 5) { + const newId = teamMembers.length > 0 ? Math.max(...teamMembers.map(t => t.id)) + 1 : 0; + dispatch(setTeamMembers([...teamMembers, { id: newId, value: suggestion }])); + } + setShowSuggestions(false); }; useEffect(() => { - setTimeout(() => { - inputRefs.current[teamMembers.length - 1]?.focus(); - // Set initial form values - const initialValues: Record = {}; - teamMembers.forEach(teamMember => { - initialValues[`email-${teamMember.id}`] = teamMember.value; - }); - form.setFieldsValue(initialValues); - }, 200); + setTimeout(() => inputRefs.current[0]?.focus(), 200); }, []); - const formRules = { - email: [ - { - validator: async (_: any, value: string) => { - if (!value) return; - if (!validateEmail(value)) { - throw new Error(t('invalidEmail')); - } - }, - }, - ], + const getEmailStatus = (email: string, memberId: number) => { + if (!email.trim()) return 'empty'; + if (!validatedEmails.has(memberId)) return 'empty'; + return validateEmail(email) ? 'valid' : 'invalid'; }; + const handleBlur = (memberId: number, email: string) => { + setFocusedIndex(null); + if (email.trim()) setValidatedEmails(prev => new Set(prev).add(memberId)); + }; + + const languages = [ + { key: 'en', label: t('languages.en'), flag: '🇺🇸' }, + { key: 'es', label: t('languages.es'), flag: '🇪🇸' }, + { key: 'pt', label: t('languages.pt'), flag: '🇵🇹' }, + { key: 'de', label: t('languages.de'), flag: '🇩🇪' }, + { key: 'alb', label: t('languages.alb'), flag: '🇦🇱' }, + { key: 'zh', label: t('languages.zh'), flag: '🇨🇳' } + ]; + + const handleLanguageChange = (languageKey: string) => { + dispatch(setLanguage(languageKey)); + i18n.changeLanguage(languageKey); + }; + + const currentLanguage = languages.find(lang => lang.key === language) || languages[0]; + const languageMenuItems: MenuProps['items'] = languages.map(lang => ({ key: lang.key, label:
{lang.flag}{lang.label}
, onClick: () => handleLanguageChange(lang.key) })); + return ( -
- - - {t('step3Title')} "<mark>{organizationName}</mark>". + <div className="w-full members-step"> + {/* Header */} + <div className="text-center mb-8"> + <Title level={3} className="mb-2" style={{ color: token?.colorText }}> + {t('membersStepTitle')} - - - {t('step3InputLabel')}  {t('maxMembers')} - - } - > - ( - -
- + + {t('membersStepDescription', { organizationName })} + +
+ + {/* Team Members List */} +
+
+ {teamMembers.map((teamMember, index) => { + const emailStatus = getEmailStatus(teamMember.value, teamMember.id); + return ( +
+ : + emailStatus === 'invalid' ? : + + } + /> + +
updateEmail(teamMember.id, e.target.value)} - onPressEnter={handleKeyPress} - ref={setInputRef(index)} - status={teamMember.value && !validateEmail(teamMember.value) ? 'error' : ''} - id={`member-${index}`} + onKeyPress={e => handleKeyPress(e, index)} + onFocus={() => setFocusedIndex(index)} + onBlur={() => handleBlur(teamMember.id, teamMember.value)} + ref={el => inputRefs.current[index] = el} + className="border-0 shadow-none" + style={{ + backgroundColor: 'transparent', + color: token?.colorText + }} + prefix={} + status={emailStatus === 'invalid' ? 'error' : undefined} + suffix={ + emailStatus === 'valid' ? ( + + ) : emailStatus === 'invalid' ? ( + + ) : null + } /> - -
+ + {teamMembers.length > 1 && ( +
- - )} - /> - -
+ + {/* Add Member Button */} + {teamMembers.length < 5 && ( + + )} +
+ + {/* Skip Option */} +
+
- - + /> +
+
); }; -export default MembersStep; +export default MembersStep; \ No newline at end of file diff --git a/worklenz-frontend/src/components/account-setup/organization-step.tsx b/worklenz-frontend/src/components/account-setup/organization-step.tsx index ec69b999..87e4ab9a 100644 --- a/worklenz-frontend/src/components/account-setup/organization-step.tsx +++ b/worklenz-frontend/src/components/account-setup/organization-step.tsx @@ -1,31 +1,40 @@ -import React, { useEffect, useRef } from 'react'; -import { Form, Input, InputRef, Typography } from '@/shared/antd-imports'; +import React, { useEffect, useRef, useState } from 'react'; +import { Form, Input, InputRef, Typography, Card, Tooltip } from '@/shared/antd-imports'; import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { setOrganizationName } from '@/features/account-setup/account-setup.slice'; import { RootState } from '@/app/store'; import { sanitizeInput } from '@/utils/sanitizeInput'; -import './admin-center-common.css'; -const { Title } = Typography; +const { Title, Paragraph, Text } = Typography; interface Props { onEnter: () => void; styles: any; organizationNamePlaceholder: string; + organizationNameInitialValue?: string; + isDarkMode: boolean; + token?: any; } export const OrganizationStep: React.FC = ({ onEnter, styles, organizationNamePlaceholder, + organizationNameInitialValue, + isDarkMode, + token, }) => { const { t } = useTranslation('account-setup'); const dispatch = useDispatch(); const { organizationName } = useSelector((state: RootState) => state.accountSetupReducer); const inputRef = useRef(null); + // Autofill organization name if not already set useEffect(() => { + if (!organizationName && organizationNameInitialValue) { + dispatch(setOrganizationName(organizationNameInitialValue)); + } setTimeout(() => inputRef.current?.focus(), 300); }, []); @@ -40,25 +49,85 @@ export const OrganizationStep: React.FC = ({ }; return ( -
- - - {t('organizationStepTitle')} + <div className="w-full organization-step"> + {/* Header */} + <div className="text-center mb-8"> + <Title level={3} className="mb-2" style={{ color: token?.colorText }}> + {t('organizationStepWelcome')} - - {t('organizationStepLabel')}} + + {t('organizationStepDescription')} + + + + {/* Main Form Card */} +
+ + + + {t('organizationStepLabel')} + + + + ⓘ + + +
+ } + > + +
+ + {/* Character Count and Validation */} +
+ + {organizationName.length}/50 {t('organizationStepCharacters')} + + {organizationName.length > 0 && ( +
+ {organizationName.length >= 2 ? ( + ✓ {t('organizationStepGoodLength')} + ) : ( + ⚠ {t('organizationStepTooShort')} + )} +
+ )} +
+ + + + {/* Footer Note */} +
- - - + + 🔒 {t('organizationStepPrivacyNote')} + +
+ ); -}; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/account-setup/project-step.tsx b/worklenz-frontend/src/components/account-setup/project-step.tsx index 1447bfd8..4810859b 100644 --- a/worklenz-frontend/src/components/account-setup/project-step.tsx +++ b/worklenz-frontend/src/components/account-setup/project-step.tsx @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { Button, Drawer, Form, Input, InputRef, Select, Typography } from '@/shared/antd-imports'; +import { Button, Drawer, Form, Input, InputRef, Typography, Card, Row, Col, Tag, Tooltip, Spin, Alert } from '@/shared/antd-imports'; import TemplateDrawer from '../common/template-drawer/template-drawer'; import { RootState } from '@/app/store'; @@ -13,7 +13,7 @@ import { sanitizeInput } from '@/utils/sanitizeInput'; import { projectTemplatesApiService } from '@/api/project-templates/project-templates.api.service'; import logger from '@/utils/errorLogger'; -import { IAccountSetupRequest } from '@/types/project-templates/project-templates.types'; +import { IAccountSetupRequest, IWorklenzTemplate, IProjectTemplate } from '@/types/project-templates/project-templates.types'; import { evt_account_setup_template_complete } from '@/shared/worklenz-analytics-events'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; @@ -24,15 +24,43 @@ import { setUser } from '@/features/user/userSlice'; import { setSession } from '@/utils/session-helper'; import { IAuthorizeResponse } from '@/types/auth/login.types'; -const { Title } = Typography; +const { Title, Paragraph, Text } = Typography; interface Props { onEnter: () => void; styles: any; isDarkMode: boolean; + token?: any; } -export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = false }) => { +// Default icon mapping for templates (fallback if no image_url) +const getTemplateIcon = (name?: string) => { + if (!name) return '📁'; + const lowercaseName = name.toLowerCase(); + if (lowercaseName.includes('software') || lowercaseName.includes('development')) return '💻'; + if (lowercaseName.includes('marketing') || lowercaseName.includes('campaign')) return '📢'; + if (lowercaseName.includes('construction') || lowercaseName.includes('building')) return '🏗️'; + if (lowercaseName.includes('startup') || lowercaseName.includes('launch')) return '🚀'; + if (lowercaseName.includes('design') || lowercaseName.includes('creative')) return '🎨'; + if (lowercaseName.includes('education') || lowercaseName.includes('learning')) return '📚'; + if (lowercaseName.includes('event') || lowercaseName.includes('planning')) return '📅'; + if (lowercaseName.includes('retail') || lowercaseName.includes('sales')) return '🛍️'; + return '📁'; +}; + +const getProjectSuggestions = (orgType?: string) => { + const suggestions: Record = { + 'freelancer': ['Client Website', 'Logo Design', 'Content Writing', 'App Development'], + 'startup': ['MVP Development', 'Product Launch', 'Marketing Campaign', 'Investor Pitch'], + 'small_medium_business': ['Q1 Sales Initiative', 'Website Redesign', 'Process Improvement', 'Team Training'], + 'agency': ['Client Campaign', 'Brand Strategy', 'Website Project', 'Creative Brief'], + 'enterprise': ['Digital Transformation', 'System Migration', 'Annual Planning', 'Department Initiative'], + 'other': ['New Project', 'Team Initiative', 'Q1 Goals', 'Special Project'] + }; + return suggestions[orgType || 'other'] || suggestions['other']; +}; + +export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = false, token }) => { const { t } = useTranslation('account-setup'); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -42,13 +70,58 @@ export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = fal useEffect(() => { setTimeout(() => inputRef.current?.focus(), 200); + fetchTemplates(); }, []); - const { projectName, templateId, organizationName } = useSelector( + const fetchTemplates = async () => { + try { + setLoadingTemplates(true); + setTemplateError(null); + + // Fetch list of available templates + const templatesResponse = await projectTemplatesApiService.getWorklenzTemplates(); + + if (templatesResponse.done && templatesResponse.body) { + // Fetch detailed information for first 4 templates for preview + const templateDetails = await Promise.all( + templatesResponse.body.slice(0, 4).map(async (template) => { + if (template.id) { + try { + const detailResponse = await projectTemplatesApiService.getByTemplateId(template.id); + return detailResponse.done ? detailResponse.body : null; + } catch (error) { + logger.error(`Failed to fetch template details for ${template.id}`, error); + return null; + } + } + return null; + }) + ); + + // Filter out null results and set templates + const validTemplates = templateDetails.filter((template): template is IProjectTemplate => template !== null); + setTemplates(validTemplates); + } + } catch (error) { + logger.error('Failed to fetch templates', error); + setTemplateError('Failed to load templates'); + } finally { + setLoadingTemplates(false); + } + }; + + + const { projectName, templateId, organizationName, surveyData } = useSelector( (state: RootState) => state.accountSetupReducer ); const [open, setOpen] = useState(false); const [creatingFromTemplate, setCreatingFromTemplate] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState(templateId || null); + const [templates, setTemplates] = useState([]); + const [loadingTemplates, setLoadingTemplates] = useState(true); + const [templateError, setTemplateError] = useState(null); + + const projectSuggestions = getProjectSuggestions(surveyData.organization_type); const handleTemplateSelected = (templateId: string) => { if (!templateId) return; @@ -74,8 +147,6 @@ export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = fal if (res.done && res.body.id) { toggleTemplateSelector(false); trackMixpanelEvent(evt_account_setup_template_complete); - - // Refresh user session to update setup_completed status try { const authResponse = await dispatch(verifyAuthentication()).unwrap() as IAuthorizeResponse; if (authResponse?.authenticated && authResponse?.user) { @@ -85,7 +156,6 @@ export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = fal } catch (error) { logger.error('Failed to refresh user session after template setup completion', error); } - navigate(`/worklenz/projects/${res.body.id}?tab=tasks-list&pinned_tab=tasks-list`); } } catch (error) { @@ -94,8 +164,7 @@ export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = fal }; const onPressEnter = () => { - if (!projectName.trim()) return; - onEnter(); + if (projectName.trim()) onEnter(); }; const handleProjectNameChange = (e: React.ChangeEvent) => { @@ -103,43 +172,205 @@ export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = fal dispatch(setProjectName(sanitizedValue)); }; + const handleProjectNameFocus = () => { + if (templateId) { + dispatch(setTemplateId(null)); + setSelectedTemplate(null); + } + }; + + const handleSuggestionClick = (suggestion: string) => { + dispatch(setProjectName(suggestion)); + inputRef.current?.focus(); + }; + return ( -
-
- - - {t('projectStepTitle')} - - - {t('projectStepLabel')}} - > - - -
-
- - {t('or')} + <div className="w-full project-step"> + {/* Header */} + <div className="text-center mb-8"> + <Title level={3} className="mb-2" style={{ color: token?.colorText }}> + {t('projectStepHeader')} -
+ + {t('projectStepSubheader')} +
-
- + {/* Project Name Section */} +
+ +
+
+ + {t('startFromScratch')} + + {templateId && ( + + {t('templateSelected')} + + )} +
+
+ + {t('projectStepLabel')}} + > + + + +
+ {t('quickSuggestions')} +
+ {projectSuggestions.map((suggestion, index) => ( + + ))} +
+
+
+ +
+
+
+
+
+ {t('orText')} +
+
+ +
+
+ {t('startWithTemplate')} + + {t('templateHeadStart')} + +
+ + {/* Template Preview Cards */} +
+ {loadingTemplates ? ( +
+ +
+ Loading templates... +
+
+ ) : templateError ? ( + + Retry + + } + /> + ) : ( + + {templates.map((template) => ( + + { + setSelectedTemplate(template.id || null); + dispatch(setTemplateId(template.id || '')); + }} + > +
+ {template.image_url ? ( + {template.name} { + // Fallback to icon if image fails to load + e.currentTarget.style.display = 'none'; + if (e.currentTarget.nextSibling) { + (e.currentTarget.nextSibling as HTMLElement).style.display = 'block'; + } + }} + /> + ) : null} + + {getTemplateIcon(template.name)} + +
+ + {template.name || 'Untitled Template'} + +
+ {template.phases?.slice(0, 3).map((phase, index) => ( + + {phase.name} + + ))} + {(template.phases?.length || 0) > 3 && ( + +{(template.phases?.length || 0) - 3} more + )} +
+
+
+
+ + ))} +
+ )} +
+ +
+ +
+ {t('templatesAvailable')} +
+
+
+ + {/* Template Drawer */} {createPortal( + + {t('templateDrawerTitle')} + + + {t('chooseTemplate')} + +
+ } width={1000} onClose={() => toggleTemplateSelector(false)} open={open} @@ -152,11 +383,13 @@ export const ProjectStep: React.FC = ({ onEnter, styles, isDarkMode = fal type="primary" onClick={() => createFromTemplate()} loading={creatingFromTemplate} + disabled={!templateId} > - {t('create')} +{t('createProject')}
} + style={{ backgroundColor: token?.colorBgLayout }} > = ({ onEnter, styles, isDarkMode = fal )}
); -}; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/account-setup/survey-step.tsx b/worklenz-frontend/src/components/account-setup/survey-step.tsx new file mode 100644 index 00000000..2394ac06 --- /dev/null +++ b/worklenz-frontend/src/components/account-setup/survey-step.tsx @@ -0,0 +1,368 @@ +import React from 'react'; +import { Form, Input, Typography, Button, Progress, Space } from '@/shared/antd-imports'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { setSurveyData, setSurveySubStep } 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, Paragraph } = Typography; +const { TextArea } = Input; + +interface Props { + onEnter: () => void; + styles: any; + isDarkMode: boolean; + token?: any; +} + +interface SurveyPageProps { + styles: any; + isDarkMode: boolean; + token?: any; + surveyData: IAccountSetupSurveyData; + handleSurveyDataChange: (field: keyof IAccountSetupSurveyData, value: any) => void; + handleUseCaseToggle?: (value: UseCase) => void; +} + +// Page 1: About You +const AboutYouPage: React.FC = ({ styles, token, surveyData, handleSurveyDataChange }) => { + const { t } = useTranslation('account-setup'); + + const organizationTypeOptions: { value: OrganizationType; label: string; icon?: string }[] = [ + { value: 'freelancer', label: t('organizationTypeFreelancer'), icon: '👤' }, + { value: 'startup', label: t('organizationTypeStartup'), icon: '🚀' }, + { value: 'small_medium_business', label: t('organizationTypeSmallMediumBusiness'), icon: '🏢' }, + { value: 'agency', label: t('organizationTypeAgency'), icon: '🎯' }, + { value: 'enterprise', label: t('organizationTypeEnterprise'), icon: '🏛️' }, + { value: 'other', label: t('organizationTypeOther'), icon: '📋' }, + ]; + + const userRoleOptions: { value: UserRole; label: string; icon?: string }[] = [ + { value: 'founder_ceo', label: t('userRoleFounderCeo'), icon: '👔' }, + { value: 'project_manager', label: t('userRoleProjectManager'), icon: '📊' }, + { value: 'software_developer', label: t('userRoleSoftwareDeveloper'), icon: '💻' }, + { value: 'designer', label: t('userRoleDesigner'), icon: '🎨' }, + { value: 'operations', label: t('userRoleOperations'), icon: '⚙️' }, + { value: 'other', label: t('userRoleOther'), icon: '✋' }, + ]; + + return ( +
+
+ + {t('aboutYouStepTitle')} + + + {t('aboutYouStepDescription')} + +
+ + {/* Organization Type */} + + +
+ {organizationTypeOptions.map((option) => { + const isSelected = surveyData.organization_type === option.value; + return ( + + ); + })} +
+
+ + {/* User Role */} + + +
+ {userRoleOptions.map((option) => { + const isSelected = surveyData.user_role === option.value; + return ( + + ); + })} +
+
+
+ ); +}; + +// Page 2: Your Needs +const YourNeedsPage: React.FC = ({ styles, token, surveyData, handleSurveyDataChange, handleUseCaseToggle }) => { + const { t } = useTranslation('account-setup'); + + const useCaseOptions: { value: UseCase; label: string; description: string }[] = [ + { value: 'task_management', label: t('mainUseCasesTaskManagement'), description: 'Organize and track tasks' }, + { value: 'team_collaboration', label: t('mainUseCasesTeamCollaboration'), description: 'Work together seamlessly' }, + { value: 'resource_planning', label: t('mainUseCasesResourcePlanning'), description: 'Manage time and resources' }, + { value: 'client_communication', label: t('mainUseCasesClientCommunication'), description: 'Stay connected with clients' }, + { value: 'time_tracking', label: t('mainUseCasesTimeTracking'), description: 'Monitor project hours' }, + { value: 'other', label: t('mainUseCasesOther'), description: 'Something else' }, + ]; + + const onUseCaseClick = (value: UseCase) => { + if (handleUseCaseToggle) { + handleUseCaseToggle(value); + } else { + const currentUseCases = surveyData.main_use_cases || []; + const isSelected = currentUseCases.includes(value); + const newUseCases = isSelected ? currentUseCases.filter(useCase => useCase !== value) : [...currentUseCases, value]; + handleSurveyDataChange('main_use_cases', newUseCases); + } + }; + + return ( +
+
+ + {t('yourNeedsStepTitle')} + + + {t('yourNeedsStepDescription')} + +
+ + {/* Main Use Cases */} + + +
+ {useCaseOptions.map((option) => { + const isSelected = (surveyData.main_use_cases || []).includes(option.value); + return ( + + ); + })} +
+ {surveyData.main_use_cases && surveyData.main_use_cases.length > 0 && ( +

+ {surveyData.main_use_cases.length} {t('selected')} +

+ )} +
+ + {/* Previous Tools */} + + +