From c9ef8a852bebe0e04e5d0ba7eac32b5036a4fe2d Mon Sep 17 00:00:00 2001 From: ashirova_gulnur Date: Fri, 26 Jun 2026 06:35:44 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87,=20=D0=B4?= =?UTF-8?q?=D0=BB=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C,=20=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D0=B5=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20=D0=B2=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 192 ++++++++++++++++++++++++++++++++++++++++++++++++----- index.html | 68 +++++++++++++++++++ style.css | 14 ++++ 3 files changed, 256 insertions(+), 18 deletions(-) diff --git a/app.js b/app.js index 561fcae..fb06cb6 100644 --- a/app.js +++ b/app.js @@ -50,11 +50,38 @@ const PRIORITY = { high: {label:'🔴 Высокая', cls:'bg-red-50 text-red-600'}, }; +const CATEGORIES = [ + {key:'home', label:'🏠 Дом', color:'#10b981'}, + {key:'work', label:'💼 Работа', color:'#3b82f6'}, + {key:'study', label:'📚 Учёба', color:'#8b5cf6'}, + {key:'shop', label:'🛒 Покупки', color:'#f59e0b'}, + {key:'kids', label:'👶 Дети', color:'#ec4899'}, + {key:'money', label:'💰 Финансы', color:'#06b6d4'}, + {key:'health', label:'❤️ Здоровье', color:'#ef4444'}, + {key:'other', label:'🎉 Другое', color:'#6366f1'}, +]; +function catByKey(k){ return CATEGORIES.find(c=>c.key===k) || CATEGORIES[CATEGORIES.length-1]; } +function catOptions(sel){ return CATEGORIES.map(c=>``).join(''); } +function formatDuration(min){ + min = Number(min); if(!min) return ''; + if(min<60) return min+' мин'; + const h=Math.floor(min/60), m=min%60; + return m? `${h} ч ${m} мин` : `${h} ч`; +} + /* ============================================================ ИНИЦИАЛИЗАЦИЯ ============================================================ */ function init(){ data = load(); + if(data && data.members && data.members.length){ + // миграции + data.events = data.events || []; + (data.tasks||[]).forEach(t=>{ + if(!t.category) t.category = 'other'; + if(t.duration===undefined) t.duration = null; + }); + } if(!data || !data.members || !data.members.length){ showOnboard(); return; } me = localStorage.getItem(SESSION); if(!me || !member(me)) me = data.members[0].id; @@ -89,7 +116,7 @@ $('#ob-create').onclick = ()=>{ const names = $$('#ob-members .ob-name').map(i=>i.value.trim()).filter(Boolean); if(names.length<1){ toast('Добавьте хотя бы одного члена семьи'); return; } const members = names.map((n,i)=>({ id:uid(), name:n, color:AVATAR_COLORS[i%AVATAR_COLORS.length], emoji:AVATAR_EMOJIS[i%AVATAR_EMOJIS.length] })); - data = { familyName, members, tasks:[] }; + data = { familyName, members, tasks:[], events:[] }; me = members[0].id; localStorage.setItem(SESSION, me); save(); @@ -168,12 +195,15 @@ function taskRow(t){ const from = member(t.assignerId); const overdue = t.status==='todo' && t.dueDate && parseDate(t.dueDate) < new Date(todayStr()); const st = STATUS[t.status] || STATUS.todo; + const cat = catByKey(t.category); + const dur = formatDuration(t.duration); return `
+ ${cat.label}
${escapeHtml(t.title)}
${st.label}
@@ -182,6 +212,7 @@ function taskRow(t){ ${avatarHtml(a,16)} ${escapeHtml(a?a.name:'—')} ${from?`от ${escapeHtml(from.name)}`:''} ${t.dueDate?`⏰ ${prettyDateTime(t.dueDate,t.time)} · ${relativeDue(t.dueDate)}`:''} + ${dur?`⏱ ${dur}`:''} ${PRIORITY[t.priority].label}
@@ -251,34 +282,49 @@ function renderAll(){
+
`; const apply = ()=>{ - const fm = $('#flt-member').value, fs = $('#flt-status').value; + const fm = $('#flt-member').value, fs = $('#flt-status').value, fc = $('#flt-cat').value; let tasks = data.tasks.slice().sort((a,b)=>(b.createdAt||0)-(a.createdAt||0)); if(fm) tasks = tasks.filter(t=>t.assigneeId===fm||t.assignerId===fm); if(fs) tasks = tasks.filter(t=>t.status===fs); + if(fc) tasks = tasks.filter(t=>t.category===fc); const list = $('#list'); if(!tasks.length){ list.innerHTML = emptyHtml('🗂','Задач не найдено по фильтру.'); return; } list.innerHTML = tasks.map(taskRow).join(''); list.querySelectorAll('.task').forEach(bindTaskRow); }; - $('#flt-member').onchange = apply; $('#flt-status').onchange = apply; + $('#flt-member').onchange = apply; $('#flt-status').onchange = apply; $('#flt-cat').onchange = apply; apply(); } -/* ---------- Расписание (календарь) ---------- */ +/* ---------- Расписание (календарь + личные события) ---------- */ let calRef = new Date(); +let selDay = todayStr(); +let schedScope = 'mine'; // 'mine' | 'all' + +function dayItems(ds, scope){ + let evs = (data.events||[]).filter(e=>e.date===ds); + let tks = data.tasks.filter(t=>t.dueDate===ds && t.status!=='cancelled'); + if(scope==='mine'){ + evs = evs.filter(e=>e.ownerId===me); + tks = tks.filter(t=>t.assigneeId===me); + } + return [ + ...evs.map(e=>({type:'event', sort:e.start||'00:00', obj:e})), + ...tks.map(t=>({type:'task', sort:t.time||'zz', obj:t})), + ].sort((a,b)=>a.sort.localeCompare(b.sort)); +} + function renderSchedule(){ + if(!selDay) selDay = todayStr(); const y = calRef.getFullYear(), m = calRef.getMonth(); const first = new Date(y,m,1); - const startDow = (first.getDay()+6)%7; // понедельник = 0 + const startDow = (first.getDay()+6)%7; const daysInMonth = new Date(y,m+1,0).getDate(); const monthName = first.toLocaleDateString('ru-RU',{month:'long', year:'numeric'}); - const tasksByDate = {}; - data.tasks.filter(t=>t.dueDate && t.status!=='cancelled').forEach(t=>{ - (tasksByDate[t.dueDate] = tasksByDate[t.dueDate]||[]).push(t); - }); let cells = ''; const dows = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс']; cells += dows.map(d=>`
${d}
`).join(''); @@ -286,32 +332,137 @@ function renderSchedule(){ const today = todayStr(); for(let d=1; d<=daysInMonth; d++){ const ds = fmt(new Date(y,m,d)); - const tlist = tasksByDate[ds]||[]; - cells += `
+ const items = dayItems(ds, schedScope); + cells += `
${d}
- ${tlist.slice(0,3).map(t=>`
${escapeHtml(t.title)}
`).join('')} - ${tlist.length>3?`
+${tlist.length-3} ещё
`:''} + ${items.slice(0,3).map(it=>chipHtml(it)).join('')} + ${items.length>3?`
+${items.length-3}
`:''}
`; } $('#main').innerHTML = ` -
+

${monthName}

-
+
+
+ + +
+
${cells}
+
- ${data.members.map(m=>`${avatarHtml(m,14)} ${escapeHtml(m.name)}`).join('')} + ${CATEGORIES.map(c=>`${c.label}`).join('')}
`; $('#cal-prev').onclick=()=>{ calRef=new Date(y,m-1,1); renderSchedule(); }; $('#cal-next').onclick=()=>{ calRef=new Date(y,m+1,1); renderSchedule(); }; - $('#cal-today').onclick=()=>{ calRef=new Date(); renderSchedule(); }; - $$('#main .cal-task').forEach(el=>el.onclick=()=>openTaskModal(el.dataset.id)); + $('#cal-today').onclick=()=>{ calRef=new Date(); selDay=todayStr(); renderSchedule(); }; + $('#cal-addev').onclick=()=>openEventModal(null, selDay); + $('#sc-mine').onclick=()=>{ schedScope='mine'; renderSchedule(); }; + $('#sc-all').onclick=()=>{ schedScope='all'; renderSchedule(); }; + $$('#main .cal-day[data-day]').forEach(el=>el.onclick=e=>{ if(e.target.closest('.cal-task')) return; selDay=el.dataset.day; renderSchedule(); }); + $$('#main .cal-task').forEach(el=>el.onclick=e=>{ e.stopPropagation(); el.dataset.type==='event'?openEventModal(el.dataset.id):openTaskModal(el.dataset.id); }); + renderDayPanel(); } +function chipHtml(it){ + if(it.type==='event'){ + const c=catByKey(it.obj.category); + return `
${escapeHtml(it.obj.start)} ${escapeHtml(it.obj.title)}
`; + } + const t=it.obj; + return `
✓ ${escapeHtml(t.title)}
`; +} + +function renderDayPanel(){ + const panel = $('#day-panel'); if(!panel) return; + const items = dayItems(selDay, schedScope); + const d = parseDate(selDay); + const dname = d ? d.toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long'}) : selDay; + const isToday = selDay===todayStr(); + panel.innerHTML = ` +
+

${dname} ${isToday?'сегодня':''}

+ +
+ ${items.length? `
${items.map(it=>dayRowHtml(it)).join('')}
` : emptyHtml('📅','На этот день ничего не запланировано.')}`; + $('#dp-addev')?.addEventListener('click', ()=>openEventModal(null, selDay)); + panel.querySelectorAll('.day-row').forEach(r=>{ + r.onclick=()=>{ r.dataset.type==='event'?openEventModal(r.dataset.id):openTaskModal(r.dataset.id); }; + }); +} + +function dayRowHtml(it){ + if(it.type==='event'){ + const e=it.obj, c=catByKey(e.category), owner=member(e.ownerId); + return `
+
${escapeHtml(e.start)}
${escapeHtml(e.end)}
+
+
+
${escapeHtml(e.title)}
+
${avatarHtml(owner,14)} ${escapeHtml(owner?owner.name:'—')} · ${c.label}
+
+ +
`; + } + const t=it.obj, cat=catByKey(t.category), a=member(t.assigneeId); + return `
+
${escapeHtml(t.time||'—')}
+
+
+
${escapeHtml(t.title)}
+
${avatarHtml(a,14)} ${escapeHtml(a?a.name:'—')} · ${cat.label}${t.duration?' · ⏱ '+formatDuration(t.duration):''}
+
+ ${STATUS[t.status].label} + +
`; +} + +/* ---------- модалка события ---------- */ +let editingEventId = null; +function openEventModal(id, dateStr){ + editingEventId = id; + const e = id ? data.events.find(x=>x.id===id) : null; + $('#ev-title').textContent = e ? 'Событие' : 'Новое событие'; + $('#ev-name').value = e?.title||''; + $('#ev-date').value = e?.date || dateStr || selDay || todayStr(); + $('#ev-start').value = e?.start||''; + $('#ev-end').value = e?.end||''; + $('#ev-owner').innerHTML = data.members.map(m=>``).join(''); + $('#ev-cat').innerHTML = catOptions(); + $('#ev-cat').value = e?.category||'other'; + $('#ev-delete').classList.toggle('hidden', !e); + $('#event-modal').classList.remove('hidden'); + setTimeout(()=>$('#ev-name').focus(),50); +} +$('#ev-close').onclick = ()=>$('#event-modal').classList.add('hidden'); +$('#event-modal').addEventListener('click', e=>{ if(e.target.id==='event-modal') $('#event-modal').classList.add('hidden'); }); +$('#ev-save').onclick = ()=>{ + const title=$('#ev-name').value.trim(); + const date=$('#ev-date').value; + const start=$('#ev-start').value, end=$('#ev-end').value; + if(!title){ toast('Введите название'); $('#ev-name').focus(); return; } + if(!date){ toast('Выберите дату'); return; } + if(!start||!end){ toast('Укажите время начала и конца'); return; } + if(start>=end){ toast('Конец должен быть позже начала'); return; } + const payload={ title, date, start, end, ownerId:$('#ev-owner').value, category:$('#ev-cat').value }; + if(editingEventId){ Object.assign(data.events.find(x=>x.id===editingEventId), payload); toast('Сохранено'); } + else { data.events.push(Object.assign({id:uid()}, payload)); toast('Событие добавлено 📅'); } + selDay = date; + save(); $('#event-modal').classList.add('hidden'); render(); +}; +$('#ev-delete').onclick = ()=>{ + if(!editingEventId) return; + if(!confirm('Удалить событие?')) return; + data.events = data.events.filter(x=>x.id!==editingEventId); + save(); $('#event-modal').classList.add('hidden'); render(); + toast('Событие удалено'); +}; + /* ---------- Семья ---------- */ function renderFamily(){ const cards = data.members.map(m=>{ @@ -401,6 +552,9 @@ function openTaskModal(id){ $('#f-date').value = t?.dueDate||''; $('#f-time').value = t?.time||''; $('#f-priority').value = t?.priority||'medium'; + $('#f-duration').value = t?.duration||''; + $('#f-category').innerHTML = catOptions(); + $('#f-category').value = t?.category||'other'; $('#f-from').textContent = member(me).name; // список исполнителей $('#f-assignee').innerHTML = data.members.map(m=>``).join(''); @@ -428,7 +582,9 @@ $('#modal-save').onclick = ()=>{ assigneeId, dueDate: $('#f-date').value || null, time: $('#f-time').value || null, + duration: $('#f-duration').value ? Number($('#f-duration').value) : null, priority: $('#f-priority').value, + category: $('#f-category').value, }; if(editingId){ const t = data.tasks.find(x=>x.id===editingId); diff --git a/index.html b/index.html index 94f1de4..4b41d28 100644 --- a/index.html +++ b/index.html @@ -120,6 +120,27 @@ window.addEventListener('error',function(e){
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ Личные события помогают планировать день: школа, работа, тренировки. Они не зависят от задач и видны в расписании. +
+
+
+ + +
+
+
+ diff --git a/style.css b/style.css index 331f518..f39eb98 100644 --- a/style.css +++ b/style.css @@ -53,6 +53,8 @@ textarea.field{resize:vertical} .cal-day{min-height:96px;background:#fff;border:1px solid var(--line);border-radius:10px;padding:6px;overflow:hidden} .cal-day.off{background:#fafbfc;color:#c2c8d2} .cal-day.today{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-50) inset} +.cal-day.selected{outline:2px solid var(--accent);outline-offset:-2px} +.cal-day[data-day]{cursor:pointer} .cal-dnum{font-size:12px;font-weight:700;margin-bottom:4px} .cal-task{font-size:11px;font-weight:600;border-radius:5px;padding:2px 5px;margin-bottom:3px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @@ -61,6 +63,18 @@ textarea.field{resize:vertical} .stat-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px} +/* сегмент-переключатель */ +.seg{border:none;background:none;padding:6px 12px;font-size:13px;font-weight:600;color:var(--muted);border-radius:7px;cursor:pointer;font-family:inherit} +.seg.active{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.1)} + +/* элементы расписания */ +.dot{width:10px;height:10px;border-radius:999px;flex-shrink:0} +.time-badge{font-size:11px;font-weight:700;text-align:center;line-height:1.2;min-width:52px;padding:6px 4px;border-radius:8px;white-space:nowrap} +.time-badge span{display:block;font-size:10px;font-weight:600} +.day-row{transition:box-shadow .15s} +.day-row:hover{box-shadow:0 4px 14px rgba(15,18,24,.08)} +.day-row .done{text-decoration:line-through;opacity:.6} + .menu-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:.12s} .menu-row:hover{background:var(--line)} .menu-row.active{background:var(--accent-50)}