2 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
f02d3b8085 fixes more 2026-03-17 13:06:31 +01:00
Mateusz Gruszczyński
3347df1911 fixes 2026-03-17 12:55:59 +01:00
8 changed files with 477 additions and 113 deletions

View File

@@ -233,7 +233,7 @@ textarea.form-control:disabled {
border-bottom-left-radius: 0;
}
input.form-control {
.create-list-input-group > input.form-control {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
@@ -1473,7 +1473,7 @@ h1, h2, h3, h4, h5, h6 {
#total-expense1,
#total-expense2,
#total-expense {
background: rgba(255,255,255,0.08) !important;
background: transparent;
color: #dfffea !important;
}
@@ -4885,6 +4885,14 @@ body.sorting-active .shopping-item-row .large-checkbox {
padding: 0 .8rem !important;
flex: 0 0 auto;
}
.shopping-action-btn--countdown {
width: auto !important;
min-width: 3.2rem !important;
padding: 0 .65rem !important;
font-variant-numeric: tabular-nums;
opacity: 1 !important;
}
.endpoint-list_share .shopping-item-actions,
.endpoint-shared_list .shopping-item-actions,
@@ -4907,6 +4915,13 @@ body.sorting-active .shopping-item-row .large-checkbox {
width: auto;
min-width: 5.9rem;
}
.endpoint-list_share .shopping-action-btn--countdown,
.endpoint-shared_list .shopping-action-btn--countdown,
.endpoint-list .shopping-action-btn--countdown {
width: auto;
min-width: 3.2rem;
}
@media (max-width: 575.98px) {
.shopping-item-spinner {
@@ -4933,6 +4948,14 @@ body.sorting-active .shopping-item-row .large-checkbox {
padding: 0 .72rem !important;
}
}
.shopping-action-btn--countdown,
.endpoint-list_share .shopping-action-btn--countdown,
.endpoint-shared_list .shopping-action-btn--countdown,
.endpoint-list .shopping-action-btn--countdown {
min-width: 3rem;
padding: 0 .55rem !important;
}
}
.endpoint-login .card .form-control,
.endpoint-system_auth .card .form-control,
@@ -5023,6 +5046,15 @@ body.sorting-active .shopping-item-row .large-checkbox {
min-width: 5.9rem !important;
padding: 0 .8rem !important;
}
.endpoint-list_share .shopping-action-btn--countdown,
.endpoint-shared_list .shopping-action-btn--countdown,
.endpoint-view_list .shopping-action-btn--countdown,
.endpoint-list .shopping-action-btn--countdown {
width: auto !important;
min-width: 3.2rem !important;
padding: 0 .65rem !important;
}
.endpoint-list_share .shopping-action-btn > *,
.endpoint-shared_list .shopping-action-btn > *,
@@ -5051,7 +5083,179 @@ body.sorting-active .shopping-item-row .large-checkbox {
padding: 0 .72rem !important;
}
}
.endpoint-list_share .shopping-action-btn--countdown,
.endpoint-shared_list .shopping-action-btn--countdown,
.endpoint-view_list .shopping-action-btn--countdown,
.endpoint-list .shopping-action-btn--countdown {
min-width: 3rem !important;
padding: 0 .55rem !important;
}
}
body:not(.sorting-active) .drag-handle {
display: none !important;
}
/* final hotfix 2026-03-17: consistent password toggle on auth/admin */
.ui-password-group {
display: flex !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
gap: 0 !important;
width: 100%;
}
.ui-password-group > .form-control {
flex: 1 1 auto !important;
width: 1% !important;
min-width: 0 !important;
max-width: none !important;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-right: 0 !important;
}
.ui-password-group > .ui-password-toggle {
appearance: none;
-webkit-appearance: none;
display: inline-flex !important;
align-items: center;
justify-content: center;
flex: 0 0 46px !important;
width: 46px !important;
min-width: 46px !important;
padding: 0 !important;
margin: 0 !important;
background: var(--dark-700) !important;
color: var(--text-strong) !important;
border: 1px solid var(--dark-300) !important;
border-left: 0 !important;
border-top-right-radius: 14px !important;
border-bottom-right-radius: 14px !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
box-shadow: none !important;
outline: none !important;
line-height: 1;
transition: background-color .18s ease, border-color .18s ease, color .18s ease, box-shadow .18s ease;
}
.ui-password-group > .ui-password-toggle:hover,
.ui-password-group > .ui-password-toggle:focus,
.ui-password-group > .ui-password-toggle:focus-visible {
background: var(--dark-800) !important;
color: #fff !important;
border-color: var(--primary) !important;
box-shadow: 0 0 0 .25rem rgba(24, 64, 118, .18) !important;
}
.ui-password-group > .ui-password-toggle.is-active {
background: #2a3550 !important;
color: #fff !important;
}
@media (max-width: 575.98px) {
.ui-password-group > .ui-password-toggle {
flex-basis: 44px !important;
width: 44px !important;
min-width: 44px !important;
}
}
/* final hotfix 2026-03-17b: password toggle parity on login/system-auth/admin-users */
.ui-password-group {
display: flex !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
width: 100% !important;
}
.ui-password-group > .form-control {
flex: 1 1 auto !important;
width: 1% !important;
min-width: 0 !important;
min-height: 42px !important;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-right: 0 !important;
}
.ui-password-group > .ui-password-toggle {
appearance: none !important;
-webkit-appearance: none !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
flex: 0 0 46px !important;
width: 46px !important;
min-width: 46px !important;
min-height: 42px !important;
padding: 0 !important;
margin: 0 !important;
cursor: pointer !important;
background-color: var(--dark-700) !important;
background-image: none !important;
color: var(--text-strong) !important;
border: 1px solid var(--dark-300) !important;
border-left: 0 !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-top-right-radius: 14px !important;
border-bottom-right-radius: 14px !important;
box-shadow: none !important;
line-height: 1 !important;
}
.ui-password-group > .ui-password-toggle:hover,
.ui-password-group > .ui-password-toggle:focus,
.ui-password-group > .ui-password-toggle:focus-visible {
background-color: var(--dark-800) !important;
color: #fff !important;
border-color: var(--primary) !important;
box-shadow: 0 0 0 .25rem rgba(24, 64, 118, .18) !important;
outline: none !important;
}
.ui-password-group > .ui-password-toggle.is-active {
background-color: var(--dark-800) !important;
color: #fff !important;
}
.ui-password-group > .ui-password-toggle > * {
pointer-events: none !important;
}
.endpoint-login .ui-password-group > .ui-password-toggle,
.endpoint-system_auth .ui-password-group > .ui-password-toggle,
.endpoint-user_management .ui-password-group > .ui-password-toggle,
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle {
background-color: var(--dark-700) !important;
color: var(--text-strong) !important;
border-color: var(--dark-300) !important;
}
.endpoint-login .ui-password-group > .ui-password-toggle:hover,
.endpoint-login .ui-password-group > .ui-password-toggle:focus,
.endpoint-login .ui-password-group > .ui-password-toggle:focus-visible,
.endpoint-system_auth .ui-password-group > .ui-password-toggle:hover,
.endpoint-system_auth .ui-password-group > .ui-password-toggle:focus,
.endpoint-system_auth .ui-password-group > .ui-password-toggle:focus-visible,
.endpoint-user_management .ui-password-group > .ui-password-toggle:hover,
.endpoint-user_management .ui-password-group > .ui-password-toggle:focus,
.endpoint-user_management .ui-password-group > .ui-password-toggle:focus-visible,
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle:hover,
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle:focus,
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle:focus-visible {
background-color: var(--dark-800) !important;
border-color: var(--primary) !important;
}
@media (max-width: 575.98px) {
.ui-password-group > .ui-password-toggle {
flex-basis: 44px !important;
width: 44px !important;
min-width: 44px !important;
}
}

