uprawnienia ocr i uploadu

This commit is contained in:
Mateusz Gruszczyński
2025-07-21 15:50:35 +02:00
parent 955196dd92
commit 983114575d
5 changed files with 154 additions and 83 deletions

View File

@@ -1,31 +1,41 @@
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('.clickable-item').forEach(item => {
item.addEventListener('click', function (e) {
if (!e.target.closest('button') && e.target.tagName.toLowerCase() !== 'input') {
const checkbox = this.querySelector('input[type="checkbox"]');
const itemsContainer = document.getElementById('items');
if (!itemsContainer) return;
if (checkbox.disabled) {
return;
}
itemsContainer.addEventListener('click', function (e) {
const row = e.target.closest('.clickable-item');
if (!row || !itemsContainer.contains(row)) return;
if (checkbox.checked) {
socket.emit('uncheck_item', { item_id: parseInt(this.id.replace('item-', ''), 10) });
} else {
socket.emit('check_item', { item_id: parseInt(this.id.replace('item-', ''), 10) });
}
// Ignoruj kliknięcia w przyciski i inputy
if (e.target.closest('button') || e.target.tagName.toLowerCase() === 'input') {
return;
}
checkbox.disabled = true;
this.classList.add('opacity-50');
const checkbox = row.querySelector('input[type="checkbox"]');
if (!checkbox || checkbox.disabled) {
return;
}
let existingSpinner = this.querySelector('.spinner-border');
if (!existingSpinner) {
const spinner = document.createElement('span');
spinner.className = 'spinner-border spinner-border-sm ms-2';
spinner.setAttribute('role', 'status');
spinner.setAttribute('aria-hidden', 'true');
checkbox.parentElement.appendChild(spinner);
}
}
});
const itemId = parseInt(row.id.replace('item-', ''), 10);
if (isNaN(itemId)) return;
if (checkbox.checked) {
socket.emit('uncheck_item', { item_id: itemId });
} else {
socket.emit('check_item', { item_id: itemId });
}
checkbox.disabled = true;
row.classList.add('opacity-50');
// Dodaj spinner tylko jeśli nie ma
let existingSpinner = row.querySelector('.spinner-border');
if (!existingSpinner) {
const spinner = document.createElement('span');
spinner.className = 'spinner-border spinner-border-sm ms-2';
spinner.setAttribute('role', 'status');
spinner.setAttribute('aria-hidden', 'true');
checkbox.parentElement.appendChild(spinner);
}
});
});

View File

