· 6 years ago · Jan 22, 2020, 09:58 PM
1import * as React from "react";
2import {useDispatch, useSelector} from "react-redux";
3import {toast} from "react-toastify";
4import cn from "classnames";
5import {differenceInSeconds} from "date-fns";
6import moment from 'moment';
7
8import _ from "lodash";
9
10import {Store} from "~/store";
11
12import apiHandler from "~/services/api.service";
13
14import * as api from "~/resources/user/user.api";
15import {USER_UPDATE} from "~/resources/user/user.constants";
16
17import {getSleepTimeFromTotalHours, getTotalHoursFromSleepTime} from "~helpers/helpers";
18
19import * as scopeApi from "~/resources/scope/scope.api";
20import * as scopeActions from "~/resources/scope/scope.actions";
21import * as scopeSelectors from "~/resources/scope/scope.selectors";
22import * as ScopeTypes from "~/resources/scope/scope.types";
23import * as scopeHelpers from "~/resources/scope/scope.helpers";
24import * as scopeConstants from "~/resources/scope/scope.constants";
25
26import * as Icons from "~/assets/icons";
27import onboardingAssignmentImage from "~/assets/images/onboarding-assignment.png";
28import onboardingGoalsPopupImage from "~/assets/images/onboarding-goals-popup.png";
29
30import OnboardingLayout from "~/layouts/onboarding";
31
32import PageLoader from "~/components/page-loader";
33import Button from "~/components/button";
34import Checkbox from "~/components/checkbox";
35import Chat from "~/components/chat";
36import HelpPopup from "~/components/help-popup";
37import Confirm from "~/components/Confirm";
38import {Step4Chat} from "./Step4Chat";
39import ResetSteps from "~/components/reset-steps";
40
41import * as styles from "./assignment.css";
42
43const botMessages: { [key: number]: ScopeTypes.Message } = {
44 0: {isBot: true, text: "Сколько часов в сутки ты спишь?"},
45 2: {isBot: true, text: "Сколько ты планируешь заработать за этот год?"},
46 4: {isBot: true, text: "Отлично! А теперь распредели свои ресурсы между целями"},
47};
48
49const acceptNumber = /^[0-9]*$/;
50
51const uncheckedBoth = (g: ScopeTypes.Goal) => !g.joyAtStart && !g.joyAtEnd;
52const checkedBoth = (g: ScopeTypes.Goal) => g.joyAtStart && g.joyAtEnd;
53const checkedOne = (g: ScopeTypes.Goal) => !checkedBoth(g) && !uncheckedBoth(g);
54
55function renderIcon(metaId: ScopeTypes.Scope["metaId"]) {
56 const Icon = scopeHelpers.getScopeIcon(metaId);
57 return <Icon className={styles.scopeIcon}/>;
58}
59
60function renderLabel(field: string) {
61 if (field === "hours") return "часов";
62 if (field === "money") return "₽";
63 return "";
64}
65
66function withStopPropagation(callback: Function) {
67 return (event: React.MouseEvent) => {
68 event.stopPropagation();
69 callback();
70 };
71}
72
73const secondsInDay = 86400;
74
75function useInterval(callback: Function, delay: number | null) {
76 const savedCallback = React.useRef<Function>();
77
78 React.useEffect(() => {
79 savedCallback.current = callback;
80 });
81
82 React.useEffect(() => {
83 function tick() {
84 if (savedCallback.current) savedCallback.current();
85 }
86
87 let id: number;
88 if (delay !== null) id = window.setInterval(tick, delay);
89 return () => clearInterval(id);
90 }, [delay]);
91}
92
93function formatCountdown(countdown: number) {
94 let hours: string | number = Math.floor(countdown / 3600);
95 let minutes: string | number = Math.floor((countdown - (hours * 3600)) / 60);
96 let seconds: string | number = countdown - (hours * 3600) - (minutes * 60);
97
98 if (hours < 10) hours = `0${hours}`;
99 if (minutes < 10) minutes = `0${minutes}`;
100 if (seconds < 10) seconds = `0${seconds}`;
101 return `${hours}:${minutes}:${seconds}`;
102}
103
104function addDefaults(scope: ScopeTypes.Scope) {
105 return {
106 ...scope,
107 goals: scope.goals.map(goal => ({
108 money: 0,
109 hours: 0,
110 joyAtEnd: false,
111 joyAtStart: false,
112 ...goal,
113 })),
114 };
115}
116
117const sendGoalInputData = _.throttle((data: any) => {
118 apiHandler.post("/scope/estimate", data);
119}, 1000);
120
121function OnboardingAssignment() {
122 const dispatch = useDispatch();
123 const user = useSelector<Store, Store["user"]>(store => store.user);
124
125 const _scopes = useSelector(scopeSelectors.getScopes);
126 console.log('scopes at start', _scopes);
127
128 const [isFetching, setFetching] = React.useState(false);
129 const [isUpdatingGoals, setUpdatingGoals] = React.useState(false);
130
131 const [popup, setPopup] = React.useState(false);
132 const closePopup = React.useCallback(() => {
133 window.localStorage.setItem("estimatesStarted", "true");
134 setPopup(false);
135 }, []);
136
137 const [countdown, setCountdown] = React.useState(0);
138 useInterval(() => setCountdown(c => c - 1), countdown ? 1000 : null);
139
140 React.useLayoutEffect(() => {
141 const estimatesStarted = window.localStorage.getItem("estimatesStarted");
142 if (estimatesStarted) return;
143
144 let estimatesOpenDate: string | Date | null = window.localStorage.getItem("estimatesOpenDate");
145 if (!estimatesOpenDate) {
146 estimatesOpenDate = moment();
147 window.localStorage.setItem("estimatesOpenDate", estimatesOpenDate.valueOf());
148 }
149
150 const secondsFromOpen = differenceInSeconds(new Date(), new Date(Number(estimatesOpenDate)));
151
152 if (secondsFromOpen > secondsInDay) {
153 window.localStorage.setItem("estimatesStarted", "true");
154 } else {
155 setCountdown(secondsInDay - secondsFromOpen);
156 setPopup(true);
157 }
158 }, []);
159
160 const [step, setStep] = React.useState(1);
161 React.useEffect(() => {
162 localStorage.setItem("onboarding-step-4-step", step.toString());
163 }, [step]);
164
165 const [stepFetched, fetchStep] = React.useState(false);
166 if (!stepFetched) {
167 api.me().then((r: any) => {
168 const {data: {step}} = r;
169 if (step === scopeConstants.STEP_ESTIMATES) {
170 setStep(2);
171 }
172 }).finally(() => fetchStep(true));
173 }
174
175 async function finishWaysStep() {
176 try {
177 setFetching(true);
178 await dispatch(scopeActions.completeStep({step: scopeConstants.STEP_WAYS}));
179 setStep(2);
180 } finally {
181 setFetching(false);
182 }
183 }
184
185 const [scopes, setScopes] = React.useState<ScopeTypes.Scope[]>([]);
186 React.useEffect(() => {
187 localStorage.setItem("onboarding-step-4-scopes", JSON.stringify(scopes));
188 }, [scopes]);
189
190 const [messages, setMessages] = React.useState([botMessages[0]]);
191 React.useEffect(() => {
192 localStorage.setItem("onboarding-step-4-chat-2", JSON.stringify(messages));
193 }, [messages]);
194
195 const [focusedInput, focusTotalsInput] = React.useState<string>("");
196 const [totalHours, setHours] = React.useState<number>(user.hoursPerYear);
197 const [totalMoney, setMoney] = React.useState<number>(Number(user.moneyPerYear));
198 React.useEffect(() => {
199 const storageHours = localStorage.getItem('onboarding-step-4-totalHours');
200 const storageMoney = localStorage.getItem('onboarding-step-4-totalMoney');
201 if (totalHours > 0) {
202 if (!storageHours && storageHours !== null) {
203 localStorage.setItem('onboarding-step-4-totalHours', totalHours);
204 }
205
206 if (messages.length === 1) {
207 setMessages([...messages, {
208 isBot: false,
209 text: getSleepTimeFromTotalHours(totalHours).toString(),
210 editable: true
211 }, botMessages[2]]);
212 }
213 localStorage.setItem('onboarding-step-4-chat-2', JSON.stringify([botMessages[2]]));
214 }
215
216 if (totalMoney > 0) {
217 if (!storageMoney && storageMoney !== null) {
218 localStorage.setItem('onboarding-step-4-totalMoney', Number(totalMoney));
219 }
220
221 if (messages.length === 3) {
222 setMessages([...messages, {isBot: false, text: totalMoney.toString(), editable: true}, botMessages[4]]);
223 }
224 }
225 }), [totalHours, totalMoney];
226
227 React.useLayoutEffect(() => {
228 const step = localStorage.getItem("onboarding-step-4-step");
229 if (step) setStep(parseInt(step, 10));
230
231 const savedScopes = localStorage.getItem("onboarding-step-4-scopes");
232 console.log('render scopes', _scopes);
233 const scopes = [..._scopes]; ///savedScopes ? (JSON.parse(savedScopes) as ScopeTypes.Scope[]) : _scopes;
234 setScopes(scopes.map(addDefaults));
235
236 const totalHours = localStorage.getItem("onboarding-step-4-totalHours");
237 if (totalHours) setHours(parseInt(totalHours, 10));
238
239 const totalMoney = localStorage.getItem("onboarding-step-4-totalMoney");
240 if (totalMoney) setMoney(parseInt(totalMoney, 10));
241
242 const messages = localStorage.getItem("onboarding-step-4-chat-2");
243 if (messages) setMessages(JSON.parse(messages));
244 }, []);
245
246 const [isConfirmOpen, toggleConfirmDelete] = React.useState<boolean>(false);
247 const [isChatFinished, setChatFinished] = React.useState<boolean>(false);
248
249 const [answer, setAnswer] = React.useState("");
250 const handleAnswerChange = React.useCallback(e => {
251 if (acceptNumber.test(e.target.value)) setAnswer(e.target.value);
252 }, []);
253
254 const send = React.useCallback((event) => {
255 event.preventDefault();
256
257 const number = parseInt(answer, 10);
258 if (!number) return;
259
260 const isDream = messages.length === 1;
261
262 if (isDream) {
263 if (number >= 24) {
264 toast.error("Время сна не может быть больше 24 часов в сутки");
265 return;
266 }
267 const now = new Date();
268 const hours = getTotalHoursFromSleepTime(number);
269 setHours(hours);
270 localStorage.setItem("onboarding-step-4-totalHours", hours.toString());
271 apiHandler.post("/scope/estimate", {
272 totalHours: hours
273 });
274 } else {
275 setMoney(number);
276 localStorage.setItem("onboarding-step-4-totalMoney", number.toString());
277 apiHandler.post("/scope/estimate", {
278 totalMoney: number
279 });
280 }
281
282 setMessages([...messages, {isBot: false, text: answer}, botMessages[messages.length + 1]]);
283 setAnswer("");
284 }, [answer]);
285
286 const [scopeId, setScopeId] = React.useState<number>();
287 const [goalId, setGoalId] = React.useState<number>();
288 const confirmRemoval = React.useCallback((scopeId: number, goalId: number): void => {
289 toggleConfirmDelete(true);
290 setScopeId(scopeId);
291 setGoalId(goalId);
292 }, [setScopeId, setGoalId, toggleConfirmDelete]);
293
294 const removeGoal = React.useCallback(async (scopeId, goalId) => {
295 toggleConfirmDelete(false);
296 try {
297 setScopes(scopes => scopes.map(
298 scope => {
299 if (scope.scopeId !== scopeId) return scope;
300 return {...scope, goals: scope.goals.filter(goal => goal.goalId !== goalId)};
301 },
302 ));
303 await apiHandler.post('/scope/remove_goal', {goalId});
304 toast.success('Цель успешно удалена');
305 } catch (error) {
306 toast.error('Не удалось удалить цель. Попробуйте ещё раз');
307 }
308 }, []);
309
310 const editableInputRef = React.useRef<HTMLInputElement | null>();
311 React.useLayoutEffect(() => {
312 if (editableInputRef.current) editableInputRef.current.focus();
313 });
314
315 const [editableEntity, _setEditableEntity] = React.useState<{
316 scopeId: ScopeTypes.Scope["scopeId"],
317 goal: ScopeTypes.Goal,
318 field: string,
319 } | null>(null);
320
321 const setEditableEntity = React.useCallback((scopeId, goal, field) => {
322 _setEditableEntity(entity => {
323 if (
324 entity
325 && entity.scopeId === scopeId
326 && entity.goal.goalId === goal.goalId
327 && entity.field === field
328 ) {
329 return entity;
330 }
331
332 return {scopeId, goal, field};
333 });
334 }, []);
335
336 function addGoal(scopeId: number) {
337 return async () => {
338 const data = await dispatch(scopeActions.addGoal({scopeId, goal: ''}));
339 setScopes(scopes => data.scopes.map(scope => ({
340 ...scope,
341 ...scopeHelpers.getScopeMeta(scope.metaId)
342 })));
343 };
344 }
345
346 async function saveEditableEntity(event: React.FormEvent) {
347 event.preventDefault();
348
349 if (!editableEntity) return;
350
351 const entity = editableEntity;
352 _setEditableEntity(null);
353
354 setScopes(scopes => scopes.map( //// TODO combine store with api. optimistic if error???
355 scope => {
356 if (entity && scope.scopeId !== entity.scopeId) return scope;
357 return {
358 ...scope,
359 goals: scope.goals.map(goal =>
360 (entity && goal.goalId === entity.goal.goalId)
361 ? entity.goal
362 : goal
363 )
364 };
365 },
366 ));
367
368 await apiHandler.post('/scope/update_goal', {
369 goalId: entity.goal.goalId,
370 goal: entity.goal.name,
371 hours: entity.goal.hours,
372 money: entity.goal.money
373 });
374 }
375
376 const toastId = React.useRef<string | number>("");
377
378 function handleGoalChange(event: React.ChangeEvent<HTMLInputElement>) {
379 if (!editableEntity) return;
380
381 const {value} = event.target;
382
383 if (editableEntity && editableEntity.field === "name") {
384 if (value.length > 300) {
385 if (!toast.isActive(toastId.current)) {
386 toastId.current = toast.error("Название цели не может превышать 300 символов");
387 }
388 return;
389 }
390
391 _setEditableEntity({
392 ...editableEntity,
393 goal: {...editableEntity.goal, [editableEntity.field]: value},
394 });
395 } else if (acceptNumber.test(value)) {
396 const parsedValue = parseInt(value, 10) || 0;
397 _setEditableEntity({
398 ...editableEntity,
399 goal: {...editableEntity.goal, [editableEntity.field]: parsedValue},
400 });
401
402 if (parsedValue !== 0) {
403 sendGoalInputData({
404 goal: {
405 ...editableEntity.goal,
406 [editableEntity.field]: parseInt(value, 10) || 0
407 }
408 });
409 }
410 }
411 }
412
413 function updateGoalWay(scopeId: number, goalId: number, field: string) {
414 return (checked: boolean) => {
415 console.log('checkbox ' + field + ' is clicked to ' + checked);
416 const updatedScope = scopes.find(scope => scope.scopeId === scopeId);
417 const updatedGoal = updatedScope!.goals.find(goal => goal.goalId === goalId);
418
419 console.log('UPDATED___GOAL', updatedGoal, scopes);
420
421 let updatedGoalWithCheckboxValue = {
422 ...updatedGoal,
423 [field]: checked
424 };
425
426 const tempScopes = scopes.map(
427 scope => {
428 if (scope.scopeId !== scopeId) return scope;
429 return {
430 ...scope,
431 goals: scope.goals.map(goal =>
432 goal.goalId === goalId ? {...goal, [field]: checked} : goal
433 )
434 };
435 });
436
437 // setUpdatingGoals(true);
438
439 console.log('UPDATED__GOAL__WITH__CHECKBOX__VALUE', updatedGoalWithCheckboxValue);
440
441 // scopeApi.updateGoal(updatedGoalWithCheckboxValue)
442 // .then((response) => {
443 setScopes(tempScopes);
444
445 // setUpdatingGoals(false);
446 // });
447 }
448 }
449
450 async function finishOnboarding() {
451 try {
452 setFetching(true);
453 await dispatch(scopeActions.finishOnboarding({scopes, totalHours, totalMoney}));
454 await dispatch(scopeActions.completeStep({step: scopeConstants.STEP_ESTIMATES}));
455
456 localStorage.removeItem("onboarding-step-4-step");
457 localStorage.removeItem("onboarding-step-4-scopes");
458 localStorage.removeItem("onboarding-step-4-totalHours");
459 localStorage.removeItem("onboarding-step-4-totalMoney");
460 localStorage.removeItem("onboarding-step-4-chat-1");
461 localStorage.removeItem("onboarding-step-4-chat-2");
462 } finally {
463 setFetching(false);
464 }
465 }
466
467 const [isLoading, setLoading] = React.useState(true);
468 React.useEffect(() => {
469 async function init() {
470 await dispatch(scopeActions.startOnboarding());
471 setLoading(false);
472 }
473
474 init();
475 }, []);
476
477 if (isLoading) {
478 return <PageLoader/>;
479 }
480
481 function onTotalInput(e: React.SyntheticEvent) {
482 const value = e.target.value;
483 if (focusedInput === "hours" && value < 24) {
484 const hours = value === "" ? null : getTotalHoursFromSleepTime(value);
485 setHours(hours);
486 } else if (focusedInput === "money" && value > 0 || value === "") {
487 setMoney(value === "" ? null : Number(value));
488 }
489 }
490
491 function onTotalInputChange(e: React.SyntheticEvent) {
492 const newTotalHours = focusedInput === "hours" ? getTotalHoursFromSleepTime(e.target.value) : null;
493 focusTotalsInput("");
494 if (focusedInput === "hours" && newTotalHours && newTotalHours !== user.hoursPerYear) {
495 if (e.target.value < 1 || e.target.value >= 24) {
496 setHours(user.hoursPerYear);
497 return toast.error('Время сна не может быть меньше часа или больше 24 часов');
498 }
499
500 apiHandler.post("/scope/estimate", {
501 totalHours: newTotalHours
502 });
503 setHours(newTotalHours);
504 setMessages([
505 messages[0],
506 {...messages[1], text: e.target.value},
507 ...messages.slice(2)
508 ]);
509 dispatch({
510 type: USER_UPDATE,
511 payload: {
512 ...user,
513 hoursPerYear: newTotalHours
514 }
515 });
516 localStorage.setItem('onboarding-step-4-totalHours', newTotalHours.toString());
517 } else if (focusedInput === "money" && e.target.value !== user.moneyPerYear) {
518 const money = e.target.value;
519
520 if (money <= 0) {
521 setMoney(user.moneyPerYear);
522 return toast.error('Количество денег не может быть отрицательным или равным нулю');
523 }
524 apiHandler.post("/scope/estimate", {
525 totalMoney: money
526 });
527 setMoney(money);
528 setMessages([
529 messages[0],
530 messages[1],
531 messages[2],
532 {...messages[3], text: e.target.value},
533 messages[4]
534 ]);
535 dispatch({
536 type: USER_UPDATE,
537 payload: {
538 ...user,
539 moneyPerYear: money
540 }
541 });
542 localStorage.setItem('onboarding-step-4-totalMoney', money);
543 }
544 }
545
546 function onTotalInputKeyDown(e: React.KeyboardEvent) {
547 if (e.key === "Enter") {
548 return onTotalInputChange(e);
549 }
550
551 if (e.key !== "Backspace" && !/\d/.test(e.key)) {
552 e.preventDefault();
553 }
554 }
555
556 function isEditable(goal: ScopeTypes.Goal, field: string) {
557 return editableEntity
558 && editableEntity.goal.goalId === goal.goalId
559 && editableEntity.field === field;
560 }
561
562 const needInput = (!totalHours || !totalMoney) && (totalHours !== null && totalMoney !== null);
563
564 const goalHours = scopes.reduce(
565 (sum, s) => sum + s.goals.reduce((sum, g) => sum + g.hours, 0),
566 0,
567 );
568
569 const goalMoney = scopes.reduce(
570 (sum, s) => sum + s.goals.reduce((sum, g) => sum + g.money, 0),
571 0,
572 );
573
574 const hasExceededLimit = totalHours && totalMoney && (goalHours > totalHours || goalMoney > totalMoney);
575
576 const numberOfCompletedGoals = scopes.reduce(
577 (sum, s) => sum + s.goals.reduce((sum, g) => sum + (checkedBoth(g) ? 1 : 0), 0),
578 0,
579 );
580
581 const numberOfUnassessedGoal = scopes.reduce(
582 (sum, s) => sum + s.goals.reduce((sum, g) => sum + (checkedOne(g) ? 1 : 0), 0),
583 0,
584 );
585
586 const numberOfUnnecessaryGoal = scopes.reduce(
587 (sum, s) => sum + s.goals.reduce((sum, g) => sum + (uncheckedBoth(g) ? 1 : 0), 0),
588 0,
589 );
590
591 console.log('BEFORE____RETURN', isUpdatingGoals, scopes);
592 return (
593 <OnboardingLayout
594 step={4}
595 title="Проверка целей"
596 description="Все твои цели должны быть осуществимы. Проверь каждую цель на счастье и ресурсы. Это позволит тебе быть уверенным в своем пути"
597 image={onboardingAssignmentImage}
598 >
599 {step === 1 && (
600 <section className={cn(styles.main, styles.way)}>
601 {popup && (
602 <section className={styles.popup}>
603 <span className={styles.popup__overlay}/>
604 <div className={styles.popup__content}>
605 <img src={onboardingGoalsPopupImage} className={styles.popup__image}/>
606 <h1 className={styles.popup__title}>Перед тем как продолжить</h1>
607 <p className={styles.popup__description}>Ты проделал большую работу. Перед следующим этапом нужно
608 сделать перерыв: погуляй, займись любимым делом, отдохни. Мы рекомендуем тебе продолжить работу по
609 истечении таймера. Весь прогресс твоей работы сохранится. Ждем тебя завтра</p>
610 <p className={styles.popup__countdown}>{formatCountdown(countdown)}</p>
611 <div className={styles.popup__actions}>
612 <Button onClick={closePopup} kind="primary" RightIcon={Icons.ArrowRightIcon}>
613 Продолжить так
614 </Button>
615 </div>
616 </div>
617 </section>
618 )}
619
620 <header className={styles.main__header}>
621 <h1 className={styles.main__title}>Выбор пути</h1>
622 <p className={styles.counter}>
623 <span className={styles.counter__title}>
624 <Icons.HeartIcon className={styles.heartIcon}/>
625 {numberOfCompletedGoals}
626 </span>
627 <span className={styles.counter__description}>Полное счастье</span>
628 </p>
629 <p className={styles.counter}>
630 <span className={styles.counter__title}>
631 <Icons.ClockIcon className={styles.clockIcon}/>
632 {numberOfUnassessedGoal}
633 </span>
634 <span className={styles.counter__description}>Не оценили</span>
635 </p>
636 <p className={styles.counter}>
637 <span className={styles.counter__title}>
638 <Icons.BoxIcon className={styles.boxIcon}/>
639 {numberOfUnnecessaryGoal}
640 </span>
641 <span className={styles.counter__description}>Надо удалить</span>
642 </p>
643 </header>
644 <div
645 className={cn(
646 styles.reviewTable,
647 {
648 [styles.reviewTable_isDisabled]: !isChatFinished
649 }
650 )}
651 >
652 <div className={styles.reviewTable__row}>
653 <span
654 className={
655 styles.reviewTable__title
656 }
657 >
658 Цели
659 </span>
660 <span
661 className={cn(
662 styles.reviewTable__title,
663 styles.reviewTable__title_longTitle
664 )}
665 >
666 В чем счастье
667 </span>
668 </div>
669
670 {scopes.map(scope => (
671 <React.Fragment
672 key={scope.scopeId}
673 >
674 <div
675 className={cn(
676 styles.reviewTable__row,
677 styles.reviewTable__scope
678 )}
679 style={{
680 ["--foreground-color" as any]: scope.color,
681 ["--background-color" as any]: `${scope.color}26`,
682 }}
683 >
684 <div
685 className={cn(
686 styles.reviewTable__scopeLabel,
687 styles.reviewTable__scopeLabel_bigLabel,
688 styles.reviewTable__sep
689 )}
690 >
691 {renderIcon(scope.metaId)}
692 <span className={styles.reviewTable__scopeTitle}>{scope.name}</span>
693 </div>
694 <div
695 className={cn(
696 styles.reviewTable__scopeLabel
697 )}
698 >
699 <span className={styles.reviewTable__scopeTitle}>В пути</span>
700 <span className={styles.reviewTable__scopeTitle}>В конце</span>
701 </div>
702
703 <button
704 onClick={addGoal(scope.scopeId)}
705 className={cn(
706 styles.reviewTable__scopeLabel,
707 styles.addGoalButton
708 )}
709 >
710 <span className={styles.addGoalButton__label}>Добавить цель</span>
711 <Icons.PlusIcon/>
712 </button>
713 </div>
714
715 {scope.goals
716 .sort(
717 (a, b) => parseInt(a.createdAt) - parseInt(b.createdAt)
718 )
719 .map(
720 (goal) => (
721 <div
722 key={goal.goalId}
723 className={cn(
724 styles.reviewTable__row,
725 styles.reviewTable__goal
726 )}
727 style={{
728 ["--foreground-color" as any]: scope.color,
729 }}
730 >
731 <div
732 className={cn(
733 styles.reviewTable__goalTitle,
734 styles.reviewTable__sep
735 )}
736 onClick={() => setEditableEntity(scope.scopeId, goal, "name")}
737 >
738 {isEditable(goal, "name") ? (
739 <form
740 onSubmit={saveEditableEntity}
741 className={styles.inlineForm}
742 >
743 <input
744 type="text"
745 value={editableEntity.goal.name}
746 onChange={handleGoalChange}
747 onBlur={saveEditableEntity}
748 ref={editableInputRef}
749 className={styles.reviewTable__goalInlineField}
750 placeholder="Сформулируйте название для вашей цели"
751 />
752 </form>
753 ) : (
754 <React.Fragment>
755 <span
756 className={
757 cn(
758 styles.reviewTable__goalTitleText,
759 {
760 [styles.reviewTable__goalTitleText_isDisabled]: !goal.name.length
761 }
762 )
763 }
764 >
765 {goal.name.length ? goal.name : 'Сформулируйте название для вашей цели'}
766 </span>
767 <Icons.PencilIcon/>
768 </React.Fragment>
769 )}
770 </div>
771 <div
772 className={cn(
773 styles.reviewTable__goalFieldsGroup,
774 styles.reviewTable__goalFieldsGroup_longGroup
775 )}
776 >
777 <div
778 className={cn(
779 styles.reviewTable__goalField,
780 styles.reviewTable__goalField_checkbox
781 )}
782 >
783 <Checkbox
784 id={`${goal.goalId}-joyAtStart`}
785 checked={Boolean(goal.joyAtStart)}
786 disabled={isUpdatingGoals}
787 label={undefined}
788 onChange={updateGoalWay(scope.scopeId, goal.goalId, "joyAtStart")}
789 markIcon={Icons.HeartIcon}
790 markColor="#f27e7e"
791 />
792 </div>
793 <div
794 className={cn(
795 styles.reviewTable__goalField,
796 styles.reviewTable__goalField_checkbox
797 )}
798 >
799 <Checkbox
800 id={`${goal.goalId}-joyAtEnd`}
801 checked={Boolean(goal.joyAtEnd)}
802 disabled={isUpdatingGoals}
803 label={undefined}
804 onChange={updateGoalWay(scope.scopeId, goal.goalId, "joyAtEnd")}
805 markIcon={Icons.FlagIcon}
806 markColor="#4b83f2"
807 />
808 </div>
809 </div>
810
811 <button
812 onClick={withStopPropagation(() => confirmRemoval(scope.scopeId, goal.goalId))}
813 className={styles.removeGoalButton}
814 >
815 <span className={styles.removeGoalButton__label}>Удалить</span>
816 <Icons.CloseIcon/>
817 </button>
818
819 </div>
820 )
821 )
822 }
823 </React.Fragment>
824 ))}
825 </div>
826 {
827 isConfirmOpen && (
828 <Confirm
829 text="Вы действительно хотите удалить эту цель?"
830 onClick={remove => {
831 remove ?
832 removeGoal(scopeId, goalId) :
833 toggleConfirmDelete(false)
834 }}
835 />
836 )
837 }
838 <footer className={styles.main__footer}>
839 <ResetSteps/>
840 <Button onClick={() => finishWaysStep()} kind="primary" disabled={numberOfUnnecessaryGoal !== 0}>
841 Продолжить
842 </Button>
843 </footer>
844 </section>
845 )}
846 {step === 2 && (
847 <section className={styles.main}>
848 <header className={styles.main__header}>
849 <h1 className={styles.main__title}>Финансы и время</h1>
850
851 <p className={styles.counter}>
852 <span className={styles.counter__title}>
853 <Icons.ClockIcon className={styles.clockIcon}/>
854
855 {focusedInput === "hours" ?
856 <input
857 type="text"
858 value={totalHours === null ? "" : getSleepTimeFromTotalHours(totalHours)}
859 onKeyDown={onTotalInputKeyDown}
860 onBlur={onTotalInputChange}
861 onChange={onTotalInput}
862 /> : <>
863 {totalHours ? (totalHours - goalHours) : "?"}
864 {Boolean(totalHours) && <Icons.PencilIcon
865 onClick={() => focusTotalsInput("hours")}
866 className={styles.counter__icon}
867 />}
868 </>}
869 </span>
870 <span className={styles.counter__description}>
871 {focusedInput === "hours" ? "Часов сна" : "Часов останется"}
872 </span>
873 </p>
874
875 <p className={cn(styles.counter, styles['counter--money'])}>
876 <span className={styles.counter__title}>
877 <Icons.RubleIcon className={styles.rubleIcon}/>
878
879 {focusedInput === "money" ?
880 <input
881 type="text"
882 value={totalMoney === null ? "" : totalMoney}
883 onKeyDown={onTotalInputKeyDown}
884 onBlur={onTotalInputChange}
885 onChange={onTotalInput}
886 /> : <>
887 {totalMoney ? `${totalMoney - goalMoney} ₽` : "?"}
888 {Boolean(totalMoney) &&
889 <Icons.PencilIcon onClick={() => focusTotalsInput("money")} className={styles.counter__icon}/>}
890 </>}
891 </span>
892 <span className={styles.counter__description}>Останется денег</span>
893 </p>
894 </header>
895
896 <div
897 className={cn(
898 styles.reviewTable,
899 {
900 [styles.disabled]: needInput
901 }
902 )}
903 >
904 <div className={cn(
905 styles.reviewTable__row,
906 styles.reviewTable__row_altMobileview,
907 )}>
908 <span className={styles.reviewTable__title}>Цели</span>
909 <span className={cn(
910 styles.reviewTable__title,
911 styles.reviewTable__title_altMobileview
912 )}>
913 Нужно времени
914 </span>
915 <span className={cn(
916 styles.reviewTable__title,
917 styles.reviewTable__title_altMobileview
918 )}>
919 Нужно денег
920 </span>
921 </div>
922
923 {scopes.map(scope => (
924 <React.Fragment key={scope.scopeId}>
925 <div
926 key={scope.scopeId}
927 className={cn(
928 styles.reviewTable__row,
929 styles.reviewTable__scope,
930 {
931 [styles.inactive]: editableEntity
932 }
933 )}
934 style={{
935 ["--foreground-color" as any]: scope.color,
936 ["--background-color" as any]: `${scope.color}26`,
937 }}
938 >
939 <div
940 className={cn(
941 styles.reviewTable__scopeLabel,
942 styles.reviewTable__scopeLabel_bigLabel,
943 styles.reviewTable__sep
944 )}
945 >
946 {renderIcon(scope.metaId)}
947 <span className={styles.reviewTable__scopeTitle}>{scope.name}</span>
948 </div>
949
950 <button
951 onClick={addGoal(scope.scopeId)}
952 className={cn(
953 styles.reviewTable__scopeLabel,
954 styles.addGoalButton
955 )}
956 >
957 <span className={styles.addGoalButton__label}>Добавить цель</span>
958 <Icons.PlusIcon/>
959 </button>
960 </div>
961
962 {scope.goals
963 .sort(
964 (a, b) => parseInt(a.createdAt) - parseInt(b.createdAt)
965 )
966 .map(
967 (goal) => (
968 <div
969 key={goal.goalId}
970 className={cn(
971 styles.reviewTable__row,
972 styles.reviewTable__row_altMobileview,
973 styles.reviewTable__goal
974 )}
975 style={{
976 ["--foreground-color" as any]: scope.color,
977 }}
978 >
979 <span
980 className={cn(
981 styles.reviewTable__goalTitle,
982 styles.reviewTable__sep
983 )}
984 onClick={() => setEditableEntity(scope.scopeId, goal, "name")}
985 >
986 {isEditable(goal, "name") ? (
987 <form
988 onSubmit={saveEditableEntity}
989 className={styles.inlineForm}
990 >
991 <input
992 type="text"
993 value={editableEntity.goal.name}
994 onChange={handleGoalChange}
995 onBlur={saveEditableEntity}
996 ref={editableInputRef}
997 className={styles.reviewTable__goalInlineField}
998 />
999 </form>
1000 ) : (
1001 <React.Fragment>
1002 <span
1003 className={
1004 cn(
1005 styles.reviewTable__goalTitleText,
1006 {
1007 [styles.reviewTable__goalTitleText_isDisabled]: !goal.name.length
1008 }
1009 )
1010 }
1011 >
1012 {goal.name.length ? goal.name : 'Сформулируйте название для вашей цели'}
1013 </span>
1014 <Icons.PencilIcon/>
1015 </React.Fragment>
1016 )}
1017 </span>
1018 <div
1019 className={cn(
1020 styles.reviewTable__goalFieldsGroup,
1021 styles.reviewTable__goalFieldsGroup_altMobileview,
1022 styles.reviewTable__sep
1023 )}
1024 onClick={() => setEditableEntity(scope.scopeId, goal, "hours")}
1025 >
1026 {isEditable(goal, "hours") ? (
1027 <form
1028 onSubmit={saveEditableEntity}
1029 className={styles.reviewTable__goalField}
1030 >
1031 <input
1032 type="text"
1033 value={editableEntity.goal.hours}
1034 onChange={handleGoalChange}
1035 onBlur={saveEditableEntity}
1036 ref={editableInputRef}
1037 className={styles.reviewTable__goalInlineField}
1038 maxLength="4"
1039 />
1040 </form>
1041 ) : (
1042 <div className={styles.reviewTable__goalField}>
1043 {goal.hours} часов
1044 </div>
1045 )}
1046 </div>
1047 <div
1048 className={cn(
1049 styles.reviewTable__goalFieldsGroup,
1050 styles.reviewTable__goalFieldsGroup_altMobileview,
1051 styles.reviewTable__sep
1052 )}
1053 onClick={() => setEditableEntity(scope.scopeId, goal, "money")}
1054 >
1055 {isEditable(goal, "money") ? (
1056 <form
1057 onSubmit={saveEditableEntity}
1058 className={styles.reviewTable__goalField}
1059 >
1060 <input
1061 type="text"
1062 value={editableEntity.goal.money}
1063 onChange={handleGoalChange}
1064 onBlur={saveEditableEntity}
1065 ref={editableInputRef}
1066 className={styles.reviewTable__goalInlineField}
1067 maxLength="10"
1068 />
1069 </form>
1070 ) : (
1071 <div className={styles.reviewTable__goalField}>
1072 {goal.money} ₽
1073 </div>
1074 )}
1075 </div>
1076
1077 <button
1078 onClick={withStopPropagation(() => confirmRemoval(scope.scopeId, goal.goalId))}
1079 className={styles.removeGoalButton}
1080 >
1081 <span className={styles.removeGoalButton__label}>Удалить</span>
1082 <Icons.CloseIcon/>
1083 </button>
1084
1085 </div>
1086 )
1087 )
1088 }
1089 </React.Fragment>
1090 ))}
1091 </div>
1092 {
1093 isConfirmOpen && (
1094 <Confirm
1095 text="Вы действительно хотите удалить эту цель?"
1096 onClick={remove => {
1097 remove ?
1098 removeGoal(scopeId, goalId) :
1099 toggleConfirmDelete(false)
1100 }}
1101 />
1102 )
1103 }
1104
1105 <footer className={styles.main__footer}>
1106 <ResetSteps/>
1107 <Button
1108 onClick={finishOnboarding}
1109 kind="primary"
1110 disabled={messages.length !== 5 || Boolean(hasExceededLimit) || isFetching}
1111 RightIcon={Icons.ArrowRightIcon}
1112 >
1113 Завершить оценку
1114 </Button>
1115 </footer>
1116 </section>
1117 )}
1118 <section
1119 className={cn(
1120 styles.chat,
1121 {
1122 [styles.chat_isFinished]: (isChatFinished && step === 1) || (!needInput && step === 2),
1123 }
1124 )}
1125 >
1126 <header className={styles.chat__header}>
1127 {step === 1 && (
1128 <>
1129 <HelpPopup>
1130 <h1>Проверка целей на счастье</h1>
1131 <p>На пути достижения целей, очень важно, чтобы каждый человек был счастлив, чтобы у него всегда была
1132 мотивация двигаться по своим целям.</p>
1133 <p>Ты проделал большую работу и сформулировал цели. Теперь нужно пройтись по списку целей и
1134 прочувствовать каждую цель внутри себя, проанализировать внутренний отклик: а действительно ли тебе
1135 нужна эта цель, даст ли ее достижение внутреннее состояние счастья? И, что очень важно, когда Ты
1136 будешь выполнять свою цель, Ты будешь счастлив на всем пути ее достижения?</p>
1137
1138 <div className={styles.helpPopup__point}>Я рекомендую тебе расслабиться, включить спокойную приятную
1139 музыку, вспомнить еще раз свою визуализацию и выполнить задачу. ?
1140 </div>
1141
1142 <ul className={styles.helpPopup__list}>
1143 <li>Если Ты испытываешь счастье только в пути к цели - укажи сердечко в ячейке “В пути”.</li>
1144 <li>Если Ты испытываешь счастье только при достижении цели - укажи флажок в ячейке “В конце”.</li>
1145 <li>Если Ты испытываешь счастье и в пути, и при достижении цели - укажи сердечко и флажок в обеих
1146 ячейках.
1147 </li>
1148 </ul>
1149
1150 <div className={styles.helpPopup__attention}>
1151 <span className={styles.helpPopup__attention_icon}>
1152 <Icons.WarningIcon/>
1153 </span>
1154 <p><i>Возможно, на данном этапе Ты поймешь, что какие-то цели нужно убрать из списка. Ты можешь
1155 воспользоваться опцией удаления. Будь внимателен, если Ты удалишь цель, то вернуть ее будет
1156 нельзя.</i></p>
1157 <p><i>Я Также на данном этапе у тебя есть возможность скорректировать название цели или даже добавить
1158 новую! Приступай!</i></p>
1159 </div>
1160 </HelpPopup>
1161 </>
1162 )}
1163 {step === 2 && (
1164 <>
1165 <HelpPopup>
1166 <h1>Проверка целей на ресурсы</h1>
1167 <p>Твоя главная задача - завершить свой год с максимальным уровнем достижения целей. </p>
1168
1169 <div className={styles.helpPopup__point}>Важно, чтобы на эти цели у тебя хватило времени и средство.
1170 Поэтому следующий этап - это проверка целей на наличие ресурсов. ?
1171 </div>
1172
1173 <p>Для начала ответь в чате, сколько часов Ты спишь и какой уровень дохода ожидается. От общего
1174 доступного времени в году программа отнимет то количество времени, которое Ты спишь, и оставит
1175 полезное время для реализации целей. Затем заполни таблицу по каждой цели, сколько во времени и в
1176 деньгах стоит ее достижение. Не нужно пытаться указать точную сумму или время, укажи примерно. Это
1177 необходимо сделать для того, чтобы понять, что на реализацию целей у тебя действительно хватит времени
1178 и денежных ресурсов.</p>
1179 <p>Над таблицей с целями у тебя стоит счетчик часов и финансов,
1180 который уменьшается при каждом добавлении ресурса к цели.</p>
1181
1182
1183 <div className={styles.helpPopup__attention}>
1184 <span className={styles.helpPopup__attention_icon}>
1185 <Icons.WarningIcon/>
1186 </span>
1187 <p><i>Обязательно помни золотое правило: в твоем графике должно быть примерно 30 % свободного времени.
1188 Я называю это время - “Время для чуда”. Распределяй ресурс времени таким образом, чтобы у тебя
1189 оставался запас, тогда Ты сможешь быть гибким при реализации своих целей. Как я и говорил, я
1190 рекомендую оставлять около 30 %. Например, если у тебя остается всего 2500 полезных часов для
1191 реализации целей, то неплохо было бы не опускать эту планку ниже 700-750 часов.</i></p>
1192 <p><i>Возможно, на данном этапе Ты поймешь, что какие-то цели нужно убрать из списка. Ты можешь
1193 воспользоваться опцией удаления. Будь внимателен, если Ты удалишь цель, то вернуть ее будет нельзя.
1194 Также на данном этапе у тебя есть возможность скорректировать название цели или даже добавить
1195 новую!</i></p>
1196 </div>
1197 <p><b>Рекомендации при оценке целей на ресурсы:</b></p>
1198 <div
1199 className={cn(
1200 styles.helpPopup__attention,
1201 styles.helpPopup__attention_warn
1202 )}
1203 >
1204 <p>Если Ты едешь в путешествие, например, на 3 дня, то укажи в часах длительность своего путешествия
1205 за минусом количества часов сна за эти дни. Например, в чате Ты ответил, что спишь по 6 часов в
1206 день. Значит, за 3 дня в совокупности Ты будешь спать 18 часов. Твое путешествие будет длиться по
1207 времени 3 х 24 - 18 = 54 часа. Такой расчет необходим для того, чтобы не задвоить количество часов
1208 сна, потому что ранее на основе твоего ответа, я уже посчитал, сколько часов Ты будешь в среднем
1209 спать в году.</p>
1210 </div>
1211 <div
1212 className={cn(
1213 styles.helpPopup__attention,
1214 styles.helpPopup__attention_success
1215 )}
1216 >
1217 <p>Если твоя финансовая цель связана с постоянной занятостью на рабочем месте, то укажи, какое общее
1218 количество времени Ты проведешь на работе. Например, Ты работаешь по 8 часов в день. В году в
1219 среднем 264 рабочих дня: 8 х 264 = 2 112 часов. Если Ты приступил к работе с программой, например, 1
1220 сентября, то до конца года будет примерно 88 рабочих дней (сентябрь / октябрь / ноябрь / декабрь х
1221 22). Переводя в часы, 704 часа</p>
1222 </div>
1223 <div
1224 className={cn(
1225 styles.helpPopup__attention,
1226 styles.helpPopup__attention_info
1227 )}
1228 >
1229 <p>В дальнейшем при работе с целями Ты будешь проходить этапы актуализации. Поэтому, если Ты вдруг
1230 начнешь больше спать, или твой доход разительно вырастет, Ты сможешь внести корректировку.</p>
1231 </div>
1232 </HelpPopup>
1233 </>
1234 )}
1235 </header>
1236
1237 {step === 1 &&
1238 <Step4Chat onFinish={() => setChatFinished(true)}/>
1239 }
1240
1241 {step === 2 && (
1242 <>
1243 <Chat messages={messages}/>
1244 {needInput && (
1245 <div className={styles.actions}>
1246 <form onSubmit={send} className={styles.form}>
1247 <input
1248 type="text"
1249 value={answer}
1250 onChange={handleAnswerChange}
1251 className={styles.form__input}
1252 />
1253 <button
1254 type="submit"
1255 onClick={send}
1256 className={styles.form__button}
1257 >
1258 <Icons.ArrowRightIcon/>
1259 </button>
1260 </form>
1261 </div>
1262 )}
1263 </>
1264 )}
1265 </section>
1266 </OnboardingLayout>
1267 );
1268}
1269
1270export default React.memo(OnboardingAssignment);