Group journal history into collapsible day folders with per-day summary
AI Just committed 2026-08-19 06:32:31 +0000

Analyzing diff...

index.html+44 −4
index fc6a958..c5b203b 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,7 @@
 <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
 <title>FODMAP-конструктор приёма пищи</title>
 <style>
-:root{--bg:#f6f3ed;--card:#fff;--ink:#282723;--muted:#77736a;--green:#5c9b6a;--amber:#d8a23d;--red:#cf615a;--line:#e6e0d7;--accent:#7569a8}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Arial,sans-serif;background:var(--bg);color:var(--ink)}.wrap{max-width:1180px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:20px}h1{font-size:30px;margin:0 0 6px}.sub{color:var(--muted);max-width:760px}.grid{display:grid;grid-template-columns:1.35fr .65fr;gap:18px}.card{background:var(--card);border:1px solid var(--line);border-radius:20px;padding:20px;box-shadow:0 8px 30px #00000008}h2{font-size:18px;margin:0 0 14px}.add{display:grid;grid-template-columns:1fr 110px 105px 48px;gap:10px;margin-bottom:16px}select,input,textarea,button{font:inherit;border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:white;color:var(--ink)}button{cursor:pointer;font-weight:650}.primary{background:var(--ink);color:white;border-color:var(--ink)}.meal-row{display:grid;grid-template-columns:1fr 105px 88px 38px;gap:9px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}.tag{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:700}.dot{width:10px;height:10px;border-radius:50%}.g{background:var(--green)}.a{background:var(--amber)}.r{background:var(--red)}.u{background:#9a958c}.remove{border:0;background:transparent;font-size:20px;padding:4px}.meterbox{display:flex;gap:22px;align-items:center}.glass{width:105px;height:220px;border:4px solid #b9b3a9;border-top:0;border-radius:0 0 28px 28px;position:relative;overflow:hidden;background:#faf9f6}.fill{position:absolute;left:0;right:0;bottom:0;height:0%;background:var(--green);transition:.3s}.critical{position:absolute;left:0;right:0;bottom:75%;border-top:2px dashed #7e786e;z-index:2}.critical span{position:absolute;right:4px;top:-21px;font-size:10px;color:#6e685f}.score{font-size:42px;font-weight:800}.status{font-weight:750;margin-top:5px}.small{font-size:12px;color:var(--muted);line-height:1.45}.groups{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:15px}.group{background:#f7f5f1;padding:8px 10px;border-radius:10px;font-size:12px}.advice{margin-top:14px;padding:13px;border-radius:14px;background:#f6f4fb;line-height:1.5}.advice b{display:block;margin-bottom:4px}.adapt{display:grid;gap:8px;margin-top:10px}.adapt button{text-align:left;background:white}.journal{margin-top:18px}.symptoms{display:grid;grid-template-columns:1fr 1fr;gap:10px}.history{margin-top:12px}.entry{padding:14px 0;border-top:1px solid var(--line);font-size:13px}.typeahead{position:relative}.typeahead input{width:100%}.suggest{display:none;position:absolute;z-index:30;left:0;right:0;top:100%;margin-top:4px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px #0000001f;max-height:260px;overflow:auto}.suggest.open{display:block}.suggest div{padding:9px 12px;cursor:pointer;font-size:14px}.suggest div.sel,.suggest div:hover{background:#f2efe8}.suggest .hi{color:var(--muted);font-size:12px;margin-left:6px}.suggest .sechead{padding:7px 12px 4px;font-size:11px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);background:#faf9f6;cursor:default;position:sticky;top:0}.suggest .none{color:var(--muted);cursor:default}.daychart{margin:16px 0;padding:14px;border:1px solid var(--line);border-radius:14px;background:#faf9f6}.chart-controls{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.chart-body{display:flex;gap:20px;flex-wrap:wrap;align-items:flex-start}.chart-legend{display:grid;gap:5px;font-size:12px;min-width:190px}.chart-legend div{display:flex;align-items:center;gap:7px;cursor:pointer;padding:3px 5px;border-radius:8px}.chart-legend div:hover{background:#efece5}.chart-legend i{width:11px;height:11px;border-radius:3px;display:inline-block;flex:none}.chart-bars{display:grid;gap:6px;font-size:12px;min-width:200px;flex:1}.bar-track{background:#ece8e0;border-radius:6px;height:12px;overflow:hidden}.bar-fill{height:100%;border-radius:6px}.chart-detail{margin-top:10px;line-height:1.5}.chart-slice{cursor:pointer}.chart-slice:hover{opacity:.82}
+:root{--bg:#f6f3ed;--card:#fff;--ink:#282723;--muted:#77736a;--green:#5c9b6a;--amber:#d8a23d;--red:#cf615a;--line:#e6e0d7;--accent:#7569a8}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Arial,sans-serif;background:var(--bg);color:var(--ink)}.wrap{max-width:1180px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:20px}h1{font-size:30px;margin:0 0 6px}.sub{color:var(--muted);max-width:760px}.grid{display:grid;grid-template-columns:1.35fr .65fr;gap:18px}.card{background:var(--card);border:1px solid var(--line);border-radius:20px;padding:20px;box-shadow:0 8px 30px #00000008}h2{font-size:18px;margin:0 0 14px}.add{display:grid;grid-template-columns:1fr 110px 105px 48px;gap:10px;margin-bottom:16px}select,input,textarea,button{font:inherit;border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:white;color:var(--ink)}button{cursor:pointer;font-weight:650}.primary{background:var(--ink);color:white;border-color:var(--ink)}.meal-row{display:grid;grid-template-columns:1fr 105px 88px 38px;gap:9px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}.tag{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:700}.dot{width:10px;height:10px;border-radius:50%}.g{background:var(--green)}.a{background:var(--amber)}.r{background:var(--red)}.u{background:#9a958c}.remove{border:0;background:transparent;font-size:20px;padding:4px}.meterbox{display:flex;gap:22px;align-items:center}.glass{width:105px;height:220px;border:4px solid #b9b3a9;border-top:0;border-radius:0 0 28px 28px;position:relative;overflow:hidden;background:#faf9f6}.fill{position:absolute;left:0;right:0;bottom:0;height:0%;background:var(--green);transition:.3s}.critical{position:absolute;left:0;right:0;bottom:75%;border-top:2px dashed #7e786e;z-index:2}.critical span{position:absolute;right:4px;top:-21px;font-size:10px;color:#6e685f}.score{font-size:42px;font-weight:800}.status{font-weight:750;margin-top:5px}.small{font-size:12px;color:var(--muted);line-height:1.45}.groups{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:15px}.group{background:#f7f5f1;padding:8px 10px;border-radius:10px;font-size:12px}.advice{margin-top:14px;padding:13px;border-radius:14px;background:#f6f4fb;line-height:1.5}.advice b{display:block;margin-bottom:4px}.adapt{display:grid;gap:8px;margin-top:10px}.adapt button{text-align:left;background:white}.journal{margin-top:18px}.symptoms{display:grid;grid-template-columns:1fr 1fr;gap:10px}.history{margin-top:12px}.entry{padding:14px 0;border-top:1px solid var(--line);font-size:13px}.typeahead{position:relative}.typeahead input{width:100%}.suggest{display:none;position:absolute;z-index:30;left:0;right:0;top:100%;margin-top:4px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px #0000001f;max-height:260px;overflow:auto}.suggest.open{display:block}.suggest div{padding:9px 12px;cursor:pointer;font-size:14px}.suggest div.sel,.suggest div:hover{background:#f2efe8}.suggest .hi{color:var(--muted);font-size:12px;margin-left:6px}.suggest .sechead{padding:7px 12px 4px;font-size:11px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);background:#faf9f6;cursor:default;position:sticky;top:0}.suggest .none{color:var(--muted);cursor:default}.dayfold{border:1px solid var(--line);border-radius:14px;margin-top:12px;background:#fff;overflow:hidden}.dayfold[open]{box-shadow:0 6px 20px #00000008}.dayfold>summary{list-style:none;cursor:pointer;padding:13px 15px;display:flex;flex-wrap:wrap;align-items:center;gap:10px;background:#faf9f6;font-size:14px}.dayfold>summary::-webkit-details-marker{display:none}.dayfold>summary:before{content:'▸';color:var(--muted);transition:.15s;display:inline-block}.dayfold[open]>summary:before{transform:rotate(90deg)}.dayfold>summary:hover{background:#f2efe8}.dayname{font-weight:750;text-transform:capitalize}.daychips{display:inline-flex;gap:6px}.chip{display:inline-flex;align-items:center;gap:4px;background:#fff;border:1px solid var(--line);border-radius:20px;padding:2px 8px;font-size:12px;font-weight:700}.daymeta{color:var(--muted);font-size:12px;margin-left:auto}.daygroups{display:flex;flex-wrap:wrap;gap:6px;padding:0 15px 4px}.gchip{background:#f6f4fb;color:var(--accent);border-radius:20px;padding:2px 9px;font-size:11px;font-weight:700}.daybody{padding:0 15px 8px}.daybody .entry:first-child{border-top:0}.daychart{margin:16px 0;padding:14px;border:1px solid var(--line);border-radius:14px;background:#faf9f6}.chart-controls{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.chart-body{display:flex;gap:20px;flex-wrap:wrap;align-items:flex-start}.chart-legend{display:grid;gap:5px;font-size:12px;min-width:190px}.chart-legend div{display:flex;align-items:center;gap:7px;cursor:pointer;padding:3px 5px;border-radius:8px}.chart-legend div:hover{background:#efece5}.chart-legend i{width:11px;height:11px;border-radius:3px;display:inline-block;flex:none}.chart-bars{display:grid;gap:6px;font-size:12px;min-width:200px;flex:1}.bar-track{background:#ece8e0;border-radius:6px;height:12px;overflow:hidden}.bar-fill{height:100%;border-radius:6px}.chart-detail{margin-top:10px;line-height:1.5}.chart-slice{cursor:pointer}.chart-slice:hover{opacity:.82}
 .entry-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
 .entry-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}
 .entry-actions button{padding:7px 9px;font-size:12px}
@@ -635,11 +635,51 @@ function saveReaction(){
  e.reactions=e.reactions||[];e.reactions.push(reaction);setEntries(entries);closeReactionModal();showHistory();render()
 }
 function deleteReaction(mealId,reactionId){let entries=getEntries(),e=entries.find(x=>x.id===mealId);if(!e)return;e.reactions=(e.reactions||[]).filter(r=>r.id!==reactionId);setEntries(entries);showHistory();render()}
+let openDays=JSON.parse(sessionStorage.getItem('fodmapOpenDays')||'null');
+function toggleDay(k,el){openDays=openDays||{};openDays[k]=el.open;sessionStorage.setItem('fodmapOpenDays',JSON.stringify(openDays))}
+function dayKey(e){let d=mealDateTime(e);return localDateString(d)}
+function dayTitle(k){
+ let d=new Date(k+'T12:00:00'),today=localDateString(),y=new Date();y.setDate(y.getDate()-1);
+ let label=d.toLocaleDateString('ru-RU',{day:'2-digit',month:'long',year:'numeric',weekday:'long'});
+ if(k===today)return 'Сегодня · '+label;
+ if(k===localDateString(y))return 'Вчера · '+label;
+ return label;
+}
+function daySummary(list){
+ let lv={g:0,a:0,r:0,u:0},grams=0,groups=new Set(),maxSev=null;
+ list.forEach(e=>{
+  (e.meal||[]).forEach(m=>{lv[m.level]!==undefined?lv[m.level]++:lv.u++;grams+=m.g||0;(m.groups||[]).forEach(x=>groups.add(x))});
+  (e.reactions||[]).forEach(r=>{let s=+r.severity||0;if(maxSev===null||s>maxSev)maxSev=s});
+ });
+ let chips='';
+ if(lv.g)chips+='<span class="chip"><i class="dot g"></i>'+lv.g+'</span>';
+ if(lv.a)chips+='<span class="chip"><i class="dot a"></i>'+lv.a+'</span>';
+ if(lv.r)chips+='<span class="chip"><i class="dot r"></i>'+lv.r+'</span>';
+ if(lv.u)chips+='<span class="chip"><i class="dot u"></i>'+lv.u+'</span>';
+ let sev=maxSev===null?'реакций нет':'макс. реакция '+maxSev+'/10';
+ return {chips,line:list.length+' '+plural(list.length,'приём','приёма','приёмов')+' · '+Math.round(grams)+' г · '+sev,groups:[...groups]};
+}
+function plural(n,a,b,c){let m=n%100;if(m>=11&&m<=14)return c;m=n%10;return m===1?a:(m>=2&&m<=4?b:c)}
+function entryHtml(e){
+ let rs=e.reactions||[];
+ let reactionHtml=rs.length?rs.map(r=>`<div class="advice" style="margin-top:8px"><b>Запись ${new Date(r.createdAt).toLocaleString('ru-RU')} • ${r.severity}/10 — ${esc(r.severityLabel||severityText[r.severity])}</b>${esc(r.delay)}${r.types?.length?' • '+r.types.map(esc).join(', '):''}${r.comment?'<br>'+esc(r.comment):''}<br><button class="remove danger" onclick="deleteReaction('${e.id}','${r.id}')">× удалить запись реакции</button></div>`).join(''):'<div class="small" style="margin-top:6px">Реакция пока не отмечена.</div>';
+ return `<div class="entry"><div class="entry-head"><div><b>🍽 ${mealDateLabel(e)}</b><br>${e.meal.map(m=>`${esc(m.name)} — ${m.g} г`).join(', ')}</div></div>${reactionHtml}<div class="entry-actions"><button onclick="openReactionModal('${e.id}')">+ Добавить реакцию</button><button onclick="editMeal('${e.id}')">Открыть / изменить состав</button><button class="danger" onclick="deleteMeal('${e.id}')">Удалить</button></div></div>`;
+}
 function showHistory(){
  let entries=getEntries().sort((a,b)=>mealDateTime(b)-mealDateTime(a)),h=document.getElementById('history');
- if(!entries.length)h.innerHTML='<div class="small">Сохранённых приёмов пищи пока нет.</div>';
- else h.innerHTML=entries.map(e=>{let rs=e.reactions||[];let reactionHtml=rs.length?rs.map(r=>`<div class="advice" style="margin-top:8px"><b>Запись ${new Date(r.createdAt).toLocaleString('ru-RU')} • ${r.severity}/10 — ${esc(r.severityLabel||severityText[r.severity])}</b>${esc(r.delay)}${r.types?.length?' • '+r.types.map(esc).join(', '):''}${r.comment?'<br>'+esc(r.comment):''}<br><button class="remove danger" onclick="deleteReaction('${e.id}','${r.id}')">× удалить запись реакции</button></div>`).join(''):'<div class="small" style="margin-top:6px">Реакция пока не отмечена.</div>';
- return `<div class="entry"><div class="entry-head"><div><b>🍽 ${mealDateLabel(e)}</b><br>${e.meal.map(m=>`${esc(m.name)} — ${m.g} г`).join(', ')}</div></div>${reactionHtml}<div class="entry-actions"><button onclick="openReactionModal('${e.id}')">+ Добавить реакцию</button><button onclick="editMeal('${e.id}')">Открыть / изменить состав</button><button class="danger" onclick="deleteMeal('${e.id}')">Удалить</button></div></div>`}).join('');
+ if(!entries.length){h.innerHTML='<div class="small">Сохранённых приёмов пищи пока нет.</div>';renderAnalytics(entries);if(document.getElementById('dayChart'))renderDayChart();return}
+ let days=new Map();
+ entries.forEach(e=>{let k=dayKey(e);if(!days.has(k))days.set(k,[]);days.get(k).push(e)});
+ let today=localDateString();
+ h.innerHTML=[...days.entries()].map(([k,list])=>{
+  let s=daySummary(list);
+  let open=(openDays&&k in openDays)?!!openDays[k]:k===today;
+  return `<details class="dayfold" ${open?'open':''} ontoggle="toggleDay('${k}',this)">
+   <summary><span class="dayname">${esc(dayTitle(k))}</span><span class="daychips">${s.chips}</span><span class="daymeta">${esc(s.line)}</span></summary>
+   ${s.groups.length?'<div class="daygroups">'+s.groups.map(g=>'<span class="gchip">'+esc(g)+'</span>').join('')+'</div>':''}
+   <div class="daybody">${list.map(entryHtml).join('')}</div>
+  </details>`;
+ }).join('');
  renderAnalytics(entries);if(document.getElementById('dayChart'))renderDayChart()
 }
 function renderAnalytics(entries){