duzo zmian ux
This commit is contained in:
186
app.py
186
app.py
@@ -3,7 +3,7 @@ import secrets
|
||||
import time
|
||||
import mimetypes
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort, session
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort, session, jsonify, make_response
|
||||
from markupsafe import Markup
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||
@@ -270,11 +270,15 @@ def index_guest():
|
||||
now = datetime.utcnow()
|
||||
|
||||
if current_user.is_authenticated:
|
||||
# Twoje listy
|
||||
# Twoje listy aktywne
|
||||
user_lists = ShoppingList.query.filter_by(owner_id=current_user.id, is_archived=False).filter(
|
||||
(ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)
|
||||
).order_by(ShoppingList.created_at.desc()).all()
|
||||
|
||||
# Zarchiwizowane listy
|
||||
archived_lists = ShoppingList.query.filter_by(owner_id=current_user.id, is_archived=True).order_by(ShoppingList.created_at.desc()).all()
|
||||
|
||||
# Publiczne listy innych użytkowników
|
||||
public_lists = ShoppingList.query.filter(
|
||||
ShoppingList.is_public == True,
|
||||
ShoppingList.owner_id != current_user.id,
|
||||
@@ -283,20 +287,22 @@ def index_guest():
|
||||
).order_by(ShoppingList.created_at.desc()).all()
|
||||
else:
|
||||
user_lists = []
|
||||
archived_lists = []
|
||||
public_lists = ShoppingList.query.filter(
|
||||
ShoppingList.is_public == True,
|
||||
((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)),
|
||||
ShoppingList.is_archived == False
|
||||
).order_by(ShoppingList.created_at.desc()).all()
|
||||
|
||||
for l in user_lists + public_lists:
|
||||
# Dodajemy dane o przedmiotach i wydatkach
|
||||
for l in user_lists + public_lists + archived_lists:
|
||||
items = Item.query.filter_by(list_id=l.id).all()
|
||||
l.total_count = len(items)
|
||||
l.purchased_count = len([i for i in items if i.purchased])
|
||||
expenses = Expense.query.filter_by(list_id=l.id).all()
|
||||
l.total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
return render_template("main.html", user_lists=user_lists, public_lists=public_lists)
|
||||
return render_template("main.html", user_lists=user_lists, public_lists=public_lists, archived_lists=archived_lists)
|
||||
|
||||
@app.route('/system-auth', methods=['GET', 'POST'])
|
||||
def system_auth():
|
||||
@@ -322,16 +328,25 @@ def system_auth():
|
||||
flash(f'Nieprawidłowe hasło do systemu. Pozostało prób: {remaining}', 'warning')
|
||||
return render_template('system_auth.html')
|
||||
|
||||
@app.route('/archive_my_list/<int:list_id>')
|
||||
@app.route('/toggle_archive_list/<int:list_id>')
|
||||
@login_required
|
||||
def archive_my_list(list_id):
|
||||
def toggle_archive_list(list_id):
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
if l.owner_id != current_user.id:
|
||||
flash('Nie masz uprawnień do tej listy', 'danger')
|
||||
return redirect(url_for('index_guest'))
|
||||
l.is_archived = True
|
||||
|
||||
# Pobieramy parametr archive z query string
|
||||
archive = request.args.get('archive', 'true').lower() == 'true'
|
||||
|
||||
if archive:
|
||||
l.is_archived = True
|
||||
flash(f'Lista „{l.title}” została zarchiwizowana.', 'success')
|
||||
else:
|
||||
l.is_archived = False
|
||||
flash(f'Lista „{l.title}” została przywrócona.', 'success')
|
||||
|
||||
db.session.commit()
|
||||
flash('Lista została zarchiwizowana', 'success')
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
@app.route('/edit_my_list/<int:list_id>', methods=['GET', 'POST'])
|
||||
@@ -378,7 +393,6 @@ def toggle_visibility(list_id):
|
||||
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
@@ -485,7 +499,7 @@ def guest_list(list_id):
|
||||
@login_required
|
||||
def copy_list(list_id):
|
||||
original = ShoppingList.query.get_or_404(list_id)
|
||||
token = secrets.token_hex(16)
|
||||
token = secrets.token_hex(8)
|
||||
new_list = ShoppingList(title=original.title + ' (Kopia)', owner_id=current_user.id, share_token=token)
|
||||
db.session.add(new_list)
|
||||
db.session.commit()
|
||||
@@ -547,7 +561,8 @@ def uploaded_file(filename):
|
||||
def admin_panel():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
|
||||
now = datetime.utcnow()
|
||||
user_count = User.query.count()
|
||||
list_count = ShoppingList.query.count()
|
||||
item_count = Item.query.count()
|
||||
@@ -614,6 +629,7 @@ def admin_panel():
|
||||
total_expense_sum=total_expense_sum,
|
||||
year_expense_sum=year_expense_sum,
|
||||
month_expense_sum=month_expense_sum,
|
||||
now=now
|
||||
)
|
||||
|
||||
@app.route('/admin/delete_list/<int:list_id>')
|
||||
@@ -805,7 +821,155 @@ def edit_list(list_id):
|
||||
|
||||
return render_template('admin/edit_list.html', list=l, total_expense=total_expense, users=users)
|
||||
|
||||
@app.route('/admin/products')
|
||||
@login_required
|
||||
def list_products():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
items = Item.query.order_by(Item.id.desc()).all()
|
||||
users = User.query.all()
|
||||
users_dict = {user.id: user.username for user in users}
|
||||
|
||||
# Wszystkie sugestie do słownika
|
||||
suggestions = SuggestedProduct.query.all()
|
||||
suggestions_dict = {s.name.lower(): s for s in suggestions}
|
||||
|
||||
return render_template(
|
||||
'admin/list_products.html',
|
||||
items=items,
|
||||
users_dict=users_dict,
|
||||
suggestions_dict=suggestions_dict
|
||||
)
|
||||
|
||||
@app.route('/admin/sync_suggestion/<item_name>')
|
||||
@login_required
|
||||
def sync_suggestion(item_name):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
existing = SuggestedProduct.query.filter(func.lower(SuggestedProduct.name) == item_name.lower()).first()
|
||||
if not existing:
|
||||
new_suggestion = SuggestedProduct(name=item_name)
|
||||
db.session.add(new_suggestion)
|
||||
db.session.commit()
|
||||
flash(f'Utworzono sugestię dla produktu: {item_name}', 'success')
|
||||
else:
|
||||
flash(f'Sugestia dla produktu "{item_name}" już istnieje.', 'info')
|
||||
return redirect(url_for('list_products'))
|
||||
|
||||
@app.route('/admin/delete_suggestion/<int:suggestion_id>')
|
||||
@login_required
|
||||
def delete_suggestion(suggestion_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
suggestion = SuggestedProduct.query.get_or_404(suggestion_id)
|
||||
db.session.delete(suggestion)
|
||||
db.session.commit()
|
||||
flash('Sugestia została usunięta', 'success')
|
||||
return redirect(url_for('list_products'))
|
||||
|
||||
@app.route('/admin/expenses_data')
|
||||
@login_required
|
||||
def admin_expenses_data():
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
range_type = request.args.get('range', 'monthly')
|
||||
start_date_str = request.args.get('start_date')
|
||||
end_date_str = request.args.get('end_date')
|
||||
now = datetime.utcnow()
|
||||
|
||||
labels = []
|
||||
expenses = []
|
||||
|
||||
if start_date_str and end_date_str:
|
||||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
|
||||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
|
||||
|
||||
expenses_query = (
|
||||
db.session.query(
|
||||
extract('year', Expense.added_at).label('year'),
|
||||
extract('month', Expense.added_at).label('month'),
|
||||
func.sum(Expense.amount).label('total')
|
||||
)
|
||||
.filter(Expense.added_at >= start_date, Expense.added_at <= end_date)
|
||||
.group_by('year', 'month')
|
||||
.order_by('year', 'month')
|
||||
.all()
|
||||
)
|
||||
|
||||
for row in expenses_query:
|
||||
label = f"{int(row.month):02d}/{int(row.year)}"
|
||||
labels.append(label)
|
||||
expenses.append(round(row.total, 2))
|
||||
|
||||
response = make_response(jsonify({'labels': labels, 'expenses': expenses}))
|
||||
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
||||
return response
|
||||
|
||||
if range_type == 'monthly':
|
||||
for i in range(11, -1, -1):
|
||||
year = (now - timedelta(days=i*30)).year
|
||||
month = (now - timedelta(days=i*30)).month
|
||||
label = f"{month:02d}/{year}"
|
||||
labels.append(label)
|
||||
|
||||
month_sum = (
|
||||
db.session.query(func.sum(Expense.amount))
|
||||
.filter(extract('year', Expense.added_at) == year)
|
||||
.filter(extract('month', Expense.added_at) == month)
|
||||
.scalar() or 0
|
||||
)
|
||||
expenses.append(round(month_sum, 2))
|
||||
|
||||
elif range_type == 'quarterly':
|
||||
for i in range(3, -1, -1):
|
||||
quarter_start = now - timedelta(days=i*90)
|
||||
year = quarter_start.year
|
||||
quarter = (quarter_start.month - 1) // 3 + 1
|
||||
label = f"Q{quarter}/{year}"
|
||||
quarter_sum = (
|
||||
db.session.query(func.sum(Expense.amount))
|
||||
.filter(extract('year', Expense.added_at) == year)
|
||||
.filter((extract('month', Expense.added_at) - 1)//3 + 1 == quarter)
|
||||
.scalar() or 0
|
||||
)
|
||||
labels.append(label)
|
||||
expenses.append(round(quarter_sum, 2))
|
||||
|
||||
elif range_type == 'halfyearly':
|
||||
for i in range(1, -1, -1):
|
||||
half_start = now - timedelta(days=i*180)
|
||||
year = half_start.year
|
||||
half = 1 if half_start.month <= 6 else 2
|
||||
label = f"H{half}/{year}"
|
||||
half_sum = (
|
||||
db.session.query(func.sum(Expense.amount))
|
||||
.filter(extract('year', Expense.added_at) == year)
|
||||
.filter(
|
||||
(extract('month', Expense.added_at) <= 6) if half == 1 else (extract('month', Expense.added_at) > 6)
|
||||
)
|
||||
.scalar() or 0
|
||||
)
|
||||
labels.append(label)
|
||||
expenses.append(round(half_sum, 2))
|
||||
|
||||
elif range_type == 'yearly':
|
||||
for i in range(4, -1, -1):
|
||||
year = now.year - i
|
||||
label = str(year)
|
||||
year_sum = (
|
||||
db.session.query(func.sum(Expense.amount))
|
||||
.filter(extract('year', Expense.added_at) == year)
|
||||
.scalar() or 0
|
||||
)
|
||||
labels.append(label)
|
||||
expenses.append(round(year_sum, 2))
|
||||
|
||||
response = make_response(jsonify({'labels': labels, 'expenses': expenses}))
|
||||
response.headers["Cache-Control"] = "no-store, no-cache"
|
||||
return response
|
||||
|
||||
# chyba do usuniecia przeniesione na eventy socket.io
|
||||
@app.route('/update-note/<int:item_id>', methods=['POST'])
|
||||
|
@@ -123,4 +123,14 @@ input[type="checkbox"]:disabled::before {
|
||||
}
|
||||
input[type="checkbox"]:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#tempToggle {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
input.form-control {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
93
static/js/expenses.js
Normal file
93
static/js/expenses.js
Normal file
@@ -0,0 +1,93 @@
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
let expensesChart = null;
|
||||
const rangeLabel = document.getElementById("chartRangeLabel");
|
||||
|
||||
function loadExpenses(range = "monthly", startDate = null, endDate = null) {
|
||||
let url = '/admin/expenses_data?range=' + range;
|
||||
if (startDate && endDate) {
|
||||
url += `&start_date=${startDate}&end_date=${endDate}`;
|
||||
}
|
||||
|
||||
fetch(url, {cache: "no-store"})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('expensesChart').getContext('2d');
|
||||
|
||||
if (expensesChart) {
|
||||
expensesChart.destroy();
|
||||
}
|
||||
|
||||
expensesChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Suma wydatków [PLN]',
|
||||
data: data.expenses,
|
||||
backgroundColor: '#0d6efd'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (startDate && endDate) {
|
||||
rangeLabel.textContent = `Widok: własny zakres (${startDate} → ${endDate})`;
|
||||
} else {
|
||||
let labelText = "";
|
||||
if (range === "monthly") labelText = "Widok: miesięczne";
|
||||
else if (range === "quarterly") labelText = "Widok: kwartalne";
|
||||
else if (range === "halfyearly") labelText = "Widok: półroczne";
|
||||
else if (range === "yearly") labelText = "Widok: roczne";
|
||||
rangeLabel.textContent = labelText;
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Błąd pobierania danych:", error);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loadExpensesBtn').addEventListener('click', function() {
|
||||
loadExpenses();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.range-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const range = this.getAttribute('data-range');
|
||||
loadExpenses(range);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('customRangeBtn').addEventListener('click', function() {
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
if (startDate && endDate) {
|
||||
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
|
||||
loadExpenses('custom', startDate, endDate);
|
||||
} else {
|
||||
alert("Proszę wybrać obie daty!");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const startDateInput = document.getElementById("startDate");
|
||||
const endDateInput = document.getElementById("endDate");
|
||||
|
||||
const today = new Date();
|
||||
const threeDaysAgo = new Date(today);
|
||||
threeDaysAgo.setDate(today.getDate() - 7);
|
||||
|
||||
const formatDate = (d) => d.toISOString().split('T')[0];
|
||||
|
||||
startDateInput.value = formatDate(threeDaysAgo);
|
||||
endDateInput.value = formatDate(today);
|
||||
});
|
32
static/js/toggle_button.js
Normal file
32
static/js/toggle_button.js
Normal file
@@ -0,0 +1,32 @@
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const toggleBtn = document.getElementById("tempToggle");
|
||||
const hiddenInput = document.getElementById("temporaryHidden");
|
||||
|
||||
// Inicjalizacja tooltipa
|
||||
const tooltip = new bootstrap.Tooltip(toggleBtn);
|
||||
|
||||
// Funkcja aktualizująca wygląd
|
||||
function updateToggle(isActive) {
|
||||
if (isActive) {
|
||||
toggleBtn.classList.remove("btn-outline-secondary");
|
||||
toggleBtn.classList.add("btn-success");
|
||||
toggleBtn.textContent = "Tymczasowa ✔️";
|
||||
} else {
|
||||
toggleBtn.classList.remove("btn-success");
|
||||
toggleBtn.classList.add("btn-outline-secondary");
|
||||
toggleBtn.textContent = "Tymczasowa";
|
||||
}
|
||||
}
|
||||
|
||||
// Inicjalizacja stanu
|
||||
let active = toggleBtn.getAttribute("data-active") === "1";
|
||||
updateToggle(active);
|
||||
|
||||
// Obsługa kliknięcia
|
||||
toggleBtn.addEventListener("click", function() {
|
||||
active = !active;
|
||||
toggleBtn.setAttribute("data-active", active ? "1" : "0");
|
||||
hiddenInput.value = active ? "1" : "0";
|
||||
updateToggle(active);
|
||||
});
|
||||
});
|
14
static/lib/js/chart.js
Normal file
14
static/lib/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
@@ -25,6 +25,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/receipts">📸 Wszystkie paragony</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/products">🛍️ Produkty</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle text-danger" href="#" id="clearDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
🗑️ Czyszczenie
|
||||
@@ -38,7 +41,6 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-dark text-white h-100">
|
||||
@@ -70,16 +72,17 @@
|
||||
<div class="card bg-dark text-white h-100">
|
||||
<div class="card-body">
|
||||
<h5>💸 Podsumowanie wydatków:</h5>
|
||||
<ul class="mb-0">
|
||||
<ul class="mb-3">
|
||||
<li><strong>Obecny miesiąc:</strong> {{ '%.2f'|format(month_expense_sum) }} PLN</li>
|
||||
<li><strong>Obecny rok:</strong> {{ '%.2f'|format(year_expense_sum) }} PLN</li>
|
||||
<li><strong>Całkowite:</strong> {{ '%.2f'|format(total_expense_sum) }} PLN</li>
|
||||
</ul>
|
||||
<button type="button" class="btn btn-outline-primary w-100 mt-3" data-bs-toggle="modal" data-bs-target="#expensesChartModal" id="loadExpensesBtn">
|
||||
📊 Pokaż wykres wydatków
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h3 class="mt-4">📄 Wszystkie listy zakupowe</h3>
|
||||
@@ -140,7 +143,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="d-flex flex-wrap gap-1">
|
||||
<a href="{{ url_for('edit_list', list_id=l.id) }}" class="btn btn-sm btn-outline-primary">✏️ Edytuj liste</a>
|
||||
<a href="{{ url_for('edit_list', list_id=l.id) }}" class="btn btn-sm btn-outline-primary">✏️ Edytuj</a>
|
||||
<a href="{{ url_for('archive_list', list_id=l.id) }}" class="btn btn-sm btn-outline-secondary">📥 Archiwizuj</a>
|
||||
<a href="{{ url_for('delete_list', list_id=l.id) }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
|
||||
</td>
|
||||
@@ -153,14 +156,50 @@
|
||||
<button type="submit" class="btn btn-danger mt-2">🗑️ Usuń zaznaczone listy</button>
|
||||
</form>
|
||||
|
||||
<div class="modal fade" id="expensesChartModal" tabindex="-1" aria-labelledby="expensesChartModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white rounded">
|
||||
<div class="modal-header border-0">
|
||||
<div>
|
||||
<h5 class="modal-title m-0" id="expensesChartModalLabel">📊 Wydatki</h5>
|
||||
<small id="chartRangeLabel" class="text-muted">Widok: miesięczne</small>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-outline-light btn-sm range-btn active" data-range="monthly">📅 Miesięczne</button>
|
||||
<button class="btn btn-outline-light btn-sm range-btn" data-range="quarterly">📊 Kwartalne</button>
|
||||
<button class="btn btn-outline-light btn-sm range-btn" data-range="halfyearly">🗓️ Półroczne</button>
|
||||
<button class="btn btn-outline-light btn-sm range-btn" data-range="yearly">📆 Roczne</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm mb-3 w-100" style="max-width: 570px;">
|
||||
<span class="input-group-text bg-secondary text-white border-secondary">Od</span>
|
||||
<input type="date" class="form-control bg-dark text-white border-secondary flex-grow-1" id="startDate">
|
||||
<span class="input-group-text bg-secondary text-white border-secondary">Do</span>
|
||||
<input type="date" class="form-control bg-dark text-white border-secondary flex-grow-1" id="endDate">
|
||||
<button class="btn btn-outline-success" id="customRangeBtn">Pokaż dane z zakresu 📅</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-dark rounded p-2">
|
||||
<canvas id="expensesChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js_lib', filename='chart.js') }}"></script>
|
||||
<script>
|
||||
document.getElementById('select-all').addEventListener('click', function(){
|
||||
const checkboxes = document.querySelectorAll('input[name="list_ids"]');
|
||||
checkboxes.forEach(cb => cb.checked = this.checked);
|
||||
});
|
||||
</script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='expenses.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
62
templates/admin/list_products.html
Normal file
62
templates/admin/list_products.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Produkty i sugestie{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🛍️ Produkty i sugestie</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="m-0">📦 Produkty (z synchronizacją sugestii)</h4>
|
||||
<span class="badge bg-secondary">{{ items|length }} produktów</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-dark table-striped align-middle m-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nazwa</th>
|
||||
<th>Dodana przez</th>
|
||||
<th>Sugestia</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td class="fw-bold">{{ item.name }}</td>
|
||||
<td>
|
||||
{% if item.added_by %}
|
||||
{{ users_dict.get(item.added_by, 'Nieznany') }}
|
||||
{% else %}
|
||||
Gość
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% set suggestion = suggestions_dict.get(item.name.lower()) %}
|
||||
{% if suggestion %}
|
||||
✅ Istnieje (ID: {{ suggestion.id }})
|
||||
<a href="/admin/delete_suggestion/{{ suggestion.id }}" class="btn btn-sm btn-outline-danger ms-1">🗑️ Usuń</a>
|
||||
{% else %}
|
||||
<a href="/admin/sync_suggestion/{{ item.name }}" class="btn btn-sm btn-outline-primary">🔄 Synchronizuj</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/list/{{ item.list_id }}" class="btn btn-sm btn-outline-light mb-1">📄 Zobacz listę</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if items|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">Brak produktów do wyświetlenia.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@@ -12,32 +12,32 @@ Lista: <strong>{{ list.title }}</strong>
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<a href="{{ request.url_root }}share/{{ list.share_token }}"
|
||||
class="btn btn-primary btn-sm w-100 mb-3"
|
||||
{% if not list.is_public %}disabled{% endif %}>
|
||||
✅ Otwórz tryb zakupowy / odznaczania produktów
|
||||
</a>
|
||||
<div id="share-card" class="card bg-dark text-white mb-4">
|
||||
<div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-2">
|
||||
<div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<strong id="share-header">
|
||||
{% if list.is_public %}
|
||||
🔗 Udostępnij link:
|
||||
{% else %}
|
||||
🙈 Lista jest ukryta przed gośćmi
|
||||
{% endif %}
|
||||
</strong><br>
|
||||
</strong>
|
||||
<span id="share-url" class="badge bg-secondary text-wrap" style="font-size: 0.7rem; {% if not list.is_public %}display: none;{% endif %}">
|
||||
{{ request.url_root }}share/{{ list.share_token }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-md-row gap-2 mt-2 mt-md-0">
|
||||
<button id="copyBtn" class="btn btn-success btn-sm"
|
||||
<div class="d-flex flex-column flex-md-row gap-2">
|
||||
<button id="copyBtn" class="btn btn-success btn-sm flex-fill"
|
||||
onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')"
|
||||
{% if not list.is_public %}disabled{% endif %}>
|
||||
📋 Skopiuj / Udostępnij
|
||||
</button>
|
||||
<button id="openBtn" class="btn btn-primary btn-sm"
|
||||
onclick="openList('{{ request.url_root }}share/{{ list.share_token }}')"
|
||||
{% if not list.is_public %}disabled{% endif %}>
|
||||
✅ Otwórz do odznaczania
|
||||
</button>
|
||||
<button id="toggleVisibilityBtn" class="btn btn-outline-secondary btn-sm" onclick="toggleVisibility({{ list.id }})">
|
||||
<button id="toggleVisibilityBtn" class="btn btn-outline-light btn-sm flex-fill" onclick="toggleVisibility({{ list.id }})">
|
||||
{% if list.is_public %}
|
||||
🙈 Ukryj listę
|
||||
{% else %}
|
||||
@@ -103,11 +103,10 @@ Lista: <strong>{{ list.title }}</strong>
|
||||
|
||||
{% if not list.is_archived %}
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="newItem" name="name" class="form-control" placeholder="Dodaj produkt" required>
|
||||
<input type="text" id="newItem" name="name" class="form-control" placeholder="Dodaj produkt i ilość" required>
|
||||
<input type="number" id="newQuantity" name="quantity" class="form-control" placeholder="Ilość" min="1" value="1" style="max-width: 80px;">
|
||||
<button type="button" class="btn btn-success rounded" onclick="addItem({{ list.id }})">➕ Dodaj</button>
|
||||
<button type="button" class="btn btn-success rounded-end" onclick="addItem({{ list.id }})">➕ Dodaj</button>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% set receipt_pattern = 'list_' ~ list.id %}
|
||||
|
@@ -45,7 +45,7 @@
|
||||
<div class="input-group mb-2">
|
||||
<input id="newItem" class="form-control" placeholder="Dodaj produkt i ilość">
|
||||
<input id="newQuantity" type="number" class="form-control" placeholder="Ilość" min="1" value="1" style="max-width: 90px;">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success rounded">➕ Dodaj</button>
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success rounded-end">➕ Dodaj</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<h5>💰 Dodaj wydatek</h5>
|
||||
<div class="input-group mb-2">
|
||||
<input id="expenseAmount" type="number" step="0.01" min="0" class="form-control" placeholder="Kwota (PLN)">
|
||||
<button onclick="submitExpense({{ list.id }})" class="btn btn-success rounded">💾 Zapisz</button>
|
||||
<button onclick="submitExpense({{ list.id }})" class="btn btn-success rounded-end">💾 Zapisz</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p id="total-expense2"><b>💸 Łącznie wydano:</b> {{ '%.2f'|format(total_expense) }} PLN</p>
|
||||
@@ -67,7 +67,7 @@
|
||||
{% 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">
|
||||
<img src="{{ url_for('uploaded_file', filename=file) }}" class="img-fluid rounded shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
|
||||
<img src="{{ url_for('uploaded_file', filename=file) }}" class="img-fluid rounded-end shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -83,7 +83,7 @@
|
||||
<form 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 custom-file-input" id="receiptInput">
|
||||
<button type="submit" class="btn btn-success rounded">➕ Wgraj</button>
|
||||
<button type="submit" class="btn btn-success rounded-end">➕ Wgraj</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@@ -9,28 +9,40 @@
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Stwórz nową listę</h2>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Stwórz nową listę</h2>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę nowej listy" required class="form-control">
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" name="temporary" class="form-check-input m-0" id="tempCheck">
|
||||
<label for="tempCheck" class="ms-2 mb-0">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę nowej listy" required class="form-control">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary rounded-end"
|
||||
id="tempToggle"
|
||||
data-active="0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Po włączeniu lista będzie ważna tylko 7 dni">
|
||||
Tymczasowa
|
||||
</button>
|
||||
<input type="hidden" name="temporary" id="temporaryHidden" value="0">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<h3 class="mt-4">Twoje listy</h3>
|
||||
<h3 class="mt-4 d-flex justify-content-between align-items-center flex-wrap">
|
||||
Twoje listy
|
||||
<button type="button" class="btn btn-sm btn-outline-light ms-2" data-bs-toggle="modal" data-bs-target="#archivedModal">
|
||||
📁 Zarchiwizowane
|
||||
</button>
|
||||
</h3>
|
||||
{% if user_lists %}
|
||||
<ul class="list-group mb-4">
|
||||
{% for l in user_lists %}
|
||||
@@ -40,17 +52,18 @@
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
|
||||
<span class="fw-bold">{{ l.title }} (Autor: Ty)</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-secondary me-1">📋 Kopiuj</a>
|
||||
<a href="/edit_my_list/{{ l.id }}" class="btn btn-sm btn-outline-warning me-1">✏️ Edytuj</a>
|
||||
<a href="/archive_my_list/{{ l.id }}" class="btn btn-sm btn-outline-danger me-1">🗄️ Archiwizuj</a>
|
||||
{% if l.is_public %}
|
||||
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-secondary">🙈 Ukryj</a>
|
||||
{% else %}
|
||||
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-success">👁️ Udostępnij</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap mt-2 mt-md-0">
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1 mb-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-light me-1 mb-1">📋 Kopiuj</a>
|
||||
<a href="/edit_my_list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1 mb-1">✏️ Edytuj</a>
|
||||
<a href="/toggle_archive_list/{{ l.id }}?archive=true" class="btn btn-sm btn-outline-light me-1 mb-1">🗄️ Archiwizuj</a>
|
||||
{% if l.is_public %}
|
||||
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-light me-1 mb-1">🙈 Ukryj</a>
|
||||
{% else %}
|
||||
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-light me-1 mb-1">👁️ Udostępnij</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-warning text-dark fw-bold" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
|
||||
@@ -95,4 +108,37 @@
|
||||
<p><span class="badge bg-secondary">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">
|
||||
<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" id="archivedModalLabel">Zarchiwizowane listy</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if archived_lists %}
|
||||
<ul class="list-group">
|
||||
{% for l in archived_lists %}
|
||||
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap">
|
||||
<span>{{ l.title }}</span>
|
||||
<a href="/toggle_archive_list/{{ l.id }}?archive=false" class="btn btn-sm btn-outline-success">♻️ Przywróć</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><span class="badge bg-secondary">Nie masz żadnych zarchiwizowanych list.</span></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='toggle_button.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user