@@ -138,12 +138,20 @@ function setupList(listId, username) {
quantityBadge = `<span class="badge bg-secondary">x${data.quantity}</span>`;
}
const countdownId = `countdown-${data.id}`;
const countdownBtn = `
<button type="button" class="btn btn-outline-warning" id="${countdownId}" disabled>15s</button>
`;
li.innerHTML = `
<div class="d-flex align-items-center flex-wrap gap-2 flex-grow-1">
<input class="large-checkbox" type="checkbox">
<span id="name-${data.id}" class="text-white">${data.name} ${quantityBadge}</span>
<span id="name-${data.id}" class="text-white">
${data.name} ${quantityBadge}
</span>
</div>
<div class="btn-group btn-group-sm" role="group">
${countdownBtn}
<button type="button" class="btn btn-outline-light"
onclick="editItem(${data.id}, '${data.name.replace(/'/g, "\\'")}', ${data.quantity || 1})">
✏️
@@ -155,21 +163,33 @@ function setupList(listId, username) {
</div>
`;
// góra listy
//document.getElementById('items').prepend(li);
// dół listy
document.getElementById('items').appendChild(li);
toggleEmptyPlaceholder();
// ⏳ Licznik odliczania
let seconds = 15;
const countdownEl = document.getElementById(countdownId);
const intervalId = setInterval(() => {
seconds--;
if (countdownEl) {
countdownEl.textContent = `${seconds}s`;
}
if (seconds <= 0) {
clearInterval(intervalId);
if (countdownEl) countdownEl.remove();
}
}, 1000);
// 🔁 Request listy po 15s
setTimeout(() => {
if (window.LIST_ID) {
socket.emit('request_full_list', { list_id: window.LIST_ID });
}
}, 15000);
});
socket.on('item_deleted', data => {
const li = document.getElementById(`item-${data.item_id}`);
if (li) {

View File

@@ -7,32 +7,46 @@ document.addEventListener("DOMContentLoaded", () => {
async function analyzeReceipts(listId) {
const resultsDiv = document.getElementById("analysisResults");
resultsDiv.innerHTML = `<div class="text-info">⏳ Trwa analiza paragonów...</div>`;
resultsDiv.innerHTML = `
<div class="text-info d-flex align-items-center gap-2">
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
<span>Trwa analiza paragonów...</span>
</div>`;
const start = performance.now(); // ⏱ START
const start = performance.now();
try {
const res = await fetch(`/lists/${listId}/analyze`, { method: "POST" });
const data = await res.json();
const duration = ((performance.now() - start) / 1000).toFixed(2);
const duration = ((performance.now() - start) / 1000).toFixed(2); // ⏱ STOP
let html = `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
let html = `<div class="card bg-dark text-white border-secondary p-3">`;
html += `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
html += `<p class="text-secondary"><small>⏱ Czas analizy OCR: ${duration} sek.</small></p>`;
data.results.forEach((r, i) => {
const disabled = r.already_added ? "disabled" : "";
const inputStyle = "form-control d-inline-block bg-dark text-white border-light rounded";
const inputField = `<input type="number" id="amount-${i}" value="${r.amount}" step="0.01" class="${inputStyle}" style="width: 120px;" ${disabled}>`;
const button = r.already_added
? `<span class="badge bg-primary ms-2">✅ Dodano</span>`
: `<button id="add-btn-${i}" onclick="emitExpense(${i})" class="btn btn-sm btn-outline-success ms-2"> Dodaj</button>`;
html += `
<div class="mb-2">
<span class="text-light">${r.filename}</span>:
<input type="number" id="amount-${i}" value="${r.amount}" step="0.01" class="form-control d-inline-block bg-dark text-white border-light rounded" style="width: 120px;">
<button onclick="emitExpense(${i})" class="btn btn-sm btn-outline-success ms-2"> Dodaj</button>
</div>`;
<div class="mb-2 d-flex align-items-center gap-2 flex-wrap">
<span class="text-light flex-grow-1">${r.filename}</span>
${inputField}
${button}
</div>`;
});
if (data.results.length > 1) {
html += `<button onclick="emitAllExpenses(${data.results.length})" class="btn btn-success mt-3"> Dodaj wszystkie</button>`;
html += `<button id="addAllBtn" onclick="emitAllExpenses(${data.results.length})" class="btn btn-success mt-3 w-100"> Dodaj wszystkie</button>`;
}
html += `</div>`;
resultsDiv.innerHTML = html;
window._ocr_results = data.results;
@@ -42,22 +56,44 @@ async function analyzeReceipts(listId) {
}
}
function emitExpense(i) {
const r = window._ocr_results[i];
const val = parseFloat(document.getElementById(`amount-${i}`).value);
const btn = document.getElementById(`add-btn-${i}`);
if (!isNaN(val) && val > 0) {
socket.emit('add_expense', {
list_id: LIST_ID,
amount: val
amount: val,
receipt_filename: r.filename
});
document.getElementById(`amount-${i}`).disabled = true;
if (btn) {
btn.disabled = true;
btn.classList.remove('btn-outline-success');
btn.classList.add('btn-success');
btn.textContent = '✅ Dodano';
}
}
}
function emitAllExpenses(n) {
for (let i = 0; i < n; i++) {
emitExpense(i);
const btnAll = document.getElementById('addAllBtn');
if (btnAll) {
btnAll.disabled = true;
btnAll.innerHTML = `<span class="spinner-border spinner-border-sm me-2" role="status"></span>Dodawanie...`;
}
for (let i = 0; i < n; i++) {
setTimeout(() => emitExpense(i), i * 150);
}
setTimeout(() => {
if (btnAll) {
btnAll.innerHTML = '✅ Wszystko dodano';
btnAll.classList.remove('btn-success');
btnAll.classList.add('btn-outline-success');
}
}, n * 150 + 300);
}