feat: category block + self-service questionnaire for assembler and measurer

This commit is contained in:
wasrusgen 2026-05-28 13:57:28 +03:00
parent 73119a8714
commit 9403437b28
2 changed files with 260 additions and 9 deletions

View File

@ -310,6 +310,7 @@ const SCREENS = {
reclamation: 'Акт рекламации', reclamation: 'Акт рекламации',
photo_report: 'Фото-отчёт', photo_report: 'Фото-отчёт',
profile: 'Профиль', profile: 'Профиль',
my_questionnaire: 'Моя анкета',
availability: 'Доступность', availability: 'Доступность',
notifications: 'Уведомления', notifications: 'Уведомления',
rate_team: 'Оценить команду', rate_team: 'Оценить команду',
@ -438,6 +439,7 @@ function render(id) {
case 'reclamation': return screenReclamation(); case 'reclamation': return screenReclamation();
case 'photo_report': return screenPhotoReport(); case 'photo_report': return screenPhotoReport();
case 'profile': return screenProfile(); case 'profile': return screenProfile();
case 'my_questionnaire': return screenMyQuestionnaire();
case 'availability': return screenAvailability(); case 'availability': return screenAvailability();
case 'notifications': return screenNotifications(); case 'notifications': return screenNotifications();
case 'rate_team': return screenRateTeam(); case 'rate_team': return screenRateTeam();
@ -1547,6 +1549,35 @@ function screenProfile() {
</div> </div>
</div> </div>
<!-- Категория и загрузка -->
<div class="card" style="padding:14px 16px;margin-bottom:12px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:8px">
<span style="background:#4338CA;color:#fff;font-size:11px;font-weight:800;padding:3px 10px;border-radius:20px;letter-spacing:.03em">Кат.А</span>
<span style="font-size:12px;color:var(--muted)">Присвоена 12.01.2026</span>
</div>
<button onclick="go('my_questionnaire')" style="font-size:12px;font-weight:700;color:var(--accent);background:rgba(0,62,126,.07);border:none;border-radius:8px;padding:5px 10px;cursor:pointer">Анкета →</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:10px">
<div style="flex:1;background:rgba(67,56,202,.07);border-radius:8px;padding:8px 10px;text-align:center">
<div style="font-size:18px;font-weight:800;color:#4338CA">85%</div>
<div style="font-size:10px;color:var(--muted);font-weight:600;margin-top:1px">загрузка</div>
</div>
<div style="flex:1;background:rgba(0,62,126,.07);border-radius:8px;padding:8px 10px;text-align:center">
<div style="font-size:18px;font-weight:800;color:var(--accent)">2 500 ₽</div>
<div style="font-size:10px;color:var(--muted);font-weight:600;margin-top:1px">ставка</div>
</div>
<div style="flex:1;background:rgba(16,185,129,.07);border-radius:8px;padding:8px 10px;text-align:center">
<div style="font-size:18px;font-weight:800;color:#059669">3/д</div>
<div style="font-size:10px;color:var(--muted);font-weight:600;margin-top:1px">макс. заказов</div>
</div>
</div>
<div style="height:6px;border-radius:99px;background:rgba(0,0,0,.07);overflow:hidden">
<div style="height:100%;width:85%;border-radius:99px;background:linear-gradient(90deg,#4338CA,#6366F1)"></div>
</div>
<div style="font-size:10px;color:var(--muted);margin-top:4px">Кат.А ≥ Кат.Б · Средняя Кат.Б: 54% ✓</div>
</div>
<div class="earnings-card"> <div class="earnings-card">
<div style="display:flex;justify-content:space-between"> <div style="display:flex;justify-content:space-between">
<div> <div>
@ -1841,6 +1872,102 @@ function screenRateTeam() {
${nav('home')}</div>`; ${nav('home')}</div>`;
} }
/* ─── MY QUESTIONNAIRE (Assembler) ─── */
function screenMyQuestionnaire() {
if (!window._qSaved) window._qSaved = false;
if (!window._qCar) window._qCar = 'yes';
if (!window._qTools) window._qTools = {drill:true,jigsaw:true,miter:false,plunge:false,hammer:false,grinder:false,router:false,vacuum:true};
if (!window._qFurn) window._qFurn = {corpus:true,upholstered:false,kitchen:true,office:true,premium:false};
if (!window._qZones) window._qZones = {center:true,north:true,south:false,east:false,west:false};
function chk(key, store, label, note) {
var on = window[store][key];
return '<label style="display:flex;align-items:flex-start;gap:10px;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.05);cursor:pointer">'
+'<div onclick="(function(){window[\''+store+'\'][\''+key+'\']=!window[\''+store+'\'][\''+key+'\'];'
+'document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="width:20px;height:20px;border-radius:5px;border:2px solid '+(on?'var(--accent)':'#D1D5DB')+';background:'+(on?'var(--accent)':'transparent')+';flex-shrink:0;margin-top:1px;display:flex;align-items:center;justify-content:center">'
+(on?'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>':'')
+'</div>'
+'<div><div style="font-size:13px;color:var(--ink);font-weight:500">'+label+'</div>'
+(note?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+note+'</div>':'')
+'</div></label>';
}
var saved = window._qSaved;
return '<div class="page">'
+'<div class="page-header">'
+'<button class="back-btn" onclick="go(\'profile\')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Моя анкета</h2>'
+'</div>'
+'<div style="padding:0 16px 20px">'
// Info banner
+'<div style="background:rgba(0,62,126,.06);border:1px solid rgba(0,62,126,.15);border-radius:10px;padding:10px 14px;margin:12px 0;display:flex;gap:8px;align-items:flex-start">'
+'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" style="flex-shrink:0;margin-top:1px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>'
+'<div style="font-size:12px;color:var(--ink);line-height:1.5">Данные видны директору. При изменениях (новый инструмент, зона, тип мебели) — директор получит уведомление.</div>'
+'</div>'
// Instruments
+'<div class="card" style="padding:14px 16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Инструменты</div>'
+'<div style="font-size:11px;color:var(--muted);margin-bottom:10px">Отметьте что есть в наличии</div>'
+ chk('drill', '_qTools', 'Дрель / шуруповёрт', null)
+ chk('jigsaw', '_qTools', 'Лобзик', null)
+ chk('miter', '_qTools', 'Торцовочная пила', 'для точных угловых срезов')
+ chk('plunge', '_qTools', 'Погружная пила', 'для раскроя панелей и столешниц')
+ chk('hammer', '_qTools', 'Перфоратор', null)
+ chk('grinder', '_qTools', 'Болгарка', null)
+ chk('router', '_qTools', 'Фрезер', 'для кромки и пазов')
+ chk('vacuum', '_qTools', 'Строительный пылесос', null)
+'</div>'
// Furniture types
+'<div class="card" style="padding:14px 16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Типы мебели</div>'
+ chk('corpus', '_qFurn', 'Корпусная (шкафы, стеллажи, комоды)', null)
+ chk('upholstered', '_qFurn', 'Мягкая (диваны, кресла)', null)
+ chk('kitchen', '_qFurn', 'Кухонные гарнитуры', null)
+ chk('office', '_qFurn', 'Офисная мебель', null)
+ chk('premium', '_qFurn', 'Премиальная / итальянская', 'влияет на категорию Кат.А')
+'</div>'
// Transport
+'<div class="card" style="padding:14px 16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Личный автомобиль</div>'
+'<div style="display:flex;gap:8px">'
+'<button onclick="(function(){window._qCar=\'yes\';document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="flex:1;padding:12px;border-radius:10px;border:2px solid '+(window._qCar==='yes'?'var(--accent)':'rgba(0,0,0,.1)')+';background:'+(window._qCar==='yes'?'rgba(0,62,126,.07)':'transparent')+';cursor:pointer;font-size:13px;font-weight:700;color:'+(window._qCar==='yes'?'var(--accent)':'var(--muted)')+'">'
+'🚗 Есть авто</button>'
+'<button onclick="(function(){window._qCar=\'no\';document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="flex:1;padding:12px;border-radius:10px;border:2px solid '+(window._qCar==='no'?'var(--accent)':'rgba(0,0,0,.1)')+';background:'+(window._qCar==='no'?'rgba(0,62,126,.07)':'transparent')+';cursor:pointer;font-size:13px;font-weight:700;color:'+(window._qCar==='no'?'var(--accent)':'var(--muted)')+'">'
+'🚌 Без авто</button>'
+'</div>'
+'</div>'
// Work zones
+'<div class="card" style="padding:14px 16px;margin-bottom:16px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Зона работы</div>'
+'<div style="font-size:11px;color:var(--muted);margin-bottom:10px">Комфортные районы</div>'
+ chk('center', '_qZones', 'Центр города', null)
+ chk('north', '_qZones', 'Север', null)
+ chk('south', '_qZones', 'Юг', null)
+ chk('east', '_qZones', 'Восток', null)
+ chk('west', '_qZones', 'Запад / ЗАД', null)
+'</div>'
// Save button
+(saved
? '<div style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:12px;padding:14px;text-align:center;font-size:14px;font-weight:700;color:#065f46">✓ Сохранено · Директор уведомлён</div>'
: '<button onclick="(function(){window._qSaved=true;document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="width:100%;padding:16px;background:var(--accent);color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer">Сохранить изменения</button>'
)
+'<div style="text-align:center;font-size:11px;color:var(--muted);margin-top:8px">Последнее обновление: 22.05.2026 · 10:14</div>'
+'</div>'
+ nav('profile')
+'</div>';
}
/* ─── AVAILABILITY ─── */ /* ─── AVAILABILITY ─── */
function screenAvailability() { function screenAvailability() {
const requests = [ const requests = [

View File

@ -195,6 +195,7 @@ const SCREENS = {
report_done: 'Успех отправки', report_done: 'Успех отправки',
history: 'История замеров', history: 'История замеров',
profile: 'Профиль замерщика', profile: 'Профиль замерщика',
my_questionnaire: 'Моя анкета',
schedule: 'Расписание', schedule: 'Расписание',
accept_job: 'Входящая заявка', accept_job: 'Входящая заявка',
}; };
@ -269,6 +270,7 @@ function render(id) {
case 'report_done': return screenReportDone(); case 'report_done': return screenReportDone();
case 'history': return screenHistory(); case 'history': return screenHistory();
case 'profile': return screenProfile(); case 'profile': return screenProfile();
case 'my_questionnaire': return screenMyQuestionnaire();
case 'accept_job': return screenAcceptJob(); case 'accept_job': return screenAcceptJob();
case 'schedule': return screenSchedule(); case 'schedule': return screenSchedule();
default: return '<div style="padding:40px;text-align:center;color:var(--muted)">Экран в разработке</div>'; default: return '<div style="padding:40px;text-align:center;color:var(--muted)">Экран в разработке</div>';
@ -919,6 +921,34 @@ function screenProfile() {
</div> </div>
</div> </div>
<!-- Категория и загрузка -->
<div class="card" style="padding:14px 16px;margin-bottom:12px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:8px">
<span style="background:#4338CA;color:#fff;font-size:11px;font-weight:800;padding:3px 10px;border-radius:20px;letter-spacing:.03em">Кат.А</span>
<span style="font-size:12px;color:var(--muted)">Присвоена 15.01.2026</span>
</div>
<button onclick="go('my_questionnaire')" style="font-size:12px;font-weight:700;color:var(--accent);background:rgba(0,62,126,.07);border:none;border-radius:8px;padding:5px 10px;cursor:pointer">Анкета →</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:10px">
<div style="flex:1;background:rgba(67,56,202,.07);border-radius:8px;padding:8px 10px;text-align:center">
<div style="font-size:18px;font-weight:800;color:#4338CA">79%</div>
<div style="font-size:10px;color:var(--muted);font-weight:600;margin-top:1px">загрузка</div>
</div>
<div style="flex:1;background:rgba(0,62,126,.07);border-radius:8px;padding:8px 10px;text-align:center">
<div style="font-size:18px;font-weight:800;color:var(--accent)">600 ₽</div>
<div style="font-size:10px;color:var(--muted);font-weight:600;margin-top:1px">ставка/замер</div>
</div>
<div style="flex:1;background:rgba(16,185,129,.07);border-radius:8px;padding:8px 10px;text-align:center">
<div style="font-size:18px;font-weight:800;color:#059669">4/д</div>
<div style="font-size:10px;color:var(--muted);font-weight:600;margin-top:1px">макс. замеров</div>
</div>
</div>
<div style="height:6px;border-radius:99px;background:rgba(0,0,0,.07);overflow:hidden">
<div style="height:100%;width:79%;border-radius:99px;background:linear-gradient(90deg,#4338CA,#6366F1)"></div>
</div>
</div>
<div class="earnings-card"> <div class="earnings-card">
<div style="display:flex;justify-content:space-between"> <div style="display:flex;justify-content:space-between">
<div> <div>
@ -958,6 +988,100 @@ function screenProfile() {
${nav('profile')}</div>`; ${nav('profile')}</div>`;
} }
/* ─── MY QUESTIONNAIRE (Measurer) ─── */
function screenMyQuestionnaire() {
if (!window._qSaved) window._qSaved = false;
if (!window._qCar) window._qCar = 'yes';
if (!window._qEquip) window._qEquip = {laser:true,tape:true,level:true,digital:false,theodolite:false,photo360:false};
if (!window._qTypes) window._qTypes = {flat:true,house:false,office:true,commercial:false,newbuild:true};
if (!window._qZones) window._qZones = {center:true,north:true,south:false,east:false,west:false};
function chk(key, store, label, note) {
var on = window[store][key];
return '<label style="display:flex;align-items:flex-start;gap:10px;padding:9px 0;border-bottom:1px solid rgba(0,0,0,.05);cursor:pointer">'
+'<div onclick="(function(){window[\''+store+'\'][\''+key+'\']=!window[\''+store+'\'][\''+key+'\'];'
+'document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="width:20px;height:20px;border-radius:5px;border:2px solid '+(on?'var(--accent)':'#D1D5DB')+';background:'+(on?'var(--accent)':'transparent')+';flex-shrink:0;margin-top:1px;display:flex;align-items:center;justify-content:center">'
+(on?'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>':'')
+'</div>'
+'<div><div style="font-size:13px;color:var(--ink);font-weight:500">'+label+'</div>'
+(note?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+note+'</div>':'')
+'</div></label>';
}
var saved = window._qSaved;
return '<div class="page">'
+'<div class="page-header">'
+'<button class="back-btn" onclick="go(\'profile\')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg></button>'
+'<h2>Моя анкета</h2>'
+'</div>'
+'<div style="padding:0 16px 20px">'
// Info banner
+'<div style="background:rgba(0,62,126,.06);border:1px solid rgba(0,62,126,.15);border-radius:10px;padding:10px 14px;margin:12px 0;display:flex;gap:8px;align-items:flex-start">'
+'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" style="flex-shrink:0;margin-top:1px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>'
+'<div style="font-size:12px;color:var(--ink);line-height:1.5">Данные видны директору. При изменениях (новое оборудование, зона, тип объекта) — директор получит уведомление.</div>'
+'</div>'
// Equipment
+'<div class="card" style="padding:14px 16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Измерительное оборудование</div>'
+'<div style="font-size:11px;color:var(--muted);margin-bottom:10px">Что есть в наличии</div>'
+ chk('laser', '_qEquip', 'Лазерный дальномер', 'основной инструмент')
+ chk('tape', '_qEquip', 'Рулетка 510 м', null)
+ chk('level', '_qEquip', 'Нивелир / уровень', null)
+ chk('digital', '_qEquip', 'Цифровой угломер', 'для нестандартных углов')
+ chk('theodolite', '_qEquip', 'Теодолит / тахеометр', 'для больших объектов')
+ chk('photo360', '_qEquip', '360° камера', 'для фиксации помещения')
+'</div>'
// Object types
+'<div class="card" style="padding:14px 16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Типы объектов</div>'
+ chk('flat', '_qTypes', 'Квартиры', null)
+ chk('house', '_qTypes', 'Частные дома / коттеджи', null)
+ chk('office', '_qTypes', 'Офисы', null)
+ chk('commercial', '_qTypes', 'Коммерция (магазины, кафе)', null)
+ chk('newbuild', '_qTypes', 'Новостройки (без отделки)', 'сложные объекты')
+'</div>'
// Transport
+'<div class="card" style="padding:14px 16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Личный автомобиль</div>'
+'<div style="display:flex;gap:8px">'
+'<button onclick="(function(){window._qCar=\'yes\';document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="flex:1;padding:12px;border-radius:10px;border:2px solid '+(window._qCar==='yes'?'var(--accent)':'rgba(0,0,0,.1)')+';background:'+(window._qCar==='yes'?'rgba(0,62,126,.07)':'transparent')+';cursor:pointer;font-size:13px;font-weight:700;color:'+(window._qCar==='yes'?'var(--accent)':'var(--muted)')+'">'
+'🚗 Есть авто</button>'
+'<button onclick="(function(){window._qCar=\'no\';document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="flex:1;padding:12px;border-radius:10px;border:2px solid '+(window._qCar==='no'?'var(--accent)':'rgba(0,0,0,.1)')+';background:'+(window._qCar==='no'?'rgba(0,62,126,.07)':'transparent')+';cursor:pointer;font-size:13px;font-weight:700;color:'+(window._qCar==='no'?'var(--accent)':'var(--muted)')+'">'
+'🚌 Без авто</button>'
+'</div>'
+'</div>'
// Work zones
+'<div class="card" style="padding:14px 16px;margin-bottom:16px">'
+'<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Зона работы</div>'
+'<div style="font-size:11px;color:var(--muted);margin-bottom:10px">Комфортные районы</div>'
+ chk('center', '_qZones', 'Центр города', null)
+ chk('north', '_qZones', 'Север', null)
+ chk('south', '_qZones', 'Юг', null)
+ chk('east', '_qZones', 'Восток', null)
+ chk('west', '_qZones', 'Запад / ЗАД', null)
+'</div>'
// Save
+(saved
? '<div style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:12px;padding:14px;text-align:center;font-size:14px;font-weight:700;color:#065f46">✓ Сохранено · Директор уведомлён</div>'
: '<button onclick="(function(){window._qSaved=true;document.getElementById(\'screen\').innerHTML=renderScreen(\'my_questionnaire\')})()"'
+' style="width:100%;padding:16px;background:var(--accent);color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer">Сохранить изменения</button>'
)
+'<div style="text-align:center;font-size:11px;color:var(--muted);margin-top:8px">Последнее обновление: 20.05.2026 · 09:30</div>'
+'</div>'
+ nav('profile')
+'</div>';
}
/* ─── ACCEPT JOB ─── */ /* ─── ACCEPT JOB ─── */
function screenAcceptJob() { function screenAcceptJob() {
const bk = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M15 18l-6-6 6-6"/></svg>'; const bk = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M15 18l-6-6 6-6"/></svg>';