zmiany ux i nowe funckje

This commit is contained in:
Mateusz Gruszczyński
2025-07-10 13:03:33 +02:00
parent 40fa601bbe
commit f36739aa40
11 changed files with 317 additions and 77 deletions

79
app.py
View File

@@ -35,20 +35,6 @@ UPLOAD_FOLDER = app.config.get('UPLOAD_FOLDER', 'uploads')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
AUTHORIZED_COOKIE_VALUE = app.config.get('AUTHORIZED_COOKIE_VALUE', '80d31cdfe63539c9')
PROTECTED_JS_FILES = {
"live.js",
"notes.js",
"sockets.js",
"product_suggestion.js",
"expenses.js",
"toggle_button.js",
"user_management.js",
"mass_add.js",
"functions.js",
"clickable_row.js",
"receipt_section.js"
}
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
failed_login_attempts = defaultdict(deque)
@@ -273,7 +259,7 @@ def require_system_password():
if request.endpoint == 'static_bp.serve_js':
requested_file = request.view_args.get("filename", "")
if requested_file in PROTECTED_JS_FILES:
if requested_file.endswith(".js"):
return redirect(url_for('system_auth', next=request.url))
else:
return
@@ -596,7 +582,7 @@ def all_products():
return {'allproducts': all_names}
@app.route('/upload_receipt/<int:list_id>', methods=['POST'])
""" @app.route('/upload_receipt/<int:list_id>', methods=['POST'])
def upload_receipt(list_id):
if 'receipt' not in request.files:
flash('Brak pliku', 'danger')
@@ -618,6 +604,41 @@ def upload_receipt(list_id):
return redirect(request.referrer)
flash('Niedozwolony format pliku', 'danger')
return redirect(request.referrer) """
@app.route('/upload_receipt/<int:list_id>', methods=['POST'])
def upload_receipt(list_id):
if 'receipt' not in request.files:
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'message': 'Brak pliku'}), 400
flash('Brak pliku', 'danger')
return redirect(request.referrer)
file = request.files['receipt']
if file.filename == '':
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'message': 'Nie wybrano pliku'}), 400
flash('Nie wybrano pliku', 'danger')
return redirect(request.referrer)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
full_filename = f"list_{list_id}_{filename}"
file_path = os.path.join(app.config['UPLOAD_FOLDER'], full_filename)
save_resized_image(file, file_path)
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
url = url_for('uploaded_file', filename=full_filename)
return jsonify({'success': True, 'url': url})
flash('Wgrano paragon', 'success')
return redirect(request.referrer)
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'message': 'Niedozwolony format pliku'}), 400
flash('Niedozwolony format pliku', 'danger')
return redirect(request.referrer)
@app.route('/uploads/<filename>')
@@ -1096,10 +1117,19 @@ def demote_user(user_id):
def handle_delete_item(data):
item = Item.query.get(data['item_id'])
if item:
list_id = item.list_id
db.session.delete(item)
db.session.commit()
emit('item_deleted', {'item_id': item.id}, to=str(item.list_id))
purchased_count, total_count, percent = get_progress(list_id)
emit('progress_updated', {
'purchased_count': purchased_count,
'total_count': total_count,
'percent': percent
}, to=str(list_id))
@socketio.on('edit_item')
def handle_edit_item(data):
item = Item.query.get(data['item_id'])
@@ -1188,6 +1218,14 @@ def handle_add_item(data):
'added_by': current_user.username if current_user.is_authenticated else 'Gość'
}, to=str(list_id), include_self=True)
purchased_count, total_count, percent = get_progress(list_id)
emit('progress_updated', {
'purchased_count': purchased_count,
'total_count': total_count,
'percent': percent
}, to=str(list_id))
@socketio.on('check_item')
def handle_check_item(data):
item = Item.query.get(data['item_id'])
@@ -1265,6 +1303,15 @@ def handle_add_expense(data):
'total': total
}, to=str(list_id))
@socketio.on('receipt_uploaded')
def handle_receipt_uploaded(data):
list_id = data['list_id']
url = data['url']
emit('receipt_added', {
'url': url
}, to=str(list_id), include_self=False)
@app.cli.command('create_db')
def create_db():
db.create_all()

View File

@@ -32,6 +32,16 @@
white-space: nowrap;
}
/* rodzic już ma position-relative */
.progress-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none; /* klikalne przyciski obok paska nie ucierpią */
white-space: nowrap;
}
.progress-thin {
height: 12px;
}
@@ -250,3 +260,8 @@ input.form-control {
align-items: center;
justify-content: space-between;
}
#empty-placeholder {
font-style: italic;
pointer-events: none;
}

View File

