refactor: data.js — единый источник данных для всех кабинетов (иерархия, менеджеры, расписание, заявки)

This commit is contained in:
wasrusgen 2026-05-28 22:40:41 +03:00
parent d90d9c27bc
commit 09eae60c25
6 changed files with 176 additions and 121 deletions

161
docs/data.js Normal file
View File

@ -0,0 +1,161 @@
/**
* @wasrusgen1 CRM Единый источник данных
* Подключается во все кабинеты: <script src="data.js"></script>
* Содержит: вертикаль управления, менеджеры, расписание, заявки, статистика
* Версия: 2026-05-28
*/
// ═══════════════════════════════════════════════════════════════════
// ВЕРТИКАЛЬ: КД → АДМИНИСТРАТОР → МЕНЕДЖЕРЫ
// Единый источник истины для всех кабинетов
// ═══════════════════════════════════════════════════════════════════
var _HIERARCHY = [
{
salonId:'lenina', salon:'Салон Ленина', color:'#3B82F6',
admin:{name:'Анна М.', short:'АМ', userId:'admin_lenina'},
orders:27, ordersPlan:30, revenue:1537000, revenuePlan:1700000,
overdue:1, overdueRisk:98500, purchases:2, newLeads:14, status:'warn',
managers:[
{id:'ak', name:'Анна К.', short:'АК', color:'#7C3AED', salon:'lenina', visits:42, deals:14, revenue:847000, conversion:33, avg:60500, rating:4.8, active:true},
{id:'ms', name:'Мария С.', short:'МС', color:'#0891B2', salon:'lenina', visits:35, deals:9, revenue:610000, conversion:26, avg:67800, rating:4.5, active:true},
]
},
{
salonId:'pobedy', salon:'Салон Победы', color:'#8B5CF6',
admin:{name:'Ирина С.', short:'ИС', userId:'admin_pobedy'},
orders:20, ordersPlan:22, revenue:1310000, revenuePlan:1500000,
overdue:1, overdueRisk:113000, purchases:1, newLeads:12, status:'warn',
managers:[
{id:'pv', name:'Пётр В.', short:'ПВ', color:'#059669', salon:'pobedy', visits:28, deals:7, revenue:490000, conversion:25, avg:70000, rating:4.3, active:true},
{id:'iv', name:'Иван В.', short:'ИВ', color:'#D97706', salon:'pobedy', visits:31, deals:8, revenue:530000, conversion:26, avg:66250, rating:4.1, active:false},
]
},
];
// ═══════════════════════════════════════════════════════════════════
// ПЛОСКИЙ СПИСОК ВСЕХ МЕНЕДЖЕРОВ (производный от _HIERARCHY)
// ═══════════════════════════════════════════════════════════════════
var _CHESS_MGRS = (function(){
var list = [];
_HIERARCHY.forEach(function(h){
h.managers.forEach(function(m){ list.push(m); });
});
return list;
})();
// ═══════════════════════════════════════════════════════════════════
// ШАХМАТКА: временные слоты и расписание
// ═══════════════════════════════════════════════════════════════════
var _CHESS_HOURS = ['10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00','19:00','20:00'];
var _CHESS_DATA = {
'ak':{'10:00':{client:'Орлова М.', type:'consult', status:'done'},
'11:00':{client:'Соколов А.', type:'follow', status:'done'},
'12:00':{client:'', type:'', status:'free'},
'13:00':{client:'', type:'', status:'free'},
'14:00':{client:'Ким Л.', type:'consult', status:'busy'},
'15:00':{client:'Ким Л.', type:'consult', status:'busy'},
'16:00':{client:'Захаров П.', type:'measure', status:'busy'},
'17:00':{client:'Новикова С.', type:'consult', status:'free'},
'18:00':{client:'', type:'', status:'free'},
'19:00':{client:'Громов И.', type:'follow', status:'free'},
'20:00':{client:'', type:'', status:'free'}},
'ms':{'10:00':{client:'Козлов Р.', type:'follow', status:'done'},
'11:00':{client:'', type:'', status:'free'},
'12:00':{client:'Лебедев С.', type:'consult', status:'done'},
'13:00':{client:'', type:'', status:'free'},
'14:00':{client:'Петрова А.', type:'tech', status:'busy'},
'15:00':{client:'Петрова А.', type:'tech', status:'busy'},
'16:00':{client:'', type:'', status:'free'},
'17:00':{client:'Морозов В.', type:'consult', status:'busy'},
'18:00':{client:'', type:'', status:'free'},
'19:00':{client:'', type:'', status:'free'},
'20:00':{client:'', type:'', status:'free'}},
'pv':{'10:00':{client:'', type:'', status:'free'},
'11:00':{client:'Сидорова Н.', type:'consult', status:'done'},
'12:00':{client:'Фёдоров К.', type:'follow', status:'noshow'},
'13:00':{client:'', type:'', status:'free'},
'14:00':{client:'', type:'', status:'free'},
'15:00':{client:'Баринова Т.', type:'consult', status:'busy'},
'16:00':{client:'Баринова Т.', type:'consult', status:'busy'},
'17:00':{client:'', type:'', status:'free'},
'18:00':{client:'Яковлев М.', type:'measure', status:'busy'},
'19:00':{client:'Яковлев М.', type:'measure', status:'busy'},
'20:00':{client:'', type:'', status:'free'}},
'iv':{'10:00':{client:'Тихонова Р.', type:'consult', status:'done'},
'11:00':{client:'Тихонова Р.', type:'consult', status:'done'},
'12:00':{client:'', type:'', status:'free'},
'13:00':{client:'Волков Е.', type:'follow', status:'done'},
'14:00':{client:'', type:'', status:'free'},
'15:00':{client:'Кузьмин О.', type:'consult', status:'busy'},
'16:00':{client:'', type:'', status:'free'},
'17:00':{client:'Рыбаков Д.', type:'tech', status:'free'},
'18:00':{client:'Рыбаков Д.', type:'tech', status:'free'},
'19:00':{client:'', type:'', status:'free'},
'20:00':{client:'', type:'', status:'free'}},
};
// ═══════════════════════════════════════════════════════════════════
// СТАТИСТИКА МЕНЕДЖЕРОВ ПО МЕСЯЦАМ
// ═══════════════════════════════════════════════════════════════════
var _MONTHLY_STATS = [
{id:'ak', name:'Анна К.', color:'#7C3AED', salon:'lenina', months:{
'Май':{visits:42, deals:14, revenue:847000, conversion:33, avg:60500, rating:4.8},
'Апр':{visits:38, deals:11, revenue:712000, conversion:29, avg:64700, rating:4.7},
'Мар':{visits:45, deals:16, revenue:920000, conversion:36, avg:57500, rating:4.9},
}},
{id:'ms', name:'Мария С.', color:'#0891B2', salon:'lenina', months:{
'Май':{visits:35, deals:9, revenue:610000, conversion:26, avg:67800, rating:4.5},
'Апр':{visits:41, deals:13, revenue:780000, conversion:32, avg:60000, rating:4.6},
'Мар':{visits:39, deals:12, revenue:695000, conversion:31, avg:57900, rating:4.4},
}},
{id:'pv', name:'Пётр В.', color:'#059669', salon:'pobedy', months:{
'Май':{visits:28, deals:7, revenue:490000, conversion:25, avg:70000, rating:4.3},
'Апр':{visits:32, deals:9, revenue:540000, conversion:28, avg:60000, rating:4.2},
'Мар':{visits:30, deals:8, revenue:520000, conversion:27, avg:65000, rating:4.4},
}},
{id:'iv', name:'Иван В.', color:'#D97706', salon:'pobedy', months:{
'Май':{visits:31, deals:8, revenue:530000, conversion:26, avg:66250, rating:4.1},
'Апр':{visits:29, deals:7, revenue:480000, conversion:24, avg:68600, rating:4.0},
'Мар':{visits:33, deals:10, revenue:610000, conversion:30, avg:61000, rating:4.3},
}},
];
// ═══════════════════════════════════════════════════════════════════
// ЗАЯВКИ МЕНЕДЖЕРОВ → АДМИНИСТРАТОР (+ эскалация → КД)
// ═══════════════════════════════════════════════════════════════════
window._MGR_REQUESTS = window._MGR_REQUESTS || [
{id:'r1', mgr:'Анна К.', mgrId:'ak', salon:'lenina', color:'#7C3AED', type:'supply', prio:'high', title:'Закончились образцы ткани', body:'Нет образцов искусственной замши — теряем клиентов. Нужно срочно дозаказать у Мебельтекстиля.', created:'сегодня 09:14', status:'new'},
{id:'r2', mgr:'Мария С.', mgrId:'ms', salon:'lenina', color:'#0891B2', type:'escalate', prio:'high', title:'Конфликт с клиентом Козлов Р.', body:'Клиент требует возврат 25 000 ₽ из-за задержки поставки. Говорит, что подаст жалобу. Прошу вмешаться.', created:'сегодня 10:30', status:'new'},
{id:'r3', mgr:'Пётр В.', mgrId:'pv', salon:'pobedy', color:'#059669', type:'visit', prio:'normal', title:'Нужна машина для выезда к клиенту', body:'Клиент Сидорова на Васильевском — хочет видеть образцы на дому. Нужен транспорт на 15:00.', created:'сегодня 11:05', status:'new'},
{id:'r4', mgr:'Анна К.', mgrId:'ak', salon:'lenina', color:'#7C3AED', type:'supply', prio:'normal', title:'Каталоги кухонь Hettich закончились', body:'Остался 1 экземпляр. Клиенты берут домой, не возвращают. Надо заказать 10 шт.', created:'вчера 17:22', status:'done'},
{id:'r5', mgr:'Мария С.', mgrId:'ms', salon:'lenina', color:'#0891B2', type:'schedule', prio:'normal', title:'Запрос на замену смены — 31 мая', body:'Прошу разрешить обменяться сменой с Петром: я работаю 31 мая вместо него, он — 1 июня вместо меня.', created:'вчера 14:10', status:'new'},
];
// ═══════════════════════════════════════════════════════════════════
// ЗАПРОСЫ НА СМЕНЫ
// ═══════════════════════════════════════════════════════════════════
window._SHIFT_REQS = window._SHIFT_REQS || [
{id:'sr1', mgr:'Мария С.', mgrId:'ms', salon:'lenina', color:'#0891B2', type:'swap', dates:'31 мая ↔ 1 июня', with:'Пётр В.', status:'pending'},
{id:'sr2', mgr:'Пётр В.', mgrId:'pv', salon:'pobedy', color:'#059669', type:'off', dates:'2 июня (отгул)', with:null, status:'pending'},
];
// ═══════════════════════════════════════════════════════════════════
// ХЕЛПЕРЫ — утилиты доступные всем кабинетам
// ═══════════════════════════════════════════════════════════════════
/** Найти запись _HIERARCHY по salonId */
function _getSalon(salonId){
return _HIERARCHY.find(function(h){ return h.salonId===salonId; }) || null;
}
/** Найти менеджера по id из плоского списка */
function _getMgr(mgrId){
return _CHESS_MGRS.find(function(m){ return m.id===mgrId; }) || null;
}
/** Получить KPI менеджера за период (из _MONTHLY_STATS) */
function _getMgrStats(mgrId, period){
var stat = _MONTHLY_STATS.find(function(s){ return s.id===mgrId; });
return stat ? (stat.months[period||'Май'] || {}) : {};
}