View File

@@ -1,5 +1,6 @@
document.addEventListener('DOMContentLoaded', function () {
enhancePasswordFields();
observePasswordFields();
enhanceSearchableTables();
wireCopyButtons();
wireUnsavedWarnings();
@@ -8,36 +9,78 @@ document.addEventListener('DOMContentLoaded', function () {
initResponsiveCategoryBadges();
});
function enhancePasswordFields() {
document.querySelectorAll('input[type="password"]').forEach(function (input) {
if (input.dataset.uiPasswordReady === '1') return;
if (input.closest('[data-ui-skip-toggle="true"]')) return;
function initPasswordField(input) {
if (!input || input.dataset.uiPasswordReady === '1') return;
if (input.closest('[data-ui-skip-toggle="true"]')) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ui-password-toggle';
btn.setAttribute('aria-label', 'Pokaż lub ukryj hasło');
btn.textContent = '👁';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ui-password-toggle';
btn.setAttribute('aria-label', 'Pokaż lub ukryj hasło');
btn.setAttribute('aria-pressed', 'false');
btn.title = 'Pokaż hasło';
btn.innerHTML = '<span aria-hidden="true">👁</span>';
btn.addEventListener('click', function () {
const visible = input.type === 'text';
input.type = visible ? 'password' : 'text';
btn.textContent = visible ? '👁' : '🙈';
btn.classList.toggle('is-active', !visible);
});
const syncState = function () {
const visible = input.type === 'text';
btn.innerHTML = visible ? '<span aria-hidden="true">🙈</span>' : '<span aria-hidden="true">👁</span>';
btn.classList.toggle('is-active', visible);
btn.setAttribute('aria-pressed', visible ? 'true' : 'false');
btn.title = visible ? 'Ukryj hasło' : 'Pokaż hasło';
};
if (input.parentElement && input.parentElement.classList.contains('input-group')) {
input.parentElement.appendChild(btn);
} else {
const wrapper = document.createElement('div');
wrapper.className = 'input-group ui-password-group';
input.parentNode.insertBefore(wrapper, input);
wrapper.appendChild(input);
wrapper.appendChild(btn);
btn.addEventListener('click', function () {
const selectionStart = input.selectionStart;
const selectionEnd = input.selectionEnd;
input.type = input.type === 'password' ? 'text' : 'password';
syncState();
input.focus({ preventScroll: true });
if (typeof selectionStart === 'number' && typeof selectionEnd === 'number') {
try {
input.setSelectionRange(selectionStart, selectionEnd);
} catch (err) {}
}
input.dataset.uiPasswordReady = '1';
});
const parent = input.parentElement;
if (parent && parent.classList.contains('input-group')) {
parent.classList.add('ui-password-group');
if (!parent.querySelector(':scope > .ui-password-toggle')) {
parent.appendChild(btn);
}
} else {
const wrapper = document.createElement('div');
wrapper.className = 'input-group ui-password-group';
input.parentNode.insertBefore(wrapper, input);
wrapper.appendChild(input);
wrapper.appendChild(btn);
}
input.dataset.uiPasswordReady = '1';
syncState();
}
function enhancePasswordFields(root) {
const scope = root && root.querySelectorAll ? root : document;
if (scope.matches && scope.matches('input[type="password"]')) {
initPasswordField(scope);
}
scope.querySelectorAll('input[type="password"]').forEach(initPasswordField);
}
function observePasswordFields() {
if (window.__uiPasswordObserverReady) return;
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (node) {
if (!(node instanceof HTMLElement)) return;
enhancePasswordFields(node);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
window.__uiPasswordObserverReady = true;
}
function enhanceSearchableTables() {

View File

@@ -86,22 +86,51 @@ function deleteItem(id) {
}
function editItem(id, oldName, oldQuantity) {
const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName);
if (newName === null) return;
const finalName = String(oldName ?? '').trim();
let newQuantity = parseInt(oldQuantity, 10);
const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity);
if (newQuantityStr === null) return;
if (!finalName) {
showToast('Nazwa produktu nie może być pusta.', 'warning');
return;
}
const finalName = newName.trim() !== '' ? newName.trim() : oldName;
let newQuantity = parseInt(newQuantityStr);
if (isNaN(newQuantity) || newQuantity < 1) {
newQuantity = oldQuantity;
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);
@@ -282,7 +311,15 @@ function isListDifferent(oldItems, newItems) {
}
function renderItem(item, isShare = window.IS_SHARE, showEditOnly = 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();
@@ -302,7 +339,7 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
: '';
const canEditListItem = !isShare;
const canShowShareActions = isShare && !showEditOnly;
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' : ''}>`;
@@ -326,9 +363,13 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
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 += `
<button type="button" class="${iconBtn} drag-handle" title="Przesuń produkt" aria-label="Przesuń produkt" ${isArchived ? 'disabled' : ''}>☰</button>
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="editItem(${item.id}, ${nameForEdit}, ${quantity})"`}>✏️</button>
${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>`;
}
@@ -340,7 +381,12 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
<button type="button" class="${iconBtn}" ${canMarkNotPurchased ? `onclick="markNotPurchasedModal(event, ${item.id})"` : 'disabled'}>⚠️</button>`;
}
if (canShowShareActions) {
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) {
actionButtons += `
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="openNoteModal(event, ${item.id})"`}>📝</button>`;
}

View File

@@ -139,45 +139,48 @@ function setupList(listId, username) {
note: ''
};
const li = renderItem(item, window.IS_SHARE, true);
const isOwnFreshShareItem = Boolean(
window.IS_SHARE &&
data.added_by &&
window.CURRENT_LIST_USERNAME &&
String(data.added_by) === String(window.CURRENT_LIST_USERNAME)
);
const li = renderItem(
item,
window.IS_SHARE,
isOwnFreshShareItem ? { temporaryShareUndo: true, countdownSeconds: 15 } : false
);
document.getElementById('items').appendChild(li);
toggleEmptyPlaceholder();
updateProgressBar();
if (window.IS_SHARE) {
const countdownId = `countdown-${data.id}`;
const countdownBtn = document.createElement('button');
countdownBtn.type = 'button';
countdownBtn.className = 'btn btn-outline-warning';
countdownBtn.id = countdownId;
countdownBtn.disabled = true;
countdownBtn.textContent = '15s';
const btnGroup = li.querySelector('.btn-group');
if (btnGroup) {
btnGroup.prepend(countdownBtn);
}
if (isOwnFreshShareItem) {
let seconds = 15;
const intervalId = setInterval(() => {
const el = document.getElementById(countdownId);
if (el) {
seconds--;
el.textContent = `${seconds}s`;
if (seconds <= 0) {
el.remove();
clearInterval(intervalId);
}
} else {
const currentItem = document.getElementById(`item-${data.id}`);
const countdownEl = currentItem?.querySelector(`[data-countdown-for="${data.id}"]`);
if (!currentItem || !countdownEl) {
clearInterval(intervalId);
return;
}
seconds -= 1;
if (seconds <= 0) {
clearInterval(intervalId);
return;
}
countdownEl.textContent = `${seconds}s`;
}, 1000);
setTimeout(() => {
clearInterval(intervalId);
const existing = document.getElementById(`item-${data.id}`);
if (existing) {
const updated = renderItem(item, window.IS_SHARE);
existing.replaceWith(updated);
existing.replaceWith(renderItem(item, window.IS_SHARE));
}
}, 15000);
}
@@ -218,7 +221,7 @@ function setupList(listId, username) {
window.currentItems[idx].name = data.new_name;
window.currentItems[idx].quantity = data.new_quantity;
const newItem = renderItem(window.currentItems[idx], true);
const newItem = renderItem(window.currentItems[idx], window.IS_SHARE);
const oldItem = document.getElementById(`item-${data.item_id}`);
if (oldItem && newItem) {
oldItem.replaceWith(newItem);
@@ -234,6 +237,7 @@ function setupList(listId, username) {
// --- WAŻNE: zapisz dane do reconnect ---
window.LIST_ID = listId;
window.usernameForReconnect = username;
window.CURRENT_LIST_USERNAME = username;
}

View File

@@ -126,6 +126,9 @@ socket.on('full_list', function (data) {
window.currentItems = data.items;
updateListSmoothly(data.items);
if (typeof window.syncSortModeUI === 'function') {
window.syncSortModeUI();
}
toggleEmptyPlaceholder();
if (didReceiveFirstFullList && isDifferent) {

View File

@@ -1,21 +1,52 @@
let sortable = null;
let isSorting = false;
window.isSorting = false;
function syncSortModeUI() {
const active = !!window.isSorting;
const btn = document.getElementById('sort-toggle-btn');
const itemsContainer = document.getElementById('items');
document.body.classList.toggle('sorting-active', active);
if (btn) {
if (active) {
btn.textContent = '✔️ Zakończ sortowanie';
btn.classList.remove('btn-outline-warning');
btn.classList.add('btn-outline-success');
} else {
btn.textContent = '✳️ Zmień kolejność';
btn.classList.remove('btn-outline-success');
btn.classList.add('btn-outline-warning');
}
}
if (itemsContainer && window.currentItems) {
updateListSmoothly(window.currentItems);
}
document.querySelectorAll('.drag-handle').forEach(handle => {
handle.hidden = !active;
handle.setAttribute('aria-hidden', active ? 'false' : 'true');
});
}
function enableSortMode() {
if (isSorting) return;
isSorting = true;
window.isSorting = true;
if (window.isSorting) return;
const itemsContainer = document.getElementById('items');
const listId = window.LIST_ID;
if (!itemsContainer || !listId) return;
if (window.currentItems) {
updateListSmoothly(window.currentItems);
}
window.isSorting = true;
syncSortModeUI();
setTimeout(() => {
if (sortable) sortable.destroy();
if (!window.isSorting) return;
if (sortable) {
sortable.destroy();
sortable = null;
}
sortable = Sortable.create(itemsContainer, {
animation: 150,
@@ -25,7 +56,7 @@ function enableSortMode() {
preventOnFilter: false,
onEnd: () => {
const order = Array.from(itemsContainer.children)
.map(li => parseInt(li.id.replace('item-', '')))
.map(li => parseInt(li.id.replace('item-', ''), 10))
.filter(id => !isNaN(id));
fetch('/reorder_items', {
@@ -36,16 +67,14 @@ function enableSortMode() {
showToast('Zapisano nową kolejność', 'success');
if (window.currentItems) {
window.currentItems = order.map(id =>
window.currentItems.find(item => item.id === id)
);
window.currentItems = order
.map(id => window.currentItems.find(item => item.id === id))
.filter(Boolean);
updateListSmoothly(window.currentItems);
}
});
}
});
updateSortButtonUI(true);
}, 50);
}
@@ -55,39 +84,22 @@ function disableSortMode() {
sortable = null;
}
isSorting = false;
window.isSorting = false;
if (window.currentItems) {
updateListSmoothly(window.currentItems);
}
updateSortButtonUI(false);
syncSortModeUI();
}
function toggleSortMode() {
isSorting ? disableSortMode() : enableSortMode();
}
function updateSortButtonUI(active) {
const btn = document.getElementById('sort-toggle-btn');
document.body.classList.toggle('sorting-active', !!active);
if (!btn) return;
if (active) {
btn.textContent = '✔️ Zakończ sortowanie';
btn.classList.remove('btn-outline-warning');
btn.classList.add('btn-outline-success');
if (window.isSorting) {
disableSortMode();
} else {
btn.textContent = '✳️ Zmień kolejność';
btn.classList.remove('btn-outline-success');
btn.classList.add('btn-outline-warning');
enableSortMode();
}
}
window.toggleSortMode = toggleSortMode;
window.syncSortModeUI = syncSortModeUI;
document.addEventListener('DOMContentLoaded', () => {
isSorting = false;
window.isSorting = false;
document.body.classList.remove('sorting-active');
updateSortButtonUI(false);
syncSortModeUI();
});

View File

@@ -32,7 +32,7 @@
<a href="{{ request.url_root }}share/{{ list.share_token }}" class="btn btn-outline-primary btn-sm w-100 mb-3" {% if not
list.is_public %}disabled{% endif %}>
✅ Otwórz tryb zakupowy / odznaczania produktów
✅ Otwórz tryb odznaczania
</a>
<div id="share-card" class="card bg-secondary bg-opacity-10 text-white mb-4">
@@ -68,9 +68,8 @@
<!-- Progress bar (dynamic) -->
<h5 id="progress-title" class="mb-2">
📊 Postęp listy —
<span id="purchased-count">{{ purchased_count }}</span>/
<span id="total-count">{{ total_count }}</span> kupionych
Postęp listy —
<span id="purchased-count">{{ purchased_count }}</span>/<span id="total-count">{{ total_count }}</span> kupionych
(<span id="percent-value">{{ percent|int }}</span>%)
</h5>
@@ -83,7 +82,7 @@
title="Pozostałe do kupienia"></div>
<span id="progress-label" class="progress-label small fw-bold"></span>
</div>
<br>
{% if total_expense > 0 %}
<div id="total-expense2" class="text-success fw-bold mb-3">
💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN
@@ -129,9 +128,8 @@
</div>
<div class="list-item-actions shopping-item-actions" role="group">
{% if not is_share %}
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn drag-handle" title="Przesuń produkt" aria-label="Przesuń produkt" {% if list.is_archived %}disabled{% endif %}></button>
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else
%}onclick="editItem({{ item.id }}, '{{ item.name }}', {{ item.quantity or 1 }})" {% endif %}>✏️</button>
%}onclick='openEditItemModal(event, {{ item.id }}, {{ item.name|tojson }}, {{ item.quantity or 1 }})' {% endif %}>✏️</button>
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else
%}onclick="deleteItem({{ item.id }})" {% endif %}>🗑️</button>
{% endif %}
@@ -154,6 +152,38 @@
{% endfor %}
</ul>
<div class="modal fade" id="editItemModal" tabindex="-1" aria-labelledby="editItemModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-white">
<form id="editItemForm">
<div class="modal-header">
<h5 class="modal-title" id="editItemModalLabel">Edytuj produkt</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editItemId">
<div class="mb-3">
<label for="editItemName" class="form-label">Nazwa produktu</label>
<input type="text" id="editItemName" class="form-control bg-dark text-white border-secondary" maxlength="255" required>
</div>
<div>
<label for="editItemQuantity" class="form-label">Ilość</label>
<input type="number" id="editItemQuantity" class="form-control bg-dark text-white border-secondary" min="1" step="1" required>
</div>
</div>
<div class="modal-footer justify-content-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
<button type="submit" class="btn btn-sm btn-outline-light"><span class="shopping-btn-icon" aria-hidden="true">💾</span><span class="shopping-btn-label">Zapisz</span></button>
</div>
</div>
</form>
</div>
</div>
</div>
{% if not list.is_archived %}
<div class="list-action-block mb-3">
<div class="list-action-row mb-2">
@@ -379,6 +409,27 @@
<script src="{{ url_for('static_bp.serve_js', filename='category_modal.js') }}?v={{ APP_VERSION }}"></script>
<script>
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
document.addEventListener('DOMContentLoaded', function () {
const editItemForm = document.getElementById('editItemForm');
if (!editItemForm) return;
editItemForm.addEventListener('submit', function (event) {
event.preventDefault();
const itemId = parseInt(document.getElementById('editItemId').value, 10);
const itemName = document.getElementById('editItemName').value;
const itemQuantity = document.getElementById('editItemQuantity').value;
editItem(itemId, itemName, itemQuantity);
const modalEl = document.getElementById('editItemModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
});
});
</script>
{% endblock %}

1
shopping_app/uploads Symbolic link
View File

@@ -0,0 +1 @@
../uploads