@@ -281,6 +281,7 @@ function updateListSmoothly(newItems) {
itemsContainer.appendChild(fragment);
updateProgressBar();
toggleEmptyPlaceholder();
}
document.addEventListener("DOMContentLoaded", function() {

View File

@@ -1,5 +1,25 @@
const socket = io();
/*──────────────── placeholder pustej listy ────────────────*/
function toggleEmptyPlaceholder() {
const list = document.getElementById('items');
if (!list) return;
// prawdziwe <li> to te z dataname lub id="item…"
const hasRealItems = list.querySelector('li[data-name], li[id^="item-"]') !== null;
const placeholder = document.getElementById('empty-placeholder');
if (!hasRealItems && !placeholder) {
const li = document.createElement('li');
li.id = 'empty-placeholder';
li.className = 'list-group-item bg-dark text-secondary text-center w-100';
li.textContent = 'Brak produktów w tej liście.';
list.appendChild(li);
} else if (hasRealItems && placeholder) {
placeholder.remove();
}
}
function setupList(listId, username) {
socket.emit('join_list', { room: listId, username: username });
@@ -135,6 +155,7 @@ function setupList(listId, username) {
document.getElementById('items').appendChild(li);
updateProgressBar();
toggleEmptyPlaceholder();
});
socket.on('item_deleted', data => {
@@ -144,6 +165,7 @@ function setupList(listId, username) {
}
showToast('Usunięto produkt');
updateProgressBar();
toggleEmptyPlaceholder();
});
socket.on('progress_updated', function(data) {
@@ -197,10 +219,10 @@ function setupList(listId, username) {
});
updateProgressBar();
toggleEmptyPlaceholder();
// --- WAŻNE: zapisz dane do reconnect ---
window.LIST_ID = listId;
window.usernameForReconnect = username;
}
}

View File

@@ -3,7 +3,6 @@ document.addEventListener('DOMContentLoaded', function () {
const productList = document.getElementById('mass-add-list');
modal.addEventListener('show.bs.modal', async function () {
// 🔥 Za każdym razem od nowa budujemy zbiór produktów już na liście
let addedProducts = new Set();
document.querySelectorAll('#items li').forEach(li => {
if (li.dataset.name) {
@@ -91,16 +90,11 @@ document.addEventListener('DOMContentLoaded', function () {
productList.appendChild(li);
});
} catch (err) {
productList.innerHTML = '<li class="list-group-item text-danger bg-dark">Błąd ładowania danych</li>';
}
});
// 🔥 Aktualizacja na żywo po dodaniu
socket.on('item_added', data => {
document.querySelectorAll('#mass-add-list li').forEach(li => {
const itemName = li.firstChild.textContent.trim();

View File

@@ -0,0 +1,85 @@
let receiptToastShown = false;
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("receiptForm");
const input = document.getElementById("receiptInput");
const gallery = document.getElementById("receiptGallery");
const progressContainer = document.getElementById("progressContainer");
const progressBar = document.getElementById("progressBar");
if (!form || !input || !gallery) return;
form.addEventListener("submit", function (e) {
e.preventDefault();
const file = input.files[0];
if (!file) {
showToast("Nie wybrano pliku!", "warning");
return;
}
const formData = new FormData();
formData.append("receipt", file);
const xhr = new XMLHttpRequest();
xhr.open("POST", form.action, true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + "%";
progressBar.textContent = percent + "%";
}
};
xhr.onloadstart = function () {
progressContainer.style.display = "block";
progressBar.style.width = "0%";
progressBar.textContent = "0%";
};
xhr.onloadend = function () {
progressContainer.style.display = "none";
progressBar.style.width = "0%";
progressBar.textContent = "";
input.value = "";
};
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
if (res.success && res.url) {
fetch(window.location.href)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const newGallery = doc.getElementById("receiptGallery");
if (newGallery) {
gallery.innerHTML = newGallery.innerHTML;
if (!receiptToastShown) {
showToast("Wgrano paragon", "success");
receiptToastShown = true;
}
socket.emit("receipt_uploaded", {
list_id: LIST_ID,
url: res.url
});
}
});
} else {
showToast(res.message || "Błąd podczas wgrywania.", "danger");
}
} else {
showToast("Błąd serwera. Spróbuj ponownie.", "danger");
}
}
};
xhr.send(formData);
});
});

View File

@@ -1,3 +1,5 @@
let didReceiveFirstFullList = false;
// --- Automatyczny reconnect po powrocie do karty/przywróceniu internetu ---
function reconnectIfNeeded() {
if (!socket.connected) {
@@ -75,14 +77,45 @@ socket.on('user_list', function(data) {
}
});
socket.on('full_list', function(data) {
const itemsContainer = document.getElementById('items');
const oldItems = Array.from(itemsContainer.querySelectorAll('li'));
socket.on('receipt_added', function (data) {
const gallery = document.getElementById("receiptGallery");
if (!gallery) return;
if (isListDifferent(oldItems, data.items)) {
updateListSmoothly(data.items);
showToast('Lista została zaktualizowana', 'info');
} else {
updateListSmoothly(data.items);
// Usuń placeholder, jeśli istnieje
const alert = gallery.querySelector(".alert");
if (alert) {
alert.remove();
}
// Sprawdź, czy już istnieje obraz z tym URL
const existing = Array.from(gallery.querySelectorAll("img")).find(img => img.src === data.url);
if (!existing) {
const col = document.createElement("div");
col.className = "col-6 col-md-4 col-lg-3 text-center";
col.innerHTML = `
<a href="${data.url}" data-lightbox="receipt" data-title="Paragon">
<img src="${data.url}" class="img-fluid rounded shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
</a>
`;
gallery.appendChild(col);
}
});
socket.on('full_list', function (data) {
const itemsContainer = document.getElementById('items');
const oldItems = Array.from(
itemsContainer.querySelectorAll('li[data-name], li[id^="item-"]')
);
const isDifferent = isListDifferent(oldItems, data.items);
updateListSmoothly(data.items);
toggleEmptyPlaceholder();
if (didReceiveFirstFullList && isDifferent) {
showToast('Lista została zaktualizowana', 'info');
}
didReceiveFirstFullList = true;
});

View File

@@ -33,7 +33,7 @@
</div>
{% if not image_files %}
<div class="alert alert-info" role="alert">
<div class="alert alert-info text-center" role="alert">
Nie wgrano paragonów.
</div>
{% endif %}

View File

@@ -53,12 +53,19 @@ Lista: <strong>{{ list.title }}</strong>
📊 Postęp listy — {{ purchased_count }}/{{ total_count }} kupionych ({{ percent|round(0) }}%)
</h5>
<div class="progress progress-dark">
<div id="progress-bar" class="progress-bar bg-warning text-dark fw-bold" role="progressbar"
style="width: {{ percent }}%;" aria-valuenow="{{ percent }}"
aria-valuemin="0" aria-valuemax="100">
{{ percent|round(0) }}%
<div class="progress progress-dark position-relative">
{# właściwy pasek postępu #}
<div id="progress-bar"
class="progress-bar bg-warning text-dark"
role="progressbar"
style="width: {{ percent }}%;"
aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
</div>
<span class="progress-label small fw-bold
{% if percent < 50 %}text-white{% else %}text-dark{% endif %}">
{{ percent|round(0) }}%
</span>
</div>
{% if total_expense > 0 %}
@@ -66,7 +73,7 @@ Lista: <strong>{{ list.title }}</strong>
💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN
</div>
{% else %}
<div id="total-expense2" class="text-success fw-bold mb-3" style="display: none;">
<div id="total-expense2" class="text-success fw-bold mb-3">
💸 Łącznie wydano: 0.00 PLN
</div>
{% endif %}
@@ -97,6 +104,11 @@ Lista: <strong>{{ list.title }}</strong>
</button>
</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>
@@ -118,10 +130,11 @@ Lista: <strong>{{ list.title }}</strong>
{% endif %}
{% set receipt_pattern = 'list_' ~ list.id %}
{% if receipt_files %}
<hr>
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
<div class="row g-3 mt-2">
<hr>
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
<div class="row g-3 mt-2" id="receiptGallery">
{% if receipt_files %}
{% for file in receipt_files %}
<div class="col-6 col-md-4 col-lg-3 text-center">
<a href="{{ url_for('uploaded_file', filename=file) }}" data-lightbox="receipt" data-title="Paragon">
@@ -129,11 +142,12 @@ Lista: <strong>{{ list.title }}</strong>
</a>
</div>
{% endfor %}
</div>
{% else %}
<hr>
<p><span class="badge bg-secondary">Brak wgranych paragonów do tej listy.</span></p>
{% endif %}
{% else %}
<div class="alert alert-info text-center w-100" role="alert">
Brak wgranych paragonów do tej listy.
</div>
{% endif %}
</div>
<div class="modal fade" id="massAddModal" tabindex="-1" aria-labelledby="massAddModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
@@ -158,6 +172,7 @@ Lista: <strong>{{ list.title }}</strong>
<script>
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
</script>
<script src="{{ url_for('static_bp.serve_js', filename='receipt_upload.js') }}"></script>
{% endblock %}
{% endblock %}

View File

@@ -37,7 +37,12 @@
{% if list.is_archived %}disabled{% else %}onclick="openNoteModal(event, {{ item.id }})"{% endif %}>
📝
</button>
</li>
</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>
@@ -64,9 +69,11 @@
</button>
<div class="collapse" id="receiptSection">
{% set receipt_pattern = 'list_' ~ list.id %}
{% if receipt_files %}
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
<div class="row g-3 mt-2">
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
<div class="row g-3 mt-2" id="receiptGallery">
{% if receipt_files %}
{% for file in receipt_files %}
<div class="col-6 col-md-4 col-lg-3 text-center">
<a href="{{ url_for('uploaded_file', filename=file) }}" data-lightbox="receipt" data-title="Paragon">
@@ -74,21 +81,27 @@
</a>
</div>
{% endfor %}
</div>
{% else %}
<p><span class="badge bg-secondary">Brak wgranych paragonów do tej listy.</span></p>
{% endif %}
{% else %}
<div class="alert alert-info text-center w-100" role="alert">
Brak wgranych paragonów do tej listy.
</div>
{% endif %}
</div>
{% if not list.is_archived %}
<hr>
<h5>📤 Dodaj zdjęcie paragonu</h5>
<form action="{{ url_for('upload_receipt', list_id=list.id) }}" method="post" enctype="multipart/form-data">
<form id="receiptForm" action="{{ url_for('upload_receipt', list_id=list.id) }}" method="post" enctype="multipart/form-data">
<div class="input-group mb-2">
<input type="file" name="receipt" accept="image/*" capture="environment" class="form-control bg-dark text-white border-secondary custom-file-input" id="receiptInput">
<input type="file" name="receipt" accept="image/*" capture="environment" class="form-control bg-dark text-white border-secondary" id="receiptInput">
<button type="submit" class="btn btn-success rounded-end"> Wgraj</button>
</div>
<div id="progressContainer" class="progress mb-2" style="height: 20px; display: none;">
<div id="progressBar" class="progress-bar bg-success fw-bold" role="progressbar" style="width: 0%;">0%</div>
</div>
</form>
{% endif %}
</div>
<!-- Modal notatki -->
@@ -116,6 +129,7 @@
<script src="{{ url_for('static_bp.serve_js', filename='notes.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='clickable_row.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='receipt_section.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='receipt_upload.js') }}"></script>
<script>
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
</script>

View File

@@ -65,13 +65,19 @@
{% endif %}
</div>
</div>
<div class="progress progress-dark progress-thin mt-2">
<div class="progress-bar bg-warning text-dark fw-bold small" role="progressbar" style="width: {{ percent }}%;" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
{% if l.total_expense > 0 %}
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
{% endif %}
</div>
<div class="progress progress-dark progress-thin mt-2 position-relative">
<div class="progress-bar bg-warning text-dark"
role="progressbar"
style="width: {{ percent }}%;"
aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
</div>
<span class="progress-label small fw-bold
{% if percent < 50 %}text-white{% else %}text-dark{% endif %}">
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
{% if l.total_expense > 0 %}
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
{% endif %}
</span>
</div>
</li>
{% endfor %}
@@ -93,19 +99,25 @@
<span class="fw-bold">{{ l.title }} (Autor: {{ l.owner.username }})</span>
<a href="/guest-list/{{ l.id }}" class="btn btn-sm btn-outline-light">📄 Otwórz</a>
</div>
<div class="progress progress-dark progress-thin mt-2">
<div class="progress-bar bg-warning text-dark fw-bold small" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
{% if l.total_expense > 0 %}
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
{% endif %}
</div>
</div>
</li>
{% endfor %}
<div class="progress progress-dark progress-thin mt-2 position-relative">
<div class="progress-bar bg-warning text-dark"
role="progressbar"
style="width: {{ percent }}%;"
aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
</div>
<span class="progress-label small fw-bold
{% if percent < 50 %}text-white{% else %}text-dark{% endif %}">
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
{% if l.total_expense > 0 %}
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
{% endif %}
</span>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p><span class="badge bg-secondary">Brak dostępnych list publicznych do wyświetlenia.</span></p>
<p><span class="badge rounded-pill bg-secondary opacity-75">Brak dostępnych list publicznych do wyświetlenia</span></p>
{% endif %}
<div class="modal fade" id="archivedModal" tabindex="-1" aria-labelledby="archivedModalLabel" aria-hidden="true">
@@ -126,7 +138,9 @@
{% endfor %}
</ul>
{% else %}
<p><span class="badge bg-secondary">Nie masz żadnych zarchiwizowanych list.</span></p>
<div class="alert alert-info text-center" role="alert">
Nie masz żadnych zarchiwizowanych list.
</div>
{% endif %}
</div>
<div class="modal-footer">