Added meal planner, pantry and shopping list
This commit is contained in:
237
js/views/Shopping.js
Normal file
237
js/views/Shopping.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
addFreeformLine,
|
||||
addFreeformList,
|
||||
categoryLabel,
|
||||
deleteList,
|
||||
getActiveList,
|
||||
getListSummaries,
|
||||
KITCHEN_LIST_ID,
|
||||
removeItemFromList,
|
||||
setActiveListId,
|
||||
toggleItemInList,
|
||||
} from '../services/pantryShopping.js';
|
||||
import { showAppToast } from '../ui/toast.js';
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export function getShoppingHTML() {
|
||||
return `
|
||||
<div id="shopping-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-gray-50 z-10 pb-24">
|
||||
<div class="shrink-0 bg-white border-b border-gray-200 mt-3 px-4 pt-2 pb-3 space-y-3">
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="sr-only" for="shopping-list-select">Aktywna lista</label>
|
||||
<select id="shopping-list-select" class="flex-1 min-w-0 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2.5 text-sm font-medium text-gray-900 outline-none focus:border-gray-400"></select>
|
||||
<button type="button" id="shopping-new-list" class="shrink-0 w-10 h-10 rounded-xl border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 flex items-center justify-center" title="Nowa lista dowolna" aria-label="Nowa lista dowolna">
|
||||
<i class="fas fa-plus text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" id="shopping-delete-list" class="hidden w-full py-2 rounded-lg text-xs font-medium text-red-600 hover:bg-red-50 transition-colors">
|
||||
Usuń tę listę (nie dotyczy listy kuchennej)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="shopping-freeform-add" class="hidden shrink-0 px-4 pt-3 pb-2 space-y-2 border-b border-gray-100">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="shopping-freeform-input" class="flex-1 rounded-xl border border-gray-200 px-3 py-2.5 text-sm outline-none focus:border-gray-400" placeholder="Co kupić?" maxlength="200" />
|
||||
<button type="button" id="shopping-freeform-submit" class="shrink-0 px-4 py-2.5 rounded-xl bg-gray-900 text-white text-xs font-semibold hover:bg-black">Dodaj</button>
|
||||
</div>
|
||||
<input type="text" id="shopping-freeform-note" class="w-full rounded-lg border border-gray-100 px-3 py-2 text-xs text-gray-600 outline-none focus:border-gray-300" placeholder="Opcjonalna notatka (ilość, sklep…)" maxlength="120" />
|
||||
</div>
|
||||
|
||||
<div id="shopping-scroll" class="flex-1 overflow-y-auto px-4 py-3 pb-4 no-scrollbar">
|
||||
<div id="shopping-list-root" class="space-y-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function syncListSelect() {
|
||||
const sel = document.getElementById('shopping-list-select');
|
||||
if (!sel) return;
|
||||
const { lists, activeListId } = getListSummaries();
|
||||
sel.innerHTML = lists.map((l) => {
|
||||
const suffix = l.openCount ? ` (${l.openCount})` : '';
|
||||
const label = `${l.name}${suffix}`;
|
||||
return `<option value="${escapeHtml(l.id)}">${escapeHtml(label)}</option>`;
|
||||
}).join('');
|
||||
sel.value = activeListId;
|
||||
}
|
||||
|
||||
function syncChromeForList() {
|
||||
const list = getActiveList();
|
||||
const isKitchen = list.type === 'kitchen';
|
||||
const delBtn = document.getElementById('shopping-delete-list');
|
||||
const ffAdd = document.getElementById('shopping-freeform-add');
|
||||
|
||||
if (ffAdd) ffAdd.classList.toggle('hidden', isKitchen);
|
||||
|
||||
if (delBtn) {
|
||||
delBtn.classList.toggle('hidden', isKitchen);
|
||||
}
|
||||
}
|
||||
|
||||
function renderKitchenItems() {
|
||||
const root = document.getElementById('shopping-list-root');
|
||||
if (!root) return;
|
||||
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'kitchen') return;
|
||||
|
||||
const items = list.items;
|
||||
if (items.length === 0) {
|
||||
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Brak pozycji.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = {};
|
||||
for (const it of items) {
|
||||
const c = it.category || 'inne';
|
||||
if (!groups[c]) groups[c] = [];
|
||||
groups[c].push(it);
|
||||
}
|
||||
|
||||
root.innerHTML = Object.keys(groups)
|
||||
.sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b)))
|
||||
.map((cat) => `
|
||||
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
|
||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wide px-3 py-2 bg-gray-50 border-b border-gray-100">${escapeHtml(categoryLabel(cat))}</p>
|
||||
<ul class="divide-y divide-gray-100">
|
||||
${groups[cat].map((it) => `
|
||||
<li class="flex items-start gap-3 px-3 py-3 ${it.checked ? 'opacity-60' : ''}">
|
||||
<button type="button" data-shop-toggle="${escapeHtml(it.id)}" class="mt-0.5 w-5 h-5 rounded border shrink-0 flex items-center justify-center transition-colors ${it.checked ? 'bg-gray-900 border-gray-900 text-white' : 'border-gray-300 bg-white'}" aria-label="Kupione">
|
||||
${it.checked ? '<i class="fas fa-check text-[10px]"></i>' : ''}
|
||||
</button>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 ${it.checked ? 'line-through text-gray-500' : ''}">${escapeHtml(it.name)}</p>
|
||||
<p class="text-xs text-gray-600 tabular-nums mt-0.5">${escapeHtml(String(it.amount))} ${escapeHtml(it.unit)}</p>
|
||||
${it.sourceNote ? `<p class="text-[10px] text-gray-400 mt-1">${escapeHtml(it.sourceNote)}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" data-shop-remove="${escapeHtml(it.id)}" class="shrink-0 w-8 h-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" aria-label="Usuń">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
bindItemButtons(list.id);
|
||||
}
|
||||
|
||||
function renderFreeformItems() {
|
||||
const root = document.getElementById('shopping-list-root');
|
||||
if (!root) return;
|
||||
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'freeform') return;
|
||||
|
||||
const items = list.items;
|
||||
if (items.length === 0) {
|
||||
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Dodaj dowolny tekst powyżej — bez powiązania z katalogiem składników.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
|
||||
<ul class="divide-y divide-gray-100">
|
||||
${items.map((it) => `
|
||||
<li class="flex items-start gap-3 px-3 py-3 ${it.checked ? 'opacity-60' : ''}">
|
||||
<button type="button" data-shop-toggle="${escapeHtml(it.id)}" class="mt-0.5 w-5 h-5 rounded border shrink-0 flex items-center justify-center transition-colors ${it.checked ? 'bg-gray-900 border-gray-900 text-white' : 'border-gray-300 bg-white'}">
|
||||
${it.checked ? '<i class="fas fa-check text-[10px]"></i>' : ''}
|
||||
</button>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 ${it.checked ? 'line-through text-gray-500' : ''}">${escapeHtml(it.text)}</p>
|
||||
${it.note ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(it.note)}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" data-shop-remove="${escapeHtml(it.id)}" class="shrink-0 w-8 h-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" aria-label="Usuń">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</li>`).join('')}
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
bindItemButtons(list.id);
|
||||
}
|
||||
|
||||
function bindItemButtons(listId) {
|
||||
const root = document.getElementById('shopping-list-root');
|
||||
if (!root) return;
|
||||
|
||||
root.querySelectorAll('[data-shop-toggle]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-shop-toggle');
|
||||
if (id) toggleItemInList(listId, id);
|
||||
refreshShopping();
|
||||
});
|
||||
});
|
||||
root.querySelectorAll('[data-shop-remove]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-shop-remove');
|
||||
if (id) removeItemFromList(listId, id);
|
||||
refreshShopping();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshShopping() {
|
||||
syncListSelect();
|
||||
syncChromeForList();
|
||||
const list = getActiveList();
|
||||
if (list.type === 'kitchen') renderKitchenItems();
|
||||
else renderFreeformItems();
|
||||
}
|
||||
|
||||
export function setupShopping() {
|
||||
const sel = document.getElementById('shopping-list-select');
|
||||
sel?.addEventListener('change', () => {
|
||||
const v = sel.value;
|
||||
if (v) setActiveListId(v);
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
document.getElementById('shopping-new-list')?.addEventListener('click', () => {
|
||||
const name = window.prompt('Nazwa nowej listy (dowolne zakupy):', 'Nowa lista');
|
||||
if (name === null) return;
|
||||
addFreeformList(name);
|
||||
showAppToast('Utworzono listę.');
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
document.getElementById('shopping-delete-list')?.addEventListener('click', () => {
|
||||
const list = getActiveList();
|
||||
if (list.id === KITCHEN_LIST_ID) return;
|
||||
if (!window.confirm(`Usunąć listę „${list.name}”?`)) return;
|
||||
deleteList(list.id);
|
||||
showAppToast('Lista usunięta.');
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
const submitFreeform = () => {
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'freeform') return;
|
||||
const input = document.getElementById('shopping-freeform-input');
|
||||
const note = document.getElementById('shopping-freeform-note');
|
||||
const text = input?.value || '';
|
||||
addFreeformLine(list.id, text, note?.value || '');
|
||||
if (input) input.value = '';
|
||||
if (note) note.value = '';
|
||||
refreshShopping();
|
||||
};
|
||||
|
||||
document.getElementById('shopping-freeform-submit')?.addEventListener('click', submitFreeform);
|
||||
document.getElementById('shopping-freeform-input')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
submitFreeform();
|
||||
}
|
||||
});
|
||||
|
||||
refreshShopping();
|
||||
window.refreshShopping = refreshShopping;
|
||||
}
|
||||
Reference in New Issue
Block a user