View File

@ -65,110 +65,20 @@ body[data-theme="dark"]{--accent:#4338CA;--accent2:#6366F1;--bg:#111827;--card:#
<div class="bottom-nav" id="nav"></div>
</div>
<script src="data.js"></script>
<script>
// ── ДАННЫЕ: ЗАЯВКИ МЕНЕДЖЕРОВ ─────────────────────────────────────────────────
window._MGR_REQUESTS = window._MGR_REQUESTS || [
{id:'r1', mgr:'Анна К.', mgrId:'ak', salon:'lenina', color:'#7C3AED', type:'supply', prio:'high', title:'Закончились образцы ткани', body:'Нет образцов искусственной замши — теряем клиентов. Нужно срочно дозаказать у Мебельтекстиля.', created:'сегодня 09:14', status:'new'},
{id:'r2', mgr:'Мария С.', mgrId:'ms', salon:'lenina', color:'#0891B2', type:'escalate', prio:'high', title:'Конфликт с клиентом Козлов Р.', body:'Клиент требует возврат 25 000 ₽ из-за задержки поставки. Говорит, что подаст жалобу. Прошу вмешаться.', created:'сегодня 10:30', status:'new'},
{id:'r3', mgr:'Пётр В.', mgrId:'pv', salon:'pobedy', color:'#059669', type:'visit', prio:'normal', title:'Нужна машина для выезда к клиенту', body:'Клиент Сидорова на Васильевском — хочет видеть образцы на дому. Нужен транспорт на 15:00.', created:'сегодня 11:05', status:'new'},
{id:'r4', mgr:'Анна К.', mgrId:'ak', salon:'lenina', color:'#7C3AED', type:'supply', prio:'normal', title:'Каталоги кухонь Hettich закончились', body:'Остался 1 экземпляр. Клиенты берут домой, не возвращают. Надо заказать 10 шт.', created:'вчера 17:22', status:'done'},
{id:'r5', mgr:'Мария С.', mgrId:'ms', salon:'lenina', color:'#0891B2', type:'schedule', prio:'normal', title:'Запрос на замену смены — 31 мая', body:'Прошу разрешить обменяться сменой с Петром: я работаю 31 мая вместо него, он — 1 июня вместо меня.', created:'вчера 14:10', status:'new'},
];
// ── КАБИНЕТ: АДМИНИСТРАТОР · САЛОН ЛЕНИНА ────────────────────────────────────
// Данные вертикали (_HIERARCHY, _CHESS_MGRS, _CHESS_DATA, _CHESS_HOURS,
// _MONTHLY_STATS, _MGR_REQUESTS, _SHIFT_REQS) — загружены из data.js
window._staffSubTab = window._staffSubTab || 'chess';
// ── ДАННЫЕ: ШАХМАТКА (расписание менеджеров × слоты) ─────────────────────────
var _CHESS_HOURS = ['10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00','19:00','20:00'];
// Идентичность этого кабинета: Администратор Анна М. · Салон Ленина
// ── ИДЕНТИЧНОСТЬ КАБИНЕТА (роль-специфично, остаётся здесь) ──────────────────
var _ADMIN_IDENTITY = {name:'Анна М.', short:'АМ', salon:'Салон Ленина', salonId:'lenina'};
// Менеджеры ЭТОГО салона (Ленина). Пётр В. и Иван В. — Салон Победы, в этом кабинете не отображаются.
var _CHESS_MGRS = [
{id:'ak', name:'Анна К.', short:'АК', color:'#7C3AED', salon:'lenina'},
{id:'ms', name:'Мария С.', short:'МС', color:'#0891B2', salon:'lenina'},
{id:'pv', name:'Пётр В.', short:'ПВ', color:'#059669', salon:'pobedy'},
{id:'iv', name:'Иван В.', short:'ИВ', color:'#D97706', salon:'pobedy'},
];
// Фильтр — только менеджеры своего салона
// Фильтр из data.js: только менеджеры своего салона
var _MY_MGRS = _CHESS_MGRS.filter(function(m){ return m.salon===_ADMIN_IDENTITY.salonId; });
// ячейка: {client, type, status} status: free|busy|done|noshow
// type: consult|measure|follow|tech
var _CHESS_DATA = {
'ak':{'10:00':{client:'Орлова М.', type:'consult', status:'done'},
'11:00':{client:'Соколов А.', type:'follow', status:'done'},
'12:00':{client:'', type:'', status:'free'},
'13:00':{client:'', type:'', status:'free'},
'14:00':{client:'Ким Л.', type:'consult', status:'busy'},
'15:00':{client:'Ким Л.', type:'consult', status:'busy'},
'16:00':{client:'Захаров П.', type:'measure', status:'busy'},
'17:00':{client:'Новикова С.', type:'consult', status:'free'},
'18:00':{client:'', type:'', status:'free'},
'19:00':{client:'Громов И.', type:'follow', status:'free'},
'20:00':{client:'', type:'', status:'free'}},
'ms':{'10:00':{client:'Козлов Р.', type:'follow', status:'done'},
'11:00':{client:'', type:'', status:'free'},
'12:00':{client:'Лебедев С.', type:'consult', status:'done'},
'13:00':{client:'', type:'', status:'free'},
'14:00':{client:'Петрова А.', type:'tech', status:'busy'},
'15:00':{client:'Петрова А.', type:'tech', status:'busy'},
'16:00':{client:'', type:'', status:'free'},
'17:00':{client:'Морозов В.', type:'consult', status:'busy'},
'18:00':{client:'', type:'', status:'free'},
'19:00':{client:'', type:'', status:'free'},
'20:00':{client:'', type:'', status:'free'}},
'pv':{'10:00':{client:'', type:'', status:'free'},
'11:00':{client:'Сидорова Н.', type:'consult', status:'done'},
'12:00':{client:'Фёдоров К.', type:'follow', status:'noshow'},
'13:00':{client:'', type:'', status:'free'},
'14:00':{client:'', type:'', status:'free'},
'15:00':{client:'Баринова Т.', type:'consult', status:'busy'},
'16:00':{client:'Баринова Т.', type:'consult', status:'busy'},
'17:00':{client:'', type:'', status:'free'},
'18:00':{client:'Яковлев М.', type:'measure', status:'busy'},
'19:00':{client:'Яковлев М.', type:'measure', status:'busy'},
'20:00':{client:'', type:'', status:'free'}},
'iv':{'10:00':{client:'Тихонова Р.', type:'consult', status:'done'},
'11:00':{client:'Тихонова Р.', type:'consult', status:'done'},
'12:00':{client:'', type:'', status:'free'},
'13:00':{client:'Волков Е.', type:'follow', status:'done'},
'14:00':{client:'', type:'', status:'free'},
'15:00':{client:'Кузьмин О.', type:'consult', status:'busy'},
'16:00':{client:'', type:'', status:'free'},
'17:00':{client:'Рыбаков Д.', type:'tech', status:'free'},
'18:00':{client:'Рыбаков Д.', type:'tech', status:'free'},
'19:00':{client:'', type:'', status:'free'},
'20:00':{client:'', type:'', status:'free'}},
};
// ── ДАННЫЕ: ИТОГИ ПО МЕНЕДЖЕРАМ (по месяцам) ──────────────────────────────
var _MONTHLY_STATS = [
{id:'ak', name:'Анна К.', color:'#7C3AED', months:{
'Май':{visits:42, deals:14, revenue:847000, conversion:33, avg:60500, rating:4.8},
'Апр':{visits:38, deals:11, revenue:712000, conversion:29, avg:64700, rating:4.7},
'Мар':{visits:45, deals:16, revenue:920000, conversion:36, avg:57500, rating:4.9},
}},
{id:'ms', name:'Мария С.', color:'#0891B2', months:{
'Май':{visits:35, deals:9, revenue:610000, conversion:26, avg:67800, rating:4.5},
'Апр':{visits:41, deals:13, revenue:780000, conversion:32, avg:60000, rating:4.6},
'Мар':{visits:39, deals:12, revenue:695000, conversion:31, avg:57900, rating:4.4},
}},
{id:'pv', name:'Пётр В.', color:'#059669', months:{
'Май':{visits:28, deals:7, revenue:490000, conversion:25, avg:70000, rating:4.3},
'Апр':{visits:32, deals:9, revenue:540000, conversion:28, avg:60000, rating:4.2},
'Мар':{visits:30, deals:8, revenue:520000, conversion:27, avg:65000, rating:4.4},
}},
{id:'iv', name:'Иван В.', color:'#D97706', months:{
'Май':{visits:31, deals:8, revenue:530000, conversion:26, avg:66250, rating:4.1},
'Апр':{visits:29, deals:7, revenue:480000, conversion:24, avg:68600, rating:4.0},
'Мар':{visits:33, deals:10, revenue:610000, conversion:30, avg:61000, rating:4.3},
}},
];
window._monthlyPeriod = window._monthlyPeriod || 'Май';
// ── СОГЛАСОВАНИЕ СМЕН: запросы ─────────────────────────────────────────────
window._SHIFT_REQS = window._SHIFT_REQS || [
{id:'sr1', mgr:'Мария С.', color:'#0891B2', type:'swap', dates:'31 мая ↔ 1 июня', with:'Пётр В.', status:'pending'},
{id:'sr2', mgr:'Пётр В.', color:'#059669', type:'off', dates:'2 июня (отгул)', with:null, status:'pending'},
];
// ── CONFIG (регулируется директором) ─────────────────────────────────────────
window._CONFIG = window._CONFIG || {
supplyApproval: false, // true = заявка уходит на согласование директору

View File

@ -294,6 +294,7 @@ body[data-theme="dark"] .stat-table td{border-color:#374151}
</div>
</div>
<script src="data.js"></script>
<script>
const SCREENS = {
home: 'Главная',

View File

@ -97,7 +97,11 @@ body[data-theme="dark"]{--accent:#4338CA;--accent2:#6366F1;--bg:#111827;--card:#
<div class="bottom-nav" id="nav"></div>
</div>
<script src="data.js"></script>
<script>
// ── КАБИНЕТ: КОММЕРЧЕСКИЙ ДИРЕКТОР ───────────────────────────────────────────
// Данные вертикали (_HIERARCHY, _CHESS_MGRS, _MONTHLY_STATS, _MGR_REQUESTS) — из data.js
window._sc = window._sc || 'home';
window._ordFilter = window._ordFilter || 'all';
window._ordSalon = window._ordSalon || 'all';
@ -460,30 +464,7 @@ function _sOrders(){
+'</div>';
}
// ─── ВЕРТИКАЛЬ: КД → АДМИНИСТРАТОР → МЕНЕДЖЕРЫ ─────────────────────
// Единый источник истины — отражает обе стороны (КД и Администратор)
var _HIERARCHY=[
{
salonId:'lenina', salon:'Салон Ленина', color:'#3B82F6',
admin:{name:'Анна М.', short:'АМ'},
orders:27, ordersPlan:30, revenue:1537000, revenuePlan:1700000,
overdue:1, overdueRisk:98500, purchases:2, newLeads:14, status:'warn',
managers:[
{id:'ak', name:'Анна К.', short:'АК', color:'#7C3AED', visits:42, deals:14, revenue:847000, conversion:33, avg:60500, rating:4.8, active:true},
{id:'ms', name:'Мария С.', short:'МС', color:'#0891B2', visits:35, deals:9, revenue:610000, conversion:26, avg:67800, rating:4.5, active:true},
]
},
{
salonId:'pobedy', salon:'Салон Победы', color:'#8B5CF6',
admin:{name:'Ирина С.', short:'ИС'},
orders:20, ordersPlan:22, revenue:1310000, revenuePlan:1500000,
overdue:1, overdueRisk:113000, purchases:1, newLeads:12, status:'warn',
managers:[
{id:'pv', name:'Пётр В.', short:'ПВ', color:'#059669', visits:28, deals:7, revenue:490000, conversion:25, avg:70000, rating:4.3, active:true},
{id:'iv', name:'Иван В.', short:'ИВ', color:'#D97706', visits:31, deals:8, revenue:530000, conversion:26, avg:66250, rating:4.1, active:false},
]
},
];
// _HIERARCHY, _CHESS_MGRS, _MONTHLY_STATS, _MGR_REQUESTS — из data.js
window._mgrExpSalon = window._mgrExpSalon || null;
// ─── МЕНЕДЖЕРЫ ──────────────────────────────────────────────────────

View File

@ -236,6 +236,7 @@ body[data-theme="dark"]{--accent:#4338CA;--accent2:#6366F1;--bg:#111827;--card:#
<div id="nav"></div>
</div>
<script src="data.js"></script>
<script>
// ── State ────────────────────────────────────────────────────────────────────
window._managerOrders = window._managerOrders || [

View File

@ -182,6 +182,7 @@ body[data-theme="dark"] .photo-slot{background:#374151;border-color:#4B5563}
</div>
</div>
<script src="data.js"></script>
<script>
const SCREENS = {
home: 'Главная',