feat: категории задач, длительность, личные события в расписании
This commit is contained in:
parent
1d2281a0ec
commit
c9ef8a852b
192
app.js
192
app.js
@ -50,11 +50,38 @@ const PRIORITY = {
|
|||||||
high: {label:'🔴 Высокая', cls:'bg-red-50 text-red-600'},
|
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=>`<option value="${c.key}">${c.label}</option>`).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(){
|
function init(){
|
||||||
data = load();
|
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; }
|
if(!data || !data.members || !data.members.length){ showOnboard(); return; }
|
||||||
me = localStorage.getItem(SESSION);
|
me = localStorage.getItem(SESSION);
|
||||||
if(!me || !member(me)) me = data.members[0].id;
|
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);
|
const names = $$('#ob-members .ob-name').map(i=>i.value.trim()).filter(Boolean);
|
||||||
if(names.length<1){ toast('Добавьте хотя бы одного члена семьи'); return; }
|
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] }));
|
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;
|
me = members[0].id;
|
||||||
localStorage.setItem(SESSION, me);
|
localStorage.setItem(SESSION, me);
|
||||||
save();
|
save();
|
||||||
@ -168,12 +195,15 @@ function taskRow(t){
|
|||||||
const from = member(t.assignerId);
|
const from = member(t.assignerId);
|
||||||
const overdue = t.status==='todo' && t.dueDate && parseDate(t.dueDate) < new Date(todayStr());
|
const overdue = t.status==='todo' && t.dueDate && parseDate(t.dueDate) < new Date(todayStr());
|
||||||
const st = STATUS[t.status] || STATUS.todo;
|
const st = STATUS[t.status] || STATUS.todo;
|
||||||
|
const cat = catByKey(t.category);
|
||||||
|
const dur = formatDuration(t.duration);
|
||||||
return `
|
return `
|
||||||
<div class="task ${t.status==='done'?'done':''} ${t.status==='cancelled'?'cancelled':''} ${overdue?'overdue':''}" data-id="${t.id}">
|
<div class="task ${t.status==='done'?'done':''} ${t.status==='cancelled'?'cancelled':''} ${overdue?'overdue':''}" data-id="${t.id}">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<button class="iconbtn primary t-done" title="Отметить выполненной" ${t.status==='done'?'disabled':''}>✓</button>
|
<button class="iconbtn primary t-done" title="Отметить выполненной" ${t.status==='done'?'disabled':''}>✓</button>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-start gap-2 flex-wrap">
|
<div class="flex items-start gap-2 flex-wrap">
|
||||||
|
<span class="chip" style="background:${cat.color}22;color:${cat.color}">${cat.label}</span>
|
||||||
<div class="t-title font-semibold flex-1 min-w-0">${escapeHtml(t.title)}</div>
|
<div class="t-title font-semibold flex-1 min-w-0">${escapeHtml(t.title)}</div>
|
||||||
<span class="badge ${st.cls}">${st.label}</span>
|
<span class="badge ${st.cls}">${st.label}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -182,6 +212,7 @@ function taskRow(t){
|
|||||||
<span class="chip" style="background:${(a&&a.color)||'#94a3b8'}22;color:${(a&&a.color)||'#94a3b8'}">${avatarHtml(a,16)} ${escapeHtml(a?a.name:'—')}</span>
|
<span class="chip" style="background:${(a&&a.color)||'#94a3b8'}22;color:${(a&&a.color)||'#94a3b8'}">${avatarHtml(a,16)} ${escapeHtml(a?a.name:'—')}</span>
|
||||||
${from?`<span class="text-muted">от ${escapeHtml(from.name)}</span>`:''}
|
${from?`<span class="text-muted">от ${escapeHtml(from.name)}</span>`:''}
|
||||||
${t.dueDate?`<span class="badge ${overdue?'bg-red-100 text-red-700':'bg-line text-muted'}">⏰ ${prettyDateTime(t.dueDate,t.time)} · ${relativeDue(t.dueDate)}</span>`:''}
|
${t.dueDate?`<span class="badge ${overdue?'bg-red-100 text-red-700':'bg-line text-muted'}">⏰ ${prettyDateTime(t.dueDate,t.time)} · ${relativeDue(t.dueDate)}</span>`:''}
|
||||||
|
${dur?`<span class="badge bg-gray-100 text-gray-600">⏱ ${dur}</span>`:''}
|
||||||
<span class="badge ${PRIORITY[t.priority].cls}">${PRIORITY[t.priority].label}</span>
|
<span class="badge ${PRIORITY[t.priority].cls}">${PRIORITY[t.priority].label}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -251,34 +282,49 @@ function renderAll(){
|
|||||||
<div class="flex flex-wrap gap-2 mb-4" id="filters">
|
<div class="flex flex-wrap gap-2 mb-4" id="filters">
|
||||||
<select id="flt-member" class="field !w-auto !py-2"><option value="">Все члены</option>${data.members.map(m=>`<option value="${m.id}">${escapeHtml(m.name)}</option>`).join('')}</select>
|
<select id="flt-member" class="field !w-auto !py-2"><option value="">Все члены</option>${data.members.map(m=>`<option value="${m.id}">${escapeHtml(m.name)}</option>`).join('')}</select>
|
||||||
<select id="flt-status" class="field !w-auto !py-2"><option value="">Любой статус</option>${statuses.map(s=>`<option value="${s}">${STATUS[s].label}</option>`).join('')}</select>
|
<select id="flt-status" class="field !w-auto !py-2"><option value="">Любой статус</option>${statuses.map(s=>`<option value="${s}">${STATUS[s].label}</option>`).join('')}</select>
|
||||||
|
<select id="flt-cat" class="field !w-auto !py-2"><option value="">Все виды</option>${CATEGORIES.map(c=>`<option value="${c.key}">${c.label}</option>`).join('')}</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="list" class="space-y-3"></div>`;
|
<div id="list" class="space-y-3"></div>`;
|
||||||
const apply = ()=>{
|
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));
|
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(fm) tasks = tasks.filter(t=>t.assigneeId===fm||t.assignerId===fm);
|
||||||
if(fs) tasks = tasks.filter(t=>t.status===fs);
|
if(fs) tasks = tasks.filter(t=>t.status===fs);
|
||||||
|
if(fc) tasks = tasks.filter(t=>t.category===fc);
|
||||||
const list = $('#list');
|
const list = $('#list');
|
||||||
if(!tasks.length){ list.innerHTML = emptyHtml('🗂','Задач не найдено по фильтру.'); return; }
|
if(!tasks.length){ list.innerHTML = emptyHtml('🗂','Задач не найдено по фильтру.'); return; }
|
||||||
list.innerHTML = tasks.map(taskRow).join('');
|
list.innerHTML = tasks.map(taskRow).join('');
|
||||||
list.querySelectorAll('.task').forEach(bindTaskRow);
|
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();
|
apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Расписание (календарь) ---------- */
|
/* ---------- Расписание (календарь + личные события) ---------- */
|
||||||
let calRef = new Date();
|
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(){
|
function renderSchedule(){
|
||||||
|
if(!selDay) selDay = todayStr();
|
||||||
const y = calRef.getFullYear(), m = calRef.getMonth();
|
const y = calRef.getFullYear(), m = calRef.getMonth();
|
||||||
const first = new Date(y,m,1);
|
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 daysInMonth = new Date(y,m+1,0).getDate();
|
||||||
const monthName = first.toLocaleDateString('ru-RU',{month:'long', year:'numeric'});
|
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 = '';
|
let cells = '';
|
||||||
const dows = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
|
const dows = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
|
||||||
cells += dows.map(d=>`<div class="cal-dow">${d}</div>`).join('');
|
cells += dows.map(d=>`<div class="cal-dow">${d}</div>`).join('');
|
||||||
@ -286,32 +332,137 @@ function renderSchedule(){
|
|||||||
const today = todayStr();
|
const today = todayStr();
|
||||||
for(let d=1; d<=daysInMonth; d++){
|
for(let d=1; d<=daysInMonth; d++){
|
||||||
const ds = fmt(new Date(y,m,d));
|
const ds = fmt(new Date(y,m,d));
|
||||||
const tlist = tasksByDate[ds]||[];
|
const items = dayItems(ds, schedScope);
|
||||||
cells += `<div class="cal-day ${ds===today?'today':''}">
|
cells += `<div class="cal-day ${ds===today?'today':''} ${ds===selDay?'selected':''}" data-day="${ds}">
|
||||||
<div class="cal-dnum">${d}</div>
|
<div class="cal-dnum">${d}</div>
|
||||||
${tlist.slice(0,3).map(t=>`<div class="cal-task" style="background:${colorFor(t.assigneeId)}22;color:${colorFor(t.assigneeId)};${t.status==='done'?'text-decoration:line-through;opacity:.6':''}" data-id="${t.id}">${escapeHtml(t.title)}</div>`).join('')}
|
${items.slice(0,3).map(it=>chipHtml(it)).join('')}
|
||||||
${tlist.length>3?`<div class="text-xs text-muted">+${tlist.length-3} ещё</div>`:''}
|
${items.length>3?`<div class="text-xs text-muted">+${items.length-3}</div>`:''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
$('#main').innerHTML = `
|
$('#main').innerHTML = `
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4 gap-2 flex-wrap">
|
||||||
<h2 class="text-2xl font-extrabold capitalize">${monthName}</h2>
|
<h2 class="text-2xl font-extrabold capitalize">${monthName}</h2>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
|
<div class="flex bg-line rounded-lg p-1">
|
||||||
|
<button id="sc-mine" class="seg ${schedScope==='mine'?'active':''}">Мои</button>
|
||||||
|
<button id="sc-all" class="seg ${schedScope==='all'?'active':''}">Вся семья</button>
|
||||||
|
</div>
|
||||||
<button id="cal-prev" class="iconbtn">‹</button>
|
<button id="cal-prev" class="iconbtn">‹</button>
|
||||||
<button id="cal-today" class="btn-ghost text-sm">Сегодня</button>
|
<button id="cal-today" class="btn-ghost text-sm">Сегодня</button>
|
||||||
<button id="cal-next" class="iconbtn">›</button>
|
<button id="cal-next" class="iconbtn">›</button>
|
||||||
|
<button id="cal-addev" class="btn-primary text-sm">+ Событие</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cal-grid">${cells}</div>
|
<div class="cal-grid">${cells}</div>
|
||||||
|
<div id="day-panel" class="mt-5"></div>
|
||||||
<div class="flex flex-wrap gap-3 mt-4 text-xs text-muted">
|
<div class="flex flex-wrap gap-3 mt-4 text-xs text-muted">
|
||||||
${data.members.map(m=>`<span class="flex items-center gap-1">${avatarHtml(m,14)} ${escapeHtml(m.name)}</span>`).join('')}
|
${CATEGORIES.map(c=>`<span class="flex items-center gap-1"><span class="dot" style="background:${c.color}"></span>${c.label}</span>`).join('')}
|
||||||
</div>`;
|
</div>`;
|
||||||
$('#cal-prev').onclick=()=>{ calRef=new Date(y,m-1,1); renderSchedule(); };
|
$('#cal-prev').onclick=()=>{ calRef=new Date(y,m-1,1); renderSchedule(); };
|
||||||
$('#cal-next').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(); };
|
$('#cal-today').onclick=()=>{ calRef=new Date(); selDay=todayStr(); renderSchedule(); };
|
||||||
$$('#main .cal-task').forEach(el=>el.onclick=()=>openTaskModal(el.dataset.id));
|
$('#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 `<div class="cal-task" style="background:${c.color}22;color:${c.color}" data-type="event" data-id="${it.obj.id}">${escapeHtml(it.obj.start)} ${escapeHtml(it.obj.title)}</div>`;
|
||||||
|
}
|
||||||
|
const t=it.obj;
|
||||||
|
return `<div class="cal-task" style="background:${colorFor(t.assigneeId)}22;color:${colorFor(t.assigneeId)};${t.status==='done'?'text-decoration:line-through;opacity:.6':''}" data-type="task" data-id="${t.id}">✓ ${escapeHtml(t.title)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||||
|
<h3 class="font-bold text-lg capitalize">${dname} ${isToday?'<span class="badge bg-accent-50 text-ink">сегодня</span>':''}</h3>
|
||||||
|
<button id="dp-addev" class="btn-ghost text-sm">+ событие в этот день</button>
|
||||||
|
</div>
|
||||||
|
${items.length? `<div class="space-y-2">${items.map(it=>dayRowHtml(it)).join('')}</div>` : 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 `<div class="day-row stat-card flex items-center gap-3" data-type="event" data-id="${e.id}" style="cursor:pointer">
|
||||||
|
<div class="time-badge" style="background:${c.color}1a;color:${c.color}">${escapeHtml(e.start)}<br><span class="opacity-60">${escapeHtml(e.end)}</span></div>
|
||||||
|
<div class="dot" style="background:${c.color}"></div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="font-semibold">${escapeHtml(e.title)}</div>
|
||||||
|
<div class="text-xs text-muted">${avatarHtml(owner,14)} ${escapeHtml(owner?owner.name:'—')} · ${c.label}</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-muted">✎</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const t=it.obj, cat=catByKey(t.category), a=member(t.assigneeId);
|
||||||
|
return `<div class="day-row stat-card flex items-center gap-3" data-type="task" data-id="${t.id}" style="cursor:pointer">
|
||||||
|
<div class="time-badge" style="background:${colorFor(t.assigneeId)}1a;color:${colorFor(t.assigneeId)}">${escapeHtml(t.time||'—')}</div>
|
||||||
|
<div class="dot" style="background:${colorFor(t.assigneeId)}"></div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="font-semibold ${t.status==='done'?'done':''}">${escapeHtml(t.title)}</div>
|
||||||
|
<div class="text-xs text-muted">${avatarHtml(a,14)} ${escapeHtml(a?a.name:'—')} · ${cat.label}${t.duration?' · ⏱ '+formatDuration(t.duration):''}</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge ${STATUS[t.status].cls}">${STATUS[t.status].label}</span>
|
||||||
|
<span class="text-muted">✎</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- модалка события ---------- */
|
||||||
|
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=>`<option value="${m.id}" ${(e?e.ownerId===m.id:m.id===me)?'selected':''}>${escapeHtml(m.name)}</option>`).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(){
|
function renderFamily(){
|
||||||
const cards = data.members.map(m=>{
|
const cards = data.members.map(m=>{
|
||||||
@ -401,6 +552,9 @@ function openTaskModal(id){
|
|||||||
$('#f-date').value = t?.dueDate||'';
|
$('#f-date').value = t?.dueDate||'';
|
||||||
$('#f-time').value = t?.time||'';
|
$('#f-time').value = t?.time||'';
|
||||||
$('#f-priority').value = t?.priority||'medium';
|
$('#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-from').textContent = member(me).name;
|
||||||
// список исполнителей
|
// список исполнителей
|
||||||
$('#f-assignee').innerHTML = data.members.map(m=>`<option value="${m.id}" ${t&&t.assigneeId===m.id?'selected':m.id===me&&!t?'selected':''}>${escapeHtml(m.name)}</option>`).join('');
|
$('#f-assignee').innerHTML = data.members.map(m=>`<option value="${m.id}" ${t&&t.assigneeId===m.id?'selected':m.id===me&&!t?'selected':''}>${escapeHtml(m.name)}</option>`).join('');
|
||||||
@ -428,7 +582,9 @@ $('#modal-save').onclick = ()=>{
|
|||||||
assigneeId,
|
assigneeId,
|
||||||
dueDate: $('#f-date').value || null,
|
dueDate: $('#f-date').value || null,
|
||||||
time: $('#f-time').value || null,
|
time: $('#f-time').value || null,
|
||||||
|
duration: $('#f-duration').value ? Number($('#f-duration').value) : null,
|
||||||
priority: $('#f-priority').value,
|
priority: $('#f-priority').value,
|
||||||
|
category: $('#f-category').value,
|
||||||
};
|
};
|
||||||
if(editingId){
|
if(editingId){
|
||||||
const t = data.tasks.find(x=>x.id===editingId);
|
const t = data.tasks.find(x=>x.id===editingId);
|
||||||
|
|||||||
68
index.html
68
index.html
@ -120,6 +120,27 @@ window.addEventListener('error',function(e){
|
|||||||
<label class="block text-sm font-semibold mb-1">Время</label>
|
<label class="block text-sm font-semibold mb-1">Время</label>
|
||||||
<input id="f-time" type="time" class="field">
|
<input id="f-time" type="time" class="field">
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">Длительность</label>
|
||||||
|
<select id="f-duration" class="field">
|
||||||
|
<option value="">— не указана —</option>
|
||||||
|
<option value="15">15 мин</option>
|
||||||
|
<option value="30">30 мин</option>
|
||||||
|
<option value="45">45 мин</option>
|
||||||
|
<option value="60">1 ч</option>
|
||||||
|
<option value="90">1.5 ч</option>
|
||||||
|
<option value="120">2 ч</option>
|
||||||
|
<option value="180">3 ч</option>
|
||||||
|
<option value="240">4 ч</option>
|
||||||
|
<option value="360">6 ч</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">Вид</label>
|
||||||
|
<select id="f-category" class="field"></select>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold mb-1">Важность</label>
|
<label class="block text-sm font-semibold mb-1">Важность</label>
|
||||||
<select id="f-priority" class="field">
|
<select id="f-priority" class="field">
|
||||||
@ -185,6 +206,53 @@ window.addEventListener('error',function(e){
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модалка личного события (расписание) -->
|
||||||
|
<div id="event-modal" class="hidden fixed inset-0 z-40 bg-black/50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||||||
|
<div class="bg-white w-full sm:max-w-lg sm:rounded-2xl rounded-t-2xl shadow-2xl max-h-[92vh] overflow-auto">
|
||||||
|
<div class="sticky top-0 bg-white border-b border-line px-5 py-3 flex items-center justify-between">
|
||||||
|
<h3 id="ev-title" class="font-bold text-lg">Событие в расписании</h3>
|
||||||
|
<button id="ev-close" class="text-2xl leading-none text-muted hover:text-ink px-2">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">Что и где *</label>
|
||||||
|
<input id="ev-name" class="field" placeholder="Напр. Школа, Бассейн, Работа">
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">Дата *</label>
|
||||||
|
<input id="ev-date" type="date" class="field">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">Чьё расписание</label>
|
||||||
|
<select id="ev-owner" class="field"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">С *</label>
|
||||||
|
<input id="ev-start" type="time" class="field">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">До *</label>
|
||||||
|
<input id="ev-end" type="time" class="field">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold mb-1">Вид / цвет</label>
|
||||||
|
<select id="ev-cat" class="field"></select>
|
||||||
|
</div>
|
||||||
|
<div class="bg-line rounded-lg p-3 text-xs text-muted">
|
||||||
|
Личные события помогают планировать день: школа, работа, тренировки. Они не зависят от задач и видны в расписании.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sticky bottom-0 bg-white border-t border-line px-5 py-3 flex gap-2">
|
||||||
|
<button id="ev-delete" class="hidden btn-danger">Удалить</button>
|
||||||
|
<button id="ev-save" class="btn-primary flex-1">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Тост -->
|
<!-- Тост -->
|
||||||
<div id="toast" class="hidden fixed bottom-5 left-1/2 -translate-x-1/2 z-50 bg-ink text-white px-5 py-3 rounded-full shadow-2xl text-sm font-semibold"></div>
|
<div id="toast" class="hidden fixed bottom-5 left-1/2 -translate-x-1/2 z-50 bg-ink text-white px-5 py-3 rounded-full shadow-2xl text-sm font-semibold"></div>
|
||||||
|
|
||||||
|
|||||||
14
style.css
14
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{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.off{background:#fafbfc;color:#c2c8d2}
|
||||||
.cal-day.today{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-50) inset}
|
.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-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}
|
.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}
|
.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{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:hover{background:var(--line)}
|
||||||
.menu-row.active{background:var(--accent-50)}
|
.menu-row.active{background:var(--accent-50)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user