This commit is contained in:
Mateusz Gruszczyński
2026-03-31 18:18:04 +02:00
parent bd83211f38
commit 2b9a305db7
4 changed files with 0 additions and 6842 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,510 +0,0 @@
function updateItemState(itemId, isChecked) {
const checkbox = document.querySelector(`#item-${itemId} input[type='checkbox']`);
if (checkbox) {
checkbox.checked = isChecked;
checkbox.disabled = false;
const li = checkbox.closest('li');
li.classList.remove('opacity-50', 'is-pending', 'bg-light', 'text-dark', 'bg-success', 'text-white', 'bg-warning', 'item-not-checked');
if (isChecked) {
li.classList.add('bg-success', 'text-white');
} else {
li.classList.add('item-not-checked');
}
li.querySelectorAll('.shopping-item-spinner, .spinner-border').forEach(sp => sp.remove());
}
updateProgressBar();
applyHidePurchased();
}
function updateProgressBar() {
const barPurchased = document.getElementById('progress-bar-purchased');
const barNotPurchased = document.getElementById('progress-bar-not-purchased');
const barRemaining = document.getElementById('progress-bar-remaining');
const progressLabel = document.getElementById('progress-label');
const percentValueEl = document.getElementById('percent-value');
if (!barPurchased || !barNotPurchased || !barRemaining || !progressLabel) {
return;
}
const items = document.querySelectorAll('#items li');
const total = items.length;
const purchased = Array.from(items).filter(li => li.classList.contains('bg-success')).length;
const notPurchased = Array.from(items).filter(li => li.classList.contains('bg-warning')).length;
const remaining = total - purchased - notPurchased;
const percentPurchased = total > 0 ? (purchased / total) * 100 : 0;
const percentNotPurchased = total > 0 ? (notPurchased / total) * 100 : 0;
const percentRemaining = total > 0 ? (remaining / total) * 100 : 0;
const percent = total > 0 ? Math.round((purchased / total) * 100) : 0;
barPurchased.style.width = `${percentPurchased}%`;
barNotPurchased.style.width = `${percentNotPurchased}%`;
barRemaining.style.width = `${percentRemaining}%`;
progressLabel.textContent = `${percent}%`;
progressLabel.classList.toggle('text-white', percent < 51);
progressLabel.classList.toggle('text-dark', percent >= 51);
const purchasedCountEl = document.getElementById('purchased-count');
const totalCountEl = document.getElementById('total-count');
if (purchasedCountEl) purchasedCountEl.textContent = purchased;
if (totalCountEl) totalCountEl.textContent = total;
if (percentValueEl) percentValueEl.textContent = percent;
}
function addItem(listId) {
const name = document.getElementById('newItem').value;
const quantityInput = document.getElementById('newQuantity');
let quantity = 1;
if (quantityInput) {
quantity = parseInt(quantityInput.value);
if (isNaN(quantity) || quantity < 1) {
quantity = 1;
}
}
if (name.trim() === '') return;
socket.emit('add_item', { list_id: listId, name: name, quantity: quantity });
document.getElementById('newItem').value = '';
if (quantityInput) quantityInput.value = 1;
document.getElementById('newItem').focus();
}
function deleteItem(id) {
if (confirm('Na pewno usunąć produkt?')) {
socket.emit('delete_item', { item_id: id });
}
}
function editItem(id, oldName, oldQuantity) {
const finalName = String(oldName ?? '').trim();
let newQuantity = parseInt(oldQuantity, 10);
if (!finalName) {
showToast('Nazwa produktu nie może być pusta.', 'warning');
return;
}
if (isNaN(newQuantity) || newQuantity < 1) {
newQuantity = 1;
}
socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity });
}
function openEditItemModal(event, id, oldName, oldQuantity) {
if (event && typeof event.stopPropagation === 'function') {
event.stopPropagation();
}
const modalEl = document.getElementById('editItemModal');
const idInput = document.getElementById('editItemId');
const nameInput = document.getElementById('editItemName');
const quantityInput = document.getElementById('editItemQuantity');
if (!modalEl || !idInput || !nameInput || !quantityInput || typeof bootstrap === 'undefined') {
editItem(id, oldName, oldQuantity);
return;
}
idInput.value = id;
nameInput.value = String(oldName ?? '').trim();
const parsedQuantity = parseInt(oldQuantity, 10);
quantityInput.value = !isNaN(parsedQuantity) && parsedQuantity > 0 ? parsedQuantity : 1;
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
setTimeout(() => {
nameInput.focus();
nameInput.select();
}, 150);
}
function submitExpense(listId) {
const amountInput = document.getElementById('expenseAmount');
const amount = parseFloat(amountInput.value);
if (isNaN(amount) || amount <= 0) {
showToast('Podaj poprawną kwotę!');
return;
}
socket.emit('add_expense', {
list_id: listId,
amount: amount
});
amountInput.value = '';
}
function copyLink(link) {
if (navigator.share) {
navigator.share({
title: 'Udostępnij link',
text: 'Udostępniam link do listy:',
url: link
}).then(() => {
showToast('Link udostępniony!');
}).catch((err) => {
tryClipboard(link);
});
return;
}
tryClipboard(link);
}
function tryClipboard(link) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(link).then(() => {
showToast('Link skopiowany do schowka!');
}).catch((err) => {
console.error('Błąd clipboard API:', err);
fallbackCopyText(link);
});
} else {
fallbackCopyText(link);
}
}
function fallbackCopyText(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = 0;
textarea.style.left = 0;
textarea.style.opacity = 0;
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showToast('Link skopiowany do schowka!');
} else {
showToast('Nie udało się skopiować linku', 'warning');
}
} catch (err) {
console.error('Fallback błąd kopiowania:', err);
showToast('Nie udało się skopiować linku', 'warning');
}
document.body.removeChild(textarea);
}
function openList(link) {
window.open(link, '_blank');
}
function applyHidePurchased(isInit = false) {
const toggle = document.getElementById('hidePurchasedToggle');
if (!toggle) return;
const hide = toggle.checked;
const items = document.querySelectorAll('#items li');
items.forEach(li => {
const isCheckedItem =
li.classList.contains('bg-success') || // kupione
li.classList.contains('bg-warning'); // niekupione
if (isCheckedItem) {
if (hide) {
if (isInit) {
// Jeśli inicjalizacja: od razu ukryj
li.classList.add('hide-purchased');
li.classList.remove('fade-out');
} else {
// Z animacją
li.classList.add('fade-out');
setTimeout(() => {
li.classList.add('hide-purchased');
}, 700);
}
} else {
// Odsłanianie
li.classList.remove('hide-purchased');
setTimeout(() => {
li.classList.remove('fade-out');
}, 10);
}
} else {
// Element nieoznaczony — zawsze pokazany
li.classList.remove('hide-purchased', 'fade-out');
}
});
}
function formatShareUrlPreview(url) {
return String(url || '').replace(/^https?:\/\//, '');
}
function setVisibilityBadgeState(el, isPublic, publicLabel = '🌍 Publiczna', privateLabel = '🔒 Prywatna') {
if (!el) return;
el.classList.remove('share-state-badge--public', 'share-state-badge--private');
el.classList.add(isPublic ? 'share-state-badge--public' : 'share-state-badge--private');
el.textContent = isPublic ? publicLabel : privateLabel;
}
function updateShareVisibilityUI(data) {
const shareUrl = data?.share_url || '';
const isPublic = !!data?.is_public;
const shareUrlInput = document.getElementById('shareUrlInput');
const shareUrlPreview = document.getElementById('shareUrlPreview');
const copyBtn = document.getElementById('copyBtn');
const toggleBtn = document.getElementById('toggleVisibilityBtn');
const mainNote = document.getElementById('shareVisibilityNote');
const sheetNote = document.getElementById('shareSheetVisibilityNote');
const mainOpenBtn = document.getElementById('openShareModeBtn');
const sheetOpenBtn = document.getElementById('openShareModeBtnSheet');
if (shareUrlInput) shareUrlInput.value = shareUrl;
if (shareUrlPreview) shareUrlPreview.textContent = formatShareUrlPreview(shareUrl);
if (copyBtn) copyBtn.disabled = false;
if (mainOpenBtn) mainOpenBtn.href = shareUrl;
if (sheetOpenBtn) sheetOpenBtn.href = shareUrl;
setVisibilityBadgeState(document.getElementById('shareVisibilityBadge'), isPublic);
setVisibilityBadgeState(document.getElementById('shareSheetVisibilityBadge'), isPublic, 'Publiczna', 'Prywatna');
if (mainNote) {
mainNote.textContent = isPublic
? 'Lista działa publicznie i przez link udostępniania.'
: 'Lista działa przez link udostępniania i dla zaproszonych osób.';
}
if (sheetNote) {
sheetNote.textContent = isPublic
? 'Lista jest widoczna publicznie i nadal działa przez link.'
: 'Lista nie jest publiczna, ale nadal działa przez link i dla zaproszonych osób.';
}
if (toggleBtn) {
toggleBtn.innerHTML = isPublic ? '🙈 Ustaw jako prywatną' : '🌍 Uczyń publiczną';
}
}
function toggleVisibility(listId) {
fetch('/toggle_visibility/' + listId, { method: 'POST' })
.then(response => response.json())
.then(data => {
updateShareVisibilityUI(data);
showToast(data.is_public ? 'Lista jest teraz publiczna.' : 'Lista jest teraz prywatna.', 'success');
})
.catch(() => {
showToast('Nie udało się zmienić widoczności listy.', 'danger');
});
}
function markNotPurchasedModal(e, id) {
e.stopPropagation();
const reason = prompt("Podaj powód oznaczenia jako niekupione:");
if (reason !== null) {
socket.emit('mark_not_purchased', { item_id: id, reason: reason });
}
}
function showToast(message, type = 'primary') {
const toastContainer = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0 show`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div></div>`;
toastContainer.appendChild(toast);
setTimeout(() => { toast.remove(); }, 1750);
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function isListDifferent(oldItems, newItems) {
if (oldItems.length !== newItems.length) return true;
const oldIds = Array.from(oldItems).map(li => parseInt(li.id.replace('item-', ''), 10)).sort();
const newIds = newItems.map(i => i.id).sort();
for (let i = 0; i < newIds.length; i++) {
if (oldIds[i] !== newIds[i]) {
return true;
}
}
return false;
}
function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = false) {
const options = (typeof optionsOrShowEditOnly === 'object' && optionsOrShowEditOnly !== null)
? optionsOrShowEditOnly
: { showEditOnly: !!optionsOrShowEditOnly };
const showEditOnly = !!options.showEditOnly;
const temporaryShareUndo = !!options.temporaryShareUndo;
const countdownSeconds = Math.max(0, parseInt(options.countdownSeconds, 10) || 15);
const li = document.createElement('li');
li.id = `item-${item.id}`;
li.dataset.name = String(item.name || '').toLowerCase();
li.dataset.isShare = isShare ? 'true' : 'false';
li.className = `list-group-item shopping-item-row clickable-item ${item.purchased ? 'bg-success text-white'
: item.not_purchased ? 'bg-warning text-dark'
: 'item-not-checked'
}`;
const isOwner = window.IS_OWNER === true || window.IS_OWNER === 'true';
const isArchived = window.IS_ARCHIVED === true || window.IS_ARCHIVED === 'true';
const safeName = escapeHtml(item.name || '');
const nameForEdit = JSON.stringify(String(item.name || ''));
const quantity = Number.isInteger(item.quantity) ? item.quantity : parseInt(item.quantity, 10) || 1;
const quantityBadge = quantity > 1
? `<span class="badge rounded-pill bg-secondary">x${quantity}</span>`
: '';
const canEditListItem = !isShare;
const canShowShareActions = isShare && !showEditOnly && !temporaryShareUndo;
const canMarkNotPurchased = !item.not_purchased && !isArchived;
const checkboxHtml = `<input id="checkbox-${item.id}" class="large-checkbox" type="checkbox" ${item.purchased ? 'checked' : ''} ${(item.not_purchased || isArchived) ? 'disabled' : ''}>`;
const infoParts = [];
if (item.note) {
infoParts.push(`<span class="text-danger">[ <b>${escapeHtml(item.note)}</b> ]</span>`);
}
if (item.not_purchased_reason) {
infoParts.push(`<span class="text-dark">[ <b>Powód: ${escapeHtml(item.not_purchased_reason)}</b> ]</span>`);
}
const addedByDisplay = item.added_by_display || (isShare ? item.added_by : '');
const addedById = item.added_by_id != null ? Number(item.added_by_id) : null;
const ownerId = item.owner_id != null ? Number(item.owner_id) : null;
const shouldShowAddedBy = !!addedByDisplay && (addedById === null || ownerId === null || addedById !== ownerId);
if (shouldShowAddedBy) {
infoParts.push(`<span class="item-added-by-meta">· dodał/a: <b>${escapeHtml(addedByDisplay)}</b></span>`);
}
const infoHtml = infoParts.length
? `<span class="info-line small" id="info-${item.id}">${infoParts.join(' ')}</span>`
: '';
const iconBtn = 'btn btn-outline-light btn-sm shopping-action-btn';
const wideBtn = 'btn btn-outline-light btn-sm shopping-action-btn shopping-action-btn--wide';
let actionButtons = '';
if (canEditListItem) {
const dragHandleButton = window.isSorting
? `<button type="button" class="${iconBtn} drag-handle" title="Przesuń produkt" aria-label="Przesuń produkt" ${isArchived ? 'disabled' : ''}>☰</button>`
: '';
actionButtons += `
${dragHandleButton}
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${JSON.stringify(String(item.name || ''))}, ${quantity})'`}>✏️</button>
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="deleteItem(${item.id})"`}>🗑️</button>`;
}
if (item.not_purchased) {
actionButtons += `
<button type="button" class="${wideBtn}" ${isArchived ? 'disabled' : `onclick="unmarkNotPurchased(${item.id})"`}>Przywróć</button>`;
} else if (!isShare || canShowShareActions || isOwner) {
actionButtons += `
<button type="button" class="${iconBtn}" ${canMarkNotPurchased ? `onclick="markNotPurchasedModal(event, ${item.id})"` : 'disabled'}>⚠️</button>`;
}
if (temporaryShareUndo) {
actionButtons += `
<button type="button" class="${iconBtn} shopping-action-btn--countdown" disabled data-countdown-for="${item.id}">${countdownSeconds}s</button>
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${nameForEdit}, ${quantity})'`}>✏️</button>
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="deleteItem(${item.id})"`}>🗑️</button>`;
} else if (canShowShareActions || (!isShare && isOwner)) {
actionButtons += `
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="openNoteModal(event, ${item.id})"`}>📝</button>`;
}
li.innerHTML = `
<div class="shopping-item-main">
${checkboxHtml}
<div class="shopping-item-content">
<div class="shopping-item-head">
<div class="shopping-item-text">
<span id="name-${item.id}" class="shopping-item-name text-white">${safeName}</span>
${quantityBadge}
${infoHtml}
</div>
<div class="list-item-actions shopping-item-actions" role="group">
${actionButtons}
</div>
</div>
</div>
</div>`;
return li;
}
function updateListSmoothly(newItems) {
const itemsContainer = document.getElementById('items');
const existingItemsMap = new Map();
Array.from(itemsContainer.querySelectorAll('li')).forEach(li => {
const id = parseInt(li.id.replace('item-', ''), 10);
existingItemsMap.set(id, li);
});
const fragment = document.createDocumentFragment();
newItems.forEach(item => {
const li = renderItem(item);
fragment.appendChild(li);
});
itemsContainer.innerHTML = '';
itemsContainer.appendChild(fragment);
updateProgressBar();
toggleEmptyPlaceholder();
applyHidePurchased();
}
document.addEventListener("DOMContentLoaded", function () {
const receiptSection = document.getElementById("receiptSection");
const toggleBtn = document.querySelector('[data-bs-target="#receiptSection"]');
if (!receiptSection || !toggleBtn) return;
if (localStorage.getItem("receiptSectionOpen") === "true") {
new bootstrap.Collapse(receiptSection, { toggle: true });
}
receiptSection.addEventListener('shown.bs.collapse', function () {
localStorage.setItem("receiptSectionOpen", "true");
});
receiptSection.addEventListener('hidden.bs.collapse', function () {
localStorage.setItem("receiptSectionOpen", "false");
});
});
document.addEventListener("DOMContentLoaded", function () {
const toggle = document.getElementById('hidePurchasedToggle');
if (!toggle) return;
const savedState = localStorage.getItem('hidePurchasedToggle');
toggle.checked = savedState === 'true';
applyHidePurchased(true);
toggle.addEventListener('change', function () {
localStorage.setItem('hidePurchasedToggle', toggle.checked ? 'true' : 'false');
applyHidePurchased();
});
});

View File

@@ -1,55 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
const receiptSection = document.getElementById("receiptSection");
const toggleEl = document.querySelector('[data-bs-target="#receiptSection"]');
if (!receiptSection || !toggleEl) return;
const collapse = bootstrap.Collapse.getOrCreateInstance(receiptSection, { toggle: false });
if (localStorage.getItem("receiptSectionOpen") === "true") {
collapse.show();
}
const titleEl = toggleEl.querySelector(".receipt-disclosure__title");
function updateUI() {
const isShown = receiptSection.classList.contains("show");
toggleEl.classList.toggle("is-open", isShown);
toggleEl.setAttribute("aria-expanded", isShown ? "true" : "false");
if (titleEl) {
titleEl.textContent = isShown
? "Ukryj sekcję paragonów"
: "Pokaż sekcję paragonów";
}
}
function toggleSection() {
collapse.toggle();
}
toggleEl.addEventListener("click", function (event) {
event.preventDefault();
toggleSection();
});
toggleEl.addEventListener("keydown", function (event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
toggleSection();
}
});
receiptSection.addEventListener("shown.bs.collapse", function () {
localStorage.setItem("receiptSectionOpen", "true");
updateUI();
});
receiptSection.addEventListener("hidden.bs.collapse", function () {
localStorage.setItem("receiptSectionOpen", "false");
updateUI();
});
updateUI();
});

View File

@@ -1,346 +0,0 @@
{% extends 'base.html' %}
{% block title %}Lista: {{ list.title }}{% endblock %}
{% block content %}
<div class="list-header-toolbar d-flex justify-content-between align-items-start mb-3 flex-wrap gap-2">
<h2 class="mb-0">
🛍️ {{ list.title }}
{% if list.is_archived %}
<span class="badge rounded-pill bg-secondary ms-2">(Archiwalna)</span>
{% endif %}
{% if total_expense > 0 %}
<span id="total-expense1" class="badge rounded-pill bg-success ms-2">
💸 {{ '%.2f'|format(total_expense) }} PLN
</span>
{% else %}
<span id="total-expense" class="badge rounded-pill bg-secondary ms-2" style="display: none;">
💸 0.00 PLN
</span>
{% endif %}
{% if list.category_badges %}
{% for cat in list.category_badges %}
<span class="badge rounded-pill rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
font-size: 0.75rem;
opacity: 0.85;">
{{ cat.name }}
</span>
{% endfor %}
{% endif %}
</h2>
<div class="list-toolbar list-toolbar--share d-flex justify-content-end align-items-center gap-2 share-page-toolbar">
<div class="form-check form-switch form-check-spaced app-switch hide-purchased-switch hide-purchased-switch--minimal hide-purchased-switch--right">
<input class="form-check-input" type="checkbox" id="hidePurchasedToggle">
<label class="form-check-label ms-2" for="hidePurchasedToggle">Ukryj zaznaczone</label>
</div>
</div>
</div>
<ul id="items" class="list-group mb-3" data-is-share="{{ 'true' if is_share else 'false' }}">
{% for item in items %}
<li data-name="{{ item.name|lower }}" id="item-{{ item.id }}"
class="list-group-item shopping-item-row clickable-item
{% if item.purchased %}bg-success text-white{% elif item.not_purchased %}bg-warning text-dark{% else %}item-not-checked{% endif %}">
<div class="shopping-item-main">
<input id="checkbox-{{ item.id }}" class="large-checkbox" type="checkbox" {% if item.purchased %}checked{% endif
%} {% if list.is_archived or item.not_purchased %}disabled{% endif %}>
<div class="shopping-item-content">
<div class="shopping-item-head">
<div class="shopping-item-text">
<span id="name-{{ item.id }}" class="shopping-item-name text-white">{{ item.name }}</span>
{% if item.quantity and item.quantity > 1 %}
<span class="badge rounded-pill bg-secondary">x{{ item.quantity }}</span>
{% endif %}
{% set info_parts = [] %}
{% if item.note %}{% set _ = info_parts.append('<span class="text-danger">[ <b>' ~ item.note ~ '</b> ]</span>') %}{% endif %}
{% if item.not_purchased_reason %}{% set _ = info_parts.append('<span class="text-dark">[ <b>Powód: ' ~ item.not_purchased_reason ~ '</b> ]</span>') %}{% endif %}
{% if item.added_by_display %}{% set _ = info_parts.append('<span class="item-added-by-meta">· dodał/a: <b>' ~ item.added_by_display ~ '</b></span>') %}{% endif %}
{% if info_parts %}
<span class="info-line small" id="info-{{ item.id }}">{{ info_parts | join(' ') | safe }}</span>
{% endif %}
</div>
<div class="list-item-actions shopping-item-actions" role="group">
{% if item.not_purchased %}
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn shopping-action-btn--wide" {% if list.is_archived %}disabled{% else %}
onclick="unmarkNotPurchased({{ item.id }})" {% endif %}>
Przywróć
</button>
{% else %}
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else %}
onclick="markNotPurchasedModal(event, {{ item.id }})" {% endif %}>
⚠️
</button>
{% endif %}
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else %}
onclick="openNoteModal(event, {{ item.id }})" {% endif %}>
📝
</button>
</div>
</div>
</div>
</div>
</li>
{% else %}
<li id="empty-placeholder" class="list-group-item bg-dark text-secondary text-center w-100">
Brak produktów w tej liście.
</li>
{% endfor %}
</ul>
{% if not list.is_archived %}
<div class="shopping-entry-card mb-3" aria-label="Sekcja dodawania produktu">
<div class="shopping-entry-card__label"> Dodaj produkt</div>
<div class="shopping-entry-card__hint">Wpisz nazwę produktu i ilość, potem kliknij Dodaj.</div>
<div class="input-group mb-0 shopping-compact-input-group shopping-product-input-group">
<input id="newItem" class="form-control bg-dark text-white border-secondary shopping-product-name-input" placeholder="Dodaj produkt" {% if
not current_user.is_authenticated %}disabled{% endif %}>
<input id="newQuantity" type="number" class="form-control bg-dark text-white border-secondary shopping-qty-input" placeholder="Ilość"
min="1" value="1" {% if not current_user.is_authenticated %}disabled{% endif %}>
<button onclick="addItem({{ list.id }})" class="btn btn-outline-success share-submit-btn shopping-compact-submit" {% if not
current_user.is_authenticated %}disabled{% endif %}><span class="shopping-btn-icon" aria-hidden="true"></span><span class="shopping-btn-label">Dodaj</span></button>
</div>
</div>
{% endif %}
{% if not list.is_archived %}
<div class="shopping-entry-card shopping-entry-card--expense mb-3" aria-label="Sekcja dodawania wydatku">
<div class="shopping-entry-card__label d-flex justify-content-between align-items-center">
<span>💰 Dodaj wydatek</span>
<span class="badge rounded-pill bg-success" id="total-expense2">
💸 Łączna suma: {{ '%.2f'|format(total_expense) }} PLN
</span>
</div>
<div class="shopping-entry-card__hint">Wpisz kwotę wydatku i kliknij Zapisz.</div>
<div class="input-group mb-0 shopping-compact-input-group shopping-expense-input-group">
<input id="expenseAmount" type="number" step="0.01" min="0"
class="form-control bg-dark text-white border-secondary shopping-expense-amount-input"
placeholder="Kwota (PLN)">
<button onclick="submitExpense({{ list.id }})"
class="btn btn-outline-primary share-submit-btn share-submit-btn--expense shopping-compact-submit">
<span class="shopping-btn-icon" aria-hidden="true">💾</span>
<span class="shopping-btn-label">Zapisz</span>
</button>
</div>
</div>
{% endif %}
<p id="total-expense2" style="display: none;">
<b>💸 Łącznie wydano:</b> {{ '%.2f'|format(total_expense) }} PLN
</p>
<div id="toggleReceiptBtn" class="receipt-disclosure mb-3" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#receiptSection" aria-expanded="false" aria-controls="receiptSection">
<div class="receipt-disclosure__content">
<div class="receipt-disclosure__icon" aria-hidden="true">🧾</div>
<div class="receipt-disclosure__text">
<div class="receipt-disclosure__eyebrow">Strefa paragonów</div>
<div class="receipt-disclosure__title">Pokaż sekcję paragonów</div>
</div>
<div class="receipt-disclosure__meta">
<span class="receipt-disclosure__count">{{ receipts|length }}</span>
<span class="receipt-disclosure__chevron" aria-hidden="true"></span>
</div>
</div>
</div>
<div class="collapse px-2 px-md-4" id="receiptSection">
{% set receipt_pattern = 'list_' ~ list.id %}
<div class="receipt-section-stack d-flex flex-column gap-3 mt-3">
<div class="card bg-dark text-white border-secondary shadow-sm">
<div class="card-body">
<div class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-2 mb-3">
<div>
<h5 class="mb-1">📄 Paragony</h5>
<p class="text-secondary small mb-0">
Przeglądaj dodane paragony, wrzucaj nowe i rozliczaj je przez OCR.
</p>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill bg-secondary">{{ receipts|length }} plik{% if receipts|length != 1 %}i{% endif %}</span>
{% if list.is_archived %}
<span class="badge rounded-pill bg-secondary">Lista archiwalna</span>
{% elif current_user.is_authenticated %}
<span class="badge rounded-pill bg-success">Możesz dodawać</span>
{% else %}
<span class="badge rounded-pill bg-warning text-dark">Tylko podgląd</span>
{% endif %}
</div>
</div>
<div class="row g-3 align-items-stretch">
<div class="col-12 col-xl-7">
<div class="border border-secondary rounded-3 p-3 h-100">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<h6 class="mb-0">📸 Dodane paragony</h6>
{% if receipts %}
<span class="text-secondary small">Kliknij miniaturę, aby otworzyć podgląd</span>
{% endif %}
</div>
<div class="row g-3" id="receiptGallery">
{% if receipts %}
{% for r in receipts %}
<div class="col-6 col-md-4 text-center">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox text-decoration-none"
data-gallery="receipt-gallery">
<div class="card bg-black border-secondary h-100 overflow-hidden shadow-sm">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
class="card-img-top img-fluid" style="height: 180px; object-fit: cover;" alt="Paragon {{ loop.index }}">
<div class="card-body p-2">
<div class="small text-truncate text-secondary">Paragon {{ loop.index }}</div>
</div>
</div>
</a>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="alert alert-info text-center mb-0" role="alert">
Brak wgranych paragonów do tej listy
</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="d-flex flex-column gap-3 h-100">
<div class="border border-secondary rounded-3 p-3 bg-black bg-opacity-25 {% if not receipts %}d-none{% endif %}" id="receiptAnalysisBlock">
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap mb-2">
<div>
<h6 class="mb-1">🔍 Analiza paragonów (OCR)</h6>
<p class="text-small text-secondary mb-0">
System spróbuje automatycznie rozpoznać kwoty. Sprawdź wynik i kliknij „Dodaj”, aby dopisać wydatek.
</p>
</div>
</div>
{% if current_user.is_authenticated %}
<button id="analyzeBtn" class="btn btn-sm btn-outline-light mb-3 w-100 w-sm-auto">
🔍 Zleć analizę OCR
</button>
{% else %}
<div class="alert alert-warning mb-3">
⚠️ Tylko zalogowani użytkownicy mogą zlecać analizę OCR.
</div>
{% endif %}
<div id="analysisResults" class="mt-2"></div>
</div>
{% if not list.is_archived and current_user.is_authenticated %}
<div class="border border-secondary rounded-3 p-3 h-100">
<h6 class="mb-1">📤 Dodaj nowy paragon</h6>
<p class="text-secondary small mb-3">Możesz dodać zdjęcie z aparatu, z galerii albo plik PDF.</p>
<form id="receiptForm" action="{{ url_for('upload_receipt', list_id=list.id) }}" method="post"
enctype="multipart/form-data" class="text-center">
<div class="d-grid gap-2">
<label for="cameraInput" id="cameraBtn"
class="btn btn-outline-light w-100 py-2 d-flex align-items-center justify-content-center gap-2">
<i class="bi bi-camera"></i> 📸 Zrób zdjęcie
</label>
<input type="file" name="receipt" accept="image/*" capture="environment" class="d-none" id="cameraInput">
<label for="galleryInput" id="galleryBtn"
class="btn btn-outline-light w-100 py-2 d-flex align-items-center justify-content-center gap-2">
<i class="bi bi-image"></i> <span id="galleryBtnText">🖼️ Z galerii</span>
</label>
<input type="file" name="receipt" accept="image/*" class="d-none" id="galleryInput">
<label for="pdfInput" id="pdfBtn"
class="btn btn-outline-light w-100 py-2 d-flex align-items-center justify-content-center gap-2">
📄 Dodaj PDF
</label>
<input type="file" name="receipt" accept="application/pdf" class="d-none" id="pdfInput">
</div>
<div id="progressContainer" class="progress progress-dark rounded-3 overflow-hidden shadow-sm mt-3"
style="height: 20px; display: none;">
<div id="progressBar" class="progress-bar bg-success fw-bold text-white text-center" role="progressbar"
style="width: 0%;">0%</div>
</div>
<div id="receiptUploadFeedback" class="mt-3"></div>
</form>
</div>
{% elif list.is_archived %}
<div class="border border-secondary rounded-3 p-3 bg-black bg-opacity-25">
<h6 class="mb-1">📤 Dodawanie zablokowane</h6>
<p class="text-secondary small mb-0">Ta lista jest archiwalna, więc nie można już dodawać nowych paragonów.</p>
</div>
{% elif not current_user.is_authenticated %}
<div class="border border-secondary rounded-3 p-3 bg-black bg-opacity-25">
<h6 class="mb-1">🔐 Dodawanie wymaga logowania</h6>
<p class="text-secondary small mb-0">Zaloguj się, aby dodawać paragony i uruchamiać analizę OCR.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal notatki -->
<div class="modal fade" id="noteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title">Dodaj notatkę</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<form id="noteForm" onsubmit="submitNote(event)">
<div class="modal-body">
<textarea id="noteText" class="form-control" rows="4" placeholder="Np. 'Promocja 2+2'"></textarea>
</div>
<div class="modal-footer">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-light btn-sm" data-bs-dismiss="modal">❌ Anuluj</button>
<button type="submit" class="btn btn-outline-light btn-sm"><span class="shopping-btn-icon" aria-hidden="true">💾</span>
<span class="shopping-btn-label">Zapisz</span></button>
</div>
</div>
</form>
</div>
</div>
</div>
{% block scripts %}
<script>
const isShare = document.getElementById('items').dataset.isShare === 'true';
window.IS_SHARE = isShare;
window.LIST_ID = {{ list.id }};
window.IS_ARCHIVED = {{ 'true' if list.is_archived else 'false' }};
window.IS_OWNER = {{ 'true' if (current_user.is_authenticated and list.user_id == current_user.id) else 'false' }};
if (typeof isSorting === 'undefined') {
var isSorting = false;
}
</script>
<script src="{{ static_asset_url('static_bp.serve_js_lib', 'Sortable.min.js') }}"></script>
<script src="{{ static_asset_url('static_bp.serve_js', 'notes.js') }}"></script>
<script src="{{ static_asset_url('static_bp.serve_js', 'clickable_row.js') }}"></script>
<script src="{{ static_asset_url('static_bp.serve_js', 'receipt_section.js') }}"></script>
<script src="{{ static_asset_url('static_bp.serve_js', 'receipt_upload.js') }}"></script>
<script src="{{ static_asset_url('static_bp.serve_js', 'receipt_analysis.js') }}"></script>
<script>
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
</script>
{% endblock %}
{% endblock %}