/
sc_coder
/
MentalCheckList
Обзор
Документация
Войти
/
sc_coder
/
MentalCheckList
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
logic.js
254 строки
9 KB
ScCoder
Initial commit: Ментальный Чек-Лист PWA
14 июл 2026, 08:35
14 июл 2026, 08:35
482499e
Код
Авторство
О чём код?
/** * Чистая бизнес-логика приложения "Ментальный Чек-Лист". * Не содержит обращений к DOM или localStorage — полностью тестируема. */ (function (root, factory) { if (typeof module !== 'undefined' && module.exports) { module.exports = factory(); } else { root.MCLLogic = factory(); } })(typeof self !== 'undefined' ? self : this, function () { 'use strict'; var DEFAULT_THRESHOLD = 30; var MIN_THRESHOLD = 10; var MAX_THRESHOLD = 100; // Каждый вопрос пула хранит две формулировки: "негативную" (описание симптома) // и "позитивную" (описание нормы). Во время опроса случайно показывается одна // из них — это защищает от автоматических ответов "по шаблону" (см. pickVariant). var DEFAULT_QUESTIONS = [ { id: 'd1', negativeText: 'Сон стал короче обычного, но бодрости не убавилось?', positiveText: 'Сон стабильный, как обычно, и высыпаешься нормально?' }, { id: 'd2', negativeText: 'Мысли скачут быстрее, чем обычно?', positiveText: 'Мысли текут в привычном, спокойном темпе?' }, { id: 'd3', negativeText: 'Появилась повышенная раздражительность?', positiveText: 'Настроение ровное, раздражительности нет?' }, { id: 'd4', negativeText: 'Ощущение прилива энергии и «гиперактивности»?', positiveText: 'Уровень энергии обычный, без прилива активности?' }, { id: 'd5', negativeText: 'Тянет на рискованные решения или траты?', positiveText: 'Решения и траты остаются осторожными, как обычно?' }, { id: 'd6', negativeText: 'Речь стала быстрее и громче обычного?', positiveText: 'Темп и громкость речи как обычно?' }, { id: 'd7', negativeText: 'Трудно сосредоточиться на одной задаче?', positiveText: 'Легко удаётся сосредоточиться на одной задаче?' }, { id: 'd8', negativeText: 'Повышенная общительность и болтливость?', positiveText: 'Общительность на привычном уровне, без болтливости?' }, { id: 'd9', negativeText: 'Чувство собственной значимости или грандиозности?', positiveText: 'Самооценка обычная, без ощущения особой значимости?' }, { id: 'd10', negativeText: 'Снизилась критичность к своим действиям?', positiveText: 'Критичность к своим действиям в норме?' } ]; function clampThreshold(value) { var num = Number(value); if (isNaN(num)) return DEFAULT_THRESHOLD; if (num < MIN_THRESHOLD) return MIN_THRESHOLD; if (num > MAX_THRESHOLD) return MAX_THRESHOLD; return Math.round(num); } function calculatePercentage(checkedCount, totalCount) { if (!totalCount || totalCount <= 0) return 0; var pct = (checkedCount / totalCount) * 100; return Math.round(pct * 10) / 10; } function getZone(percentage, threshold) { var t = clampThreshold(threshold); return percentage > t ? 'yellow' : 'green'; } function generateId() { return 'q_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); } function addQuestion(list, negativeText, positiveText) { var trimmedNegative = (negativeText || '').trim(); if (!trimmedNegative) { throw new Error('Негативная формулировка вопроса не может быть пустой'); } var trimmedPositive = (positiveText || '').trim(); // Позитивная формулировка необязательна: без неё вопрос всегда показывается // как есть и не участвует в защите от автоматических ответов. var newQuestion = { id: generateId(), negativeText: trimmedNegative, positiveText: trimmedPositive || null, custom: true }; return list.concat([newQuestion]); } function removeQuestion(list, id) { return list.filter(function (q) { return q.id !== id; }); } function isQuestionActive(list, id) { return list.some(function (q) { return q.id === id; }); } function toggleDefaultQuestion(list, poolQuestion, shouldBeActive) { var active = isQuestionActive(list, poolQuestion.id); if (shouldBeActive && !active) { return list.concat([{ id: poolQuestion.id, negativeText: poolQuestion.negativeText, positiveText: poolQuestion.positiveText, custom: false }]); } if (!shouldBeActive && active) { return removeQuestion(list, poolQuestion.id); } return list; } function pickVariant(question, randomValue) { if (!question.positiveText) return 'negative'; return randomValue < 0.5 ? 'positive' : 'negative'; } function getQuestionPromptText(question, variant) { return variant === 'positive' ? question.positiveText : question.negativeText; } function isSymptomPresent(variant, answeredYes) { return variant === 'positive' ? !answeredYes : !!answeredYes; } function pad2(n) { return n < 10 ? '0' + n : '' + n; } function formatDateISO(date) { date = date || new Date(); return date.getFullYear() + '-' + pad2(date.getMonth() + 1) + '-' + pad2(date.getDate()); } var MONTH_NAMES = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' ]; function formatDateDisplay(date) { date = date || new Date(); return date.getDate() + ' ' + MONTH_NAMES[date.getMonth()] + ' ' + date.getFullYear(); } function buildHistoryEntry(dateISO, questions, answers, threshold) { var total = questions.length; var checked = questions.reduce(function (count, q) { return count + (answers[q.id] ? 1 : 0); }, 0); var percentage = calculatePercentage(checked, total); var zone = getZone(percentage, threshold); return { date: dateISO, totalCount: total, checkedCount: checked, percentage: percentage, threshold: clampThreshold(threshold), zone: zone, answers: answers }; } function getMonthMatrix(year, month) { var firstOfMonth = new Date(year, month, 1); var startOffset = (firstOfMonth.getDay() + 6) % 7; // неделя начинается с понедельника var daysInMonth = new Date(year, month + 1, 0).getDate(); var cells = []; for (var i = 0; i < startOffset; i++) { cells.push(null); } for (var d = 1; d <= daysInMonth; d++) { cells.push(new Date(year, month, d)); } while (cells.length % 7 !== 0) { cells.push(null); } var weeks = []; for (var w = 0; w < cells.length; w += 7) { weeks.push(cells.slice(w, w + 7)); } return weeks; } function isValidTimeString(time) { return typeof time === 'string' && /^([01]\d|2[0-3]):([0-5]\d)$/.test(time); } function shouldNotifyNow(now, reminderTime, lastNotifiedDate, todayISO, hasCompletedToday) { if (!isValidTimeString(reminderTime)) return false; if (hasCompletedToday) return false; if (lastNotifiedDate === todayISO) return false; var parts = reminderTime.split(':'); var reminderMinutes = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); var nowMinutes = now.getHours() * 60 + now.getMinutes(); return nowMinutes >= reminderMinutes; } return { DEFAULT_THRESHOLD: DEFAULT_THRESHOLD, MIN_THRESHOLD: MIN_THRESHOLD, MAX_THRESHOLD: MAX_THRESHOLD, DEFAULT_QUESTIONS: DEFAULT_QUESTIONS, clampThreshold: clampThreshold, calculatePercentage: calculatePercentage, getZone: getZone, generateId: generateId, addQuestion: addQuestion, removeQuestion: removeQuestion, isQuestionActive: isQuestionActive, toggleDefaultQuestion: toggleDefaultQuestion, formatDateISO: formatDateISO, formatDateDisplay: formatDateDisplay, buildHistoryEntry: buildHistoryEntry, getMonthMatrix: getMonthMatrix, isValidTimeString: isValidTimeString, shouldNotifyNow: shouldNotifyNow, pickVariant: pickVariant, getQuestionPromptText: getQuestionPromptText, isSymptomPresent: isSymptomPresent }; });