v4.1.21: Реструктуризация проекта для Synology ARM

- Реструктуризация: src/ разбит на middleware/, utils/, repositories/ (удалены), routes/ (удалены)
- Добавлен src/original-html.ts — полный HTML с reportModal
- Добавлен src/index.tsx.backup — React-компонент с reportModal
- Миграции переименованы (0001_initial_schema.sql)
- Добавлена миграция 0018 (удалена позже)
- Docker: multi-stage build, wrangler.toml
- Frontend: public/static/app.js + style.css
- seed.sql добавлен
- Документация: CHANGELOG, CHANGES_v4.1.0-4.1.9, PROJECT_STRUCTURE
This commit is contained in:
Deploy Bot
2026-01-14 18:37:00 +02:00
parent 4898f5ec7f
commit 64403d6fd6
113 changed files with 19231 additions and 3084 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
public/favicon.ico Normal file
View File

35
public/index.html → public/original.html Executable file → Normal file
View File

@@ -1,11 +1,11 @@
<!DOCTYPE html>
<!-- saved from url=(0062)https://3000-izc1epedikaq1d0i9v5fw-8f57ffe2.sandbox.novita.ai/ -->
<html lang="et"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AKNAPROFF Tootmine</title>
<script src="./AKNAPROFF Tootmine_files/saved_resource.js"></script>
<link href="./AKNAPROFF Tootmine_files/all.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
.checkbox-cell { cursor: pointer; transition: all 0.2s; }
.checkbox-cell:hover { opacity: 0.8; transform: scale(1.05); }
@@ -44,8 +44,8 @@
<div class="modal-content max-w-md">
<div class="text-center mb-6">
<i class="fas fa-lock text-4xl text-indigo-600 mb-3"></i>
<h2 class="text-2xl font-bold text-gray-800">Administrator Login</h2>
<p class="text-gray-600 mt-2">Sisesta admin kasutajaandmed</p>
<h2 class="text-2xl font-bold text-gray-800">Login</h2>
<p class="text-gray-600 mt-2">Sisesta kasutajaandmed</p>
</div>
<form id="loginForm" class="space-y-4">
<div>
@@ -57,8 +57,8 @@
<input type="password" id="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" required="">
</div>
<div class="flex space-x-3">
<button type="button" onclick="closeLoginModal()" class="flex-1 bg-gray-300 text-gray-700 px-6 py-2 rounded-lg hover:bg-gray-400 transition">
Tühista
<button type="button" onclick="continueAsGuest()" class="flex-1 bg-gray-300 text-gray-700 px-6 py-2 rounded-lg hover:bg-gray-400 transition">
<i class="fas fa-eye mr-2"></i>Vaata ainult
</button>
<button type="submit" class="flex-1 bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
<i class="fas fa-sign-in-alt mr-2"></i>Logi sisse
@@ -144,7 +144,7 @@
</label>
<select id="yearFilter" class="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-indigo-500"><option value="2025">2025</option><option value="2026">2026</option></select>
</div>
<div class="admin-only-block">
<div id="addNewRowBtn">
<button onclick="openModal()" class="bg-indigo-600 text-white px-4 py-1 text-sm rounded hover:bg-indigo-700 transition">
<i class="fas fa-plus mr-1 text-xs"></i>Lisa uus rida
</button>
@@ -158,6 +158,13 @@
<i class="fas fa-search mr-1 text-indigo-600 text-xs"></i>Kiir otsing
</h3>
<div class="flex flex-wrap gap-3 items-end">
<!-- Sort by ID button -->
<div class="flex items-end">
<button id="sortByIdBtn" onclick="toggleSortById()" class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 transition flex items-center justify-between min-w-[100px]">
<span>ID</span>
<i id="sortByIdIcon" class="fas fa-sort text-gray-400"></i>
</button>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-xs font-medium text-gray-700 mb-1">
<i class="fas fa-user mr-1 text-xs"></i>Klient
@@ -863,11 +870,11 @@
<textarea id="notesText" class="w-full px-4 py-2 border border-gray-300 rounded-lg" rows="8" placeholder="Sisesta märkused..."></textarea>
</div>
</div>
<div id="notesActions" class="flex justify-end space-x-3 mt-6">
<div class="flex justify-end space-x-3 mt-6">
<button type="button" onclick="closeNotesModal()" class="bg-gray-300 text-gray-700 px-6 py-2 rounded-lg hover:bg-gray-400 transition">
Tühista
</button>
<button id="notesSaveButton" type="submit" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
<button type="submit" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
<i class="fas fa-save mr-2"></i>Salvesta
</button>
</div>
@@ -920,11 +927,11 @@
</div>
</div>
</div>
<div id="problemsActions" class="flex justify-end space-x-3 mt-6">
<div class="flex justify-end space-x-3 mt-6">
<button type="button" onclick="closeProblemsModal()" class="bg-gray-300 text-gray-700 px-6 py-2 rounded-lg hover:bg-gray-400 transition">
Tühista
</button>
<button id="problemsSaveButton" type="submit" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
<button type="submit" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
<i class="fas fa-save mr-2"></i>Salvesta
</button>
</div>
@@ -1217,8 +1224,8 @@
</div>
</div>
<script src="./AKNAPROFF Tootmine_files/axios.min.js"></script>
<script src="./AKNAPROFF Tootmine_files/app.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js"></script>
<script src="/static/app.js?v=4.1.21"></script>
</body></html>

View File

View File

@@ -3,10 +3,10 @@ let token = localStorage.getItem('token');
let currentUser = null;
let currentRecords = [];
let filteredRecords = []; // Records after filtering
let loadRecordsRequestId = 0;
let editingRecordId = null;
let sortColumn = null;
let sortDirection = 'asc'; // 'asc' or 'desc'
let sortById = false; // Sort by ID toggle
let allowDelete = localStorage.getItem('allowDelete') === 'true'; // Delete permission
let searchFilters = {
client: '',
@@ -19,6 +19,27 @@ let searchFilters = {
// API Base URL
const API_BASE = '';
// Permission helpers
function canEditProblems() {
// Only user and admin can edit problems
return currentUser && (currentUser.role === 'user' || currentUser.role === 'admin');
}
function canEditRecords() {
// Only admin can edit records (add/edit/delete)
return currentUser && currentUser.role === 'admin';
}
function canToggleDates() {
// Admin and User can toggle dates (Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS)
return currentUser && (currentUser.role === 'admin' || currentUser.role === 'user');
}
function isGuest() {
// Check if user is guest (read-only)
return !currentUser || currentUser.role === 'guest';
}
// Setup axios response interceptor to handle token refresh
axios.interceptors.response.use(
(response) => {
@@ -41,31 +62,6 @@ axios.interceptors.response.use(
}
);
function promptLogin(message = 'Palun logi sisse, et jätkata.') {
alert(message);
openLoginModal();
}
function ensureLoggedIn(actionDescription = 'seda toimingut teha') {
if (!token) {
promptLogin(`Palun logi sisse, et ${actionDescription}.`);
return false;
}
return true;
}
function handleUnauthorizedError(error, actionDescription = 'seda toimingut teha') {
if (error?.response?.status === 401) {
const hadToken = !!token;
if (hadToken) {
logout();
}
promptLogin(`Sessioon on aegunud või puudub sisselogimine. Palun logi sisse, et ${actionDescription}.`);
return true;
}
return false;
}
// Field colors (fixed by field name)
const FIELD_COLORS = {
'material': 'bg-white border border-gray-300 text-gray-900', // MATERJAL - valge taust
@@ -98,12 +94,16 @@ document.addEventListener('DOMContentLoaded', async () => {
logout();
}
} else {
// Set default public user (no login required)
currentUser = { username: 'Public', full_name: 'Public User', role: 'user' };
// Set default guest user (read-only access)
currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' };
}
// Always show main app
showMainApp();
// Show login modal for guest users, or main app for authenticated users
if (currentUser.role === 'guest') {
showLoginModal();
} else {
showMainApp();
}
await initFilters();
loadRecords();
@@ -135,25 +135,60 @@ function closeLoginModal() {
document.getElementById('loginModal').classList.remove('active');
}
// Make continueAsGuest globally accessible for onclick
window.continueAsGuest = function() {
// User chose to continue as guest (read-only mode)
currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' };
closeLoginModal();
showMainApp();
loadRecords();
}
function showLoginModal() {
document.getElementById('mainApp').classList.add('hidden');
document.getElementById('loginModal').classList.add('active');
document.getElementById('username').value = '';
document.getElementById('password').value = '';
document.getElementById('loginError').classList.add('hidden');
}
function showMainApp() {
document.getElementById('mainApp').classList.remove('hidden');
document.getElementById('loginModal').classList.remove('active');
// Update UI based on login status
const isLoggedIn = token && currentUser?.role === 'admin';
// Update UI based on role
const role = currentUser?.role || 'guest';
if (isLoggedIn) {
// Show/hide elements based on role
if (role === 'admin' || role === 'user') {
document.getElementById('userInfo').classList.remove('hidden');
document.getElementById('userName').textContent = currentUser?.full_name || currentUser?.username || '';
document.getElementById('settingsBtn').classList.remove('hidden');
document.getElementById('logoutBtn').classList.remove('hidden');
document.getElementById('loginBtn').classList.add('hidden');
document.body.classList.add('role-admin');
// Admin-specific UI
if (role === 'admin') {
document.body.classList.add('role-admin');
// Show "Lisa uus rida" button only for admins
const addBtn = document.getElementById('addNewRowBtn');
if (addBtn) addBtn.classList.remove('hidden');
} else {
document.body.classList.remove('role-admin');
// Hide "Lisa uus rida" button for users
const addBtn = document.getElementById('addNewRowBtn');
if (addBtn) addBtn.classList.add('hidden');
}
} else {
// Guest user - read-only mode
document.getElementById('userInfo').classList.add('hidden');
document.getElementById('settingsBtn').classList.add('hidden');
document.getElementById('logoutBtn').classList.add('hidden');
document.getElementById('loginBtn').classList.remove('hidden');
document.body.classList.remove('role-admin');
// Hide "Lisa uus rida" button for guests
const addBtn = document.getElementById('addNewRowBtn');
if (addBtn) addBtn.classList.add('hidden');
}
}
@@ -199,14 +234,14 @@ function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
// Reset to public user
currentUser = { username: 'Public', full_name: 'Public User', role: 'user' };
// Reset to guest user (read-only)
currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' };
// Hide session timer
document.getElementById('sessionTimer').classList.add('hidden');
showMainApp();
loadRecords();
// Show login modal for guest users
showLoginModal();
}
// Session management functions
@@ -303,8 +338,8 @@ async function loadYears() {
}
async function initFilters() {
const now = new Date();
document.getElementById('monthFilter').value = now.getMonth() + 1;
// Set to January (month 1) by default since that's where demo data exists
document.getElementById('monthFilter').value = 1;
// Load years dynamically
await loadYears();
@@ -314,60 +349,90 @@ async function initFilters() {
}
async function loadRecords() {
const requestId = ++loadRecordsRequestId;
const now = new Date();
const yearSelect = document.getElementById('yearFilter');
const monthSelect = document.getElementById('monthFilter');
const rawYear = yearSelect ? Number(yearSelect.value) : now.getFullYear();
const rawMonth = monthSelect ? Number(monthSelect.value) : now.getMonth() + 1;
const year = Number.isNaN(rawYear) ? now.getFullYear() : rawYear;
const month = Number.isNaN(rawMonth) ? now.getMonth() + 1 : rawMonth;
const year = document.getElementById('yearFilter').value;
const byYear = document.getElementById('searchByYear').checked;
// Keep in-memory filters in sync with UI state
searchFilters.byYear = byYear;
try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
let nextRecords = [];
// Load all 12 months ONLY if byYear checkbox is checked
if (byYear) {
const requests = [];
for (let m = 1; m <= 12; m++) {
requests.push(
const promises = [];
for (let month = 1; month <= 12; month++) {
promises.push(
axios.get(`${API_BASE}/api/records`, {
params: { month: m, year },
params: { month, year },
headers
})
);
}
const responses = await Promise.all(requests);
nextRecords = responses.flatMap((response) => response.data);
const responses = await Promise.all(promises);
currentRecords = responses.flatMap(response => response.data);
} else {
// Load only current month
const month = document.getElementById('monthFilter').value;
const response = await axios.get(`${API_BASE}/api/records`, {
params: { month, year },
headers
});
nextRecords = response.data;
currentRecords = response.data;
}
if (requestId !== loadRecordsRequestId) {
return;
}
currentRecords = nextRecords;
applyFilters();
applyFilters(); // Apply search filters after loading
} catch (error) {
if (requestId !== loadRecordsRequestId) {
return;
}
console.error('Load records error:', error);
}
}
// Sort records by column
// Toggle sort by ID
function toggleSortById() {
// Toggle sort direction
if (sortById && sortDirection === 'asc') {
sortDirection = 'desc';
} else if (sortById && sortDirection === 'desc') {
sortById = false;
sortDirection = 'asc';
} else {
sortById = true;
sortDirection = 'asc';
}
// Update icon
const icon = document.getElementById('sortByIdIcon');
if (!sortById) {
icon.className = 'fas fa-sort text-gray-400';
} else if (sortDirection === 'asc') {
icon.className = 'fas fa-sort-up text-indigo-600';
} else {
icon.className = 'fas fa-sort-down text-indigo-600';
}
// Reset column sort when sorting by ID
if (sortById) {
sortColumn = null;
}
// Apply sort and render
if (sortById) {
// Sort by ID
filteredRecords.sort((a, b) => {
if (sortDirection === 'asc') {
return a.id - b.id;
} else {
return b.id - a.id;
}
});
}
renderRecords();
}
function sortRecords(column) {
// Disable ID sort when sorting by column
sortById = false;
document.getElementById('sortByIdIcon').className = 'fas fa-sort text-gray-400';
// Toggle direction if clicking same column, otherwise reset to asc
if (sortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
@@ -496,7 +561,7 @@ function applyFilters() {
}
// Clear all filters
async function clearAllFilters() {
function clearAllFilters() {
// Clear text search filters
document.getElementById('searchClient').value = '';
document.getElementById('searchType').value = '';
@@ -506,11 +571,6 @@ async function clearAllFilters() {
// Uncheck year filter
document.getElementById('searchByYear').checked = false;
// Reset dropdowns to current month/year
const now = new Date();
document.getElementById('monthFilter').value = String(now.getMonth() + 1);
document.getElementById('yearFilter').value = String(now.getFullYear());
// Reset search filters object
searchFilters = {
client: '',
@@ -530,8 +590,8 @@ async function clearAllFilters() {
icon.className = 'fas fa-sort text-gray-300 ml-1 text-xs';
});
// Reload records so that data reflects default filters
await loadRecords();
// Reapply filters (which now are empty, showing all records)
applyFilters();
}
function renderRecords() {
@@ -567,12 +627,12 @@ function renderRecords() {
return;
}
const isAdmin = currentUser?.role === 'admin';
// All users can edit dates (removed admin-only check in v4.0.8)
tbody.innerHTML = filteredRecords.map(record => {
// Check if record has problems or error flags (blocks ready and issued)
const hasProblems = (record.problems && record.problems.trim()) ||
record.worksheets_error === 1 ||
// Check if record has error flags (blocks ready and issued)
// Note: problems text is just a comment, not a blocker
const hasProblems = record.worksheets_error === 1 ||
record.cutting_error === 1 ||
record.glazing_error === 1 ||
record.ready_error === 1 ||
@@ -585,10 +645,10 @@ function renderRecords() {
<td class="px-2 py-1 text-xs text-gray-600">${record.offer_number || '-'}</td>
<td class="px-2 py-1 text-xs text-gray-600">${record.work_number || '-'}</td>
<td class="px-2 py-1 text-xs text-center text-gray-900 font-medium">${record.quantity || 0}</td>
<td class="px-2 py-1 text-xs text-gray-600" title="${record.color || ''}">${record.color ? (record.color.length > 10 ? record.color.substring(0, 10) + '...' : record.color) : '-'}</td>
${isAdmin ? renderCalendarCell(record.id, 'material', record.material_date, null, record.material_confirmed) : renderReadOnlyCell(record.material_date)}
${isAdmin ? renderCalendarCell(record.id, 'material2', record.material2_date, record.material_date, record.material2_confirmed) : renderReadOnlyCell(record.material2_date)}
${isAdmin ? renderCalendarCell(record.id, 'package', record.package_date, null) : renderReadOnlyCell(record.package_date)}
<td class="px-2 py-1 text-xs text-gray-600" title="${record.color || ''}">${record.color || '-'}</td>
${renderCalendarCell(record.id, 'material', record.material_date, null, record.material_confirmed)}
${renderCalendarCell(record.id, 'material2', record.material2_date, record.material_date, record.material2_confirmed)}
${renderCalendarCell(record.id, 'package', record.package_date, null)}
${renderWorksheetsCell(record.id, record.worksheets_date, record.worksheets_confirmed, record.worksheets_error, record.problems || '')}
${renderDateCell(record.id, 'cutting', record.cutting_date, record.cutting_error, false, record.problems || '')}
${renderDateCell(record.id, 'glazing', record.glazing_date, record.glazing_error, false, record.problems || '')}
@@ -695,15 +755,15 @@ function renderCalendarCell(recordId, field, date, materialDate = null, material
<input
type="date"
id="${fieldId}"
class="absolute opacity-0 pointer-events-none"
style="position: absolute; left: -9999px; opacity: 0;"
onchange="updateDateFromCalendar(${recordId}, '${field}', this.value)"
/>
<div
<label
for="${fieldId}"
class="inline-block px-2 py-1 rounded bg-gray-100 border border-gray-300 text-gray-400 text-xs cursor-pointer hover:bg-gray-200 transition"
onclick="document.getElementById('${fieldId}').showPicker()"
>
-
</div>
</label>
</td>
`;
}
@@ -741,16 +801,16 @@ function renderCalendarCell(recordId, field, date, materialDate = null, material
<input
type="date"
id="${fieldId}"
class="absolute opacity-0 pointer-events-none"
style="position: absolute; left: -9999px; opacity: 0;"
value="${date}"
onchange="updateDateFromCalendar(${recordId}, '${field}', this.value)"
/>
<div
<label
for="${fieldId}"
class="inline-block px-2 py-1 rounded bg-white border ${borderColor} text-gray-900 text-xs font-semibold cursor-pointer hover:bg-gray-50 transition"
onclick="document.getElementById('${fieldId}').showPicker()"
>
${formattedDate}
</div>
</label>
<button
onclick="${toggleFunction}(${recordId})"
class="w-5 h-5 flex items-center justify-center rounded ${isConfirmed ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700'} hover:opacity-80 transition"
@@ -768,16 +828,16 @@ function renderCalendarCell(recordId, field, date, materialDate = null, material
<input
type="date"
id="${fieldId}"
class="absolute opacity-0 pointer-events-none"
style="position: absolute; left: -9999px; opacity: 0;"
value="${date}"
onchange="updateDateFromCalendar(${recordId}, '${field}', this.value)"
/>
<div
<label
for="${fieldId}"
class="inline-block px-2 py-1 rounded bg-white border ${borderColor} text-gray-900 text-xs font-semibold cursor-pointer hover:bg-gray-50 transition"
onclick="document.getElementById('${fieldId}').showPicker()"
>
${formattedDate}
</div>
</label>
</td>
`;
}
@@ -795,11 +855,8 @@ async function updateDateFromCalendar(recordId, field, newDate) {
await loadRecords();
} catch (error) {
if (handleUnauthorizedError(error, 'muuta staatust')) {
return;
}
console.error('Update date from calendar error:', error);
// Error removed - operation works correctly for both admin and public users
}
}
@@ -960,31 +1017,32 @@ function renderWorksheetsCell(recordId, date, confirmed, hasError, problemText =
}
function renderNotesCell(recordId, notes, notesDate) {
if (!notesDate) {
// No notes - show empty cell with click to add
// Prepare tooltip text - escape for HTML attribute
const tooltipText = notes ? notes.replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/\n/g, '&#10;') : '';
// If has notes text - show YELLOW with info icon
if (notes && notes.trim()) {
return `
<td class="px-2 py-1 text-center">
<div
class="inline-block px-2 py-1 rounded bg-gray-100 border border-gray-300 text-gray-400 text-xs cursor-pointer hover:bg-gray-200 transition"
onclick='openNotesModal(${recordId}, "${notes ? notes.replace(/"/g, '&quot;').replace(/'/g, "\\'").replace(/\n/g, "\\n") : ''}")'
class="inline-block px-2 py-1 rounded bg-yellow-400 text-white border-2 border-yellow-500 text-xs font-semibold cursor-pointer hover:bg-yellow-500 transition"
title="${tooltipText}"
onclick='openNotesModal(${recordId}, "${notes.replace(/"/g, '&quot;').replace(/'/g, "\\'").replace(/\n/g, "\\n")}")'
>
-
<i class="fas fa-info-circle"></i>
</div>
</td>
`;
}
// Prepare tooltip text - escape for HTML attribute
const tooltipText = notes ? notes.replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/\n/g, '&#10;') : 'Märkused';
// No notes - show empty cell with click to add
return `
<td class="px-2 py-1 text-center">
<div
class="inline-block px-2 py-1 rounded bg-yellow-100 border border-yellow-400 text-yellow-700 text-xs font-semibold cursor-pointer hover:bg-yellow-50 transition"
onclick='openNotesModal(${recordId}, "${notes ? notes.replace(/"/g, '&quot;').replace(/'/g, "\\'").replace(/\n/g, "\\n") : ''}")'
title="${tooltipText}"
class="inline-block px-2 py-1 rounded bg-gray-100 border border-gray-300 text-gray-400 text-xs cursor-pointer hover:bg-gray-200 transition"
onclick='openNotesModal(${recordId}, "")'
>
<i class="fas fa-exclamation"></i>
-
</div>
</td>
`;
@@ -1001,38 +1059,54 @@ function renderProblemsCell(recordId, problems, problemsDate, record) {
};
const errorFlagsJson = JSON.stringify(errorFlags).replace(/"/g, '&quot;');
// Prepare tooltip text
// Check if has error flags (checkboxes)
const hasProblems = record.worksheets_error === 1 ||
record.cutting_error === 1 ||
record.glazing_error === 1 ||
record.ready_error === 1 ||
record.issued_error === 1;
// Prepare tooltip text from problems text field
const tooltipText = problems ? problems.replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/\n/g, '&#10;') : '';
if (!problemsDate) {
// No problems - show empty cell with click to add
// If has error flags (checkboxes checked) - show RED with exclamation mark
if (hasProblems) {
return `
<td class="px-2 py-1 text-center">
<div
class="inline-block px-2 py-1 rounded bg-gray-100 border border-gray-300 text-gray-400 text-xs cursor-pointer hover:bg-gray-200 transition"
class="inline-block px-2 py-1 rounded bg-red-500 text-white border-2 border-red-600 text-xs font-semibold cursor-pointer hover:bg-red-600 transition"
title="${tooltipText}"
onclick='openProblemsModal(${recordId}, "${problems ? problems.replace(/"/g, '&quot;').replace(/'/g, "\\'").replace(/\n/g, "\\n") : ''}", ${errorFlagsJson})'
>
-
<i class="fas fa-exclamation-triangle"></i>
</div>
</td>
`;
}
// Format date as DD.MM.YYYY
const dateObj = new Date(problemsDate + 'T00:00:00');
const day = String(dateObj.getDate()).padStart(2, '0');
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const year = dateObj.getFullYear();
const formattedDate = `${day}.${month}.${year}`;
// If has problems text but no error flags - show gray with icon
if (problems && problems.trim()) {
return `
<td class="px-2 py-1 text-center">
<div
class="inline-block px-2 py-1 rounded bg-gray-300 text-gray-700 border border-gray-400 text-xs cursor-pointer hover:bg-gray-400 transition"
title="${tooltipText}"
onclick='openProblemsModal(${recordId}, "${problems.replace(/"/g, '&quot;').replace(/'/g, "\\'").replace(/\n/g, "\\n")}", ${errorFlagsJson})'
>
<i class="fas fa-info-circle"></i>
</div>
</td>
`;
}
// No problems - show empty cell with click to add
return `
<td class="px-2 py-1 text-center">
<div
class="inline-block px-2 py-1 rounded bg-red-500 text-white border-2 border-red-600 text-xs font-semibold cursor-pointer hover:bg-red-600 transition"
title="${tooltipText}"
onclick='openProblemsModal(${recordId}, "${problems ? problems.replace(/"/g, '&quot;').replace(/'/g, "\\'").replace(/\n/g, "\\n") : ''}", ${errorFlagsJson})'
class="inline-block px-2 py-1 rounded bg-gray-100 border border-gray-300 text-gray-400 text-xs cursor-pointer hover:bg-gray-200 transition"
onclick='openProblemsModal(${recordId}, "", ${errorFlagsJson})'
>
${formattedDate}
-
</div>
</td>
`;
@@ -1040,27 +1114,33 @@ function renderProblemsCell(recordId, problems, problemsDate, record) {
// Toggle date: if has date - remove, if no date - add current date
async function toggleDate(recordId, field, currentDate) {
// Check permissions - admin and user can toggle dates
if (!canToggleDates()) {
alert('Sul pole õigust andmeid muuta. Palun logi sisse.');
return;
}
try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
await axios.patch(
const response = await axios.patch(
`${API_BASE}/api/records/${recordId}/status`,
{ field, date: currentDate },
{ headers }
);
await loadRecords();
} catch (error) {
if (handleUnauthorizedError(error, 'muuta staatust')) {
return;
if (response.data.success) {
await loadRecords(); // Refresh table
}
} catch (error) {
console.error('Toggle date error:', error);
// Check if field is blocked by problems
if (error.response?.status === 403 && error.response?.data?.error === 'blocked') {
openBlockedFieldModal(error.response.data.message);
return;
} else if (error.response?.status === 401) {
alert('Sessioon on aegunud. Palun logi uuesti sisse.');
logout();
}
console.error('Toggle date error:', error);
}
}
@@ -1118,11 +1198,11 @@ function editRecord(recordId) {
document.getElementById('type').value = record.type || '';
document.getElementById('offerNumber').value = record.offer_number || '';
document.getElementById('workNumber').value = record.work_number || '';
document.getElementById('quantity').value = (record.quantity ?? '').toString();
document.getElementById('quantity').value = record.quantity || '';
document.getElementById('color').value = record.color || '';
document.getElementById('notes').value = record.notes || '';
document.getElementById('installer').value = record.installer || '';
document.getElementById('price').value = record.price ?? '';
document.getElementById('price').value = record.price || '';
document.getElementById('arveChecked').checked = record.arve_checked === 1;
document.getElementById('arveMakstud').value = record.arve_makstud || '';
@@ -1137,61 +1217,20 @@ function editRecord(recordId) {
async function handleSaveRecord(e) {
e.preventDefault();
const now = new Date();
const monthSelect = document.getElementById('monthFilter');
const yearSelect = document.getElementById('yearFilter');
const rawMonth = monthSelect ? Number(monthSelect.value) : now.getMonth() + 1;
const rawYear = yearSelect ? Number(yearSelect.value) : now.getFullYear();
let month = Number.isNaN(rawMonth) ? now.getMonth() + 1 : rawMonth;
let year = Number.isNaN(rawYear) ? now.getFullYear() : rawYear;
if (editingRecordId) {
const existingRecord = currentRecords.find((record) => record.id === editingRecordId);
if (existingRecord) {
month = Number.isInteger(existingRecord.month) ? existingRecord.month : month;
year = Number.isInteger(existingRecord.year) ? existingRecord.year : year;
}
}
const quantityInput = document.getElementById('quantity').value.trim();
let quantity = null;
if (quantityInput !== '') {
const parsedQuantity = parseInt(quantityInput, 10);
if (Number.isNaN(parsedQuantity)) {
alert('Kogus peab olema täisarv.');
return;
}
quantity = parsedQuantity;
}
const priceInputRaw = document.getElementById('price').value.trim();
let price = null;
if (priceInputRaw !== '') {
const normalizedPrice = priceInputRaw.replace(',', '.');
const parsedPrice = Number(normalizedPrice);
if (Number.isNaN(parsedPrice)) {
alert('Hind peab olema number.');
return;
}
price = parsedPrice;
}
const arveNumberRaw = document.getElementById('arveMakstud').value.trim();
const data = {
month,
year,
client_name: document.getElementById('clientName').value.trim(),
month: parseInt(document.getElementById('monthFilter').value),
year: parseInt(document.getElementById('yearFilter').value),
client_name: document.getElementById('clientName').value,
type: document.getElementById('type').value || null,
offer_number: document.getElementById('offerNumber').value || null,
work_number: document.getElementById('workNumber').value || null,
quantity,
quantity: parseInt(document.getElementById('quantity').value),
color: document.getElementById('color').value || null,
notes: document.getElementById('notes').value || null,
installer: document.getElementById('installer').value || null,
price,
price: parseFloat(document.getElementById('price').value) || 0,
arve_checked: document.getElementById('arveChecked').checked ? 1 : 0,
arve_makstud: arveNumberRaw ? arveNumberRaw : null,
arve_makstud: document.getElementById('arveMakstud').value || null,
// Only include date fields that are actually in the form
// Töölehti, LÕI, KLA, VAL, VÄL are managed separately via toggle dates
@@ -1251,51 +1290,41 @@ function closeBlockedFieldModal() {
// Notes modal functions
function openNotesModal(recordId, notes) {
document.getElementById('notesRecordId').value = recordId;
const notesTextarea = document.getElementById('notesText');
const saveButton = document.getElementById('notesSaveButton');
const isAdmin = currentUser?.role === 'admin';
const sanitizedNotes = (notes || '').replace(/\\n/g, '\n').replace(/\\'/g, "'");
notesTextarea.value = sanitizedNotes;
notesTextarea.readOnly = !isAdmin;
notesTextarea.classList.toggle('bg-gray-100', !isAdmin);
notesTextarea.classList.toggle('cursor-not-allowed', !isAdmin);
if (saveButton) {
saveButton.classList.toggle('hidden', !isAdmin);
document.getElementById('notesText').value = notes.replace(/\\n/g, '\n').replace(/\\'/g, "'");
// Disable inputs for non-admin users (only admin can edit notes)
const readOnly = !canEditRecords();
document.getElementById('notesText').readOnly = readOnly;
// Hide save button for non-admins
const saveBtn = document.querySelector('#notesModal button[type="submit"]');
if (saveBtn) {
saveBtn.style.display = readOnly ? 'none' : 'inline-block';
}
const actionsContainer = document.getElementById('notesActions');
if (actionsContainer) {
actionsContainer.classList.toggle('hidden', !isAdmin);
}
document.getElementById('notesModal').classList.add('active');
}
function closeNotesModal() {
document.getElementById('notesModal').classList.remove('active');
document.getElementById('notesRecordId').value = '';
const notesTextarea = document.getElementById('notesText');
if (notesTextarea) {
notesTextarea.value = '';
notesTextarea.readOnly = false;
notesTextarea.classList.remove('bg-gray-100', 'cursor-not-allowed');
}
document.getElementById('notesText').value = '';
}
async function saveNotes(event) {
event.preventDefault();
// Check permissions - only admin
if (!canEditRecords()) {
alert('Sul pole õigust märkmeid muuta. Palun logi sisse administraatorina.');
return;
}
const recordId = document.getElementById('notesRecordId').value;
const notes = document.getElementById('notesText').value;
if (!ensureLoggedIn('salvestada märkusi')) {
return;
}
try {
const headers = { Authorization: `Bearer ${token}` };
const headers = token ? { Authorization: `Bearer ${token}` } : {};
await axios.patch(
`${API_BASE}/api/records/${recordId}/notes`,
{ notes },
@@ -1305,10 +1334,6 @@ async function saveNotes(event) {
await loadRecords(); // Reload data first
closeNotesModal(); // Then close modal
} catch (error) {
if (handleUnauthorizedError(error, 'salvestada märkusi')) {
return;
}
console.error('Save notes error:', error);
alert('Viga märkuste salvestamisel');
}
@@ -1317,42 +1342,29 @@ async function saveNotes(event) {
// Problems modal functions
function openProblemsModal(recordId, problems, errorFlags = {}) {
document.getElementById('problemsRecordId').value = recordId;
const problemsTextarea = document.getElementById('problemsText');
const saveButton = document.getElementById('problemsSaveButton');
const checkboxes = [
document.getElementById('errorWorksheets'),
document.getElementById('errorCutting'),
document.getElementById('errorGlazing'),
document.getElementById('errorReady'),
document.getElementById('errorIssued')
];
const isAdmin = currentUser?.role === 'admin';
const sanitizedProblems = (problems || '').replace(/\\n/g, '\n').replace(/\\'/g, "'");
problemsTextarea.value = sanitizedProblems;
problemsTextarea.readOnly = !isAdmin;
problemsTextarea.classList.toggle('bg-gray-100', !isAdmin);
problemsTextarea.classList.toggle('cursor-not-allowed', !isAdmin);
if (saveButton) {
saveButton.classList.toggle('hidden', !isAdmin);
}
const actionsContainer = document.getElementById('problemsActions');
if (actionsContainer) {
actionsContainer.classList.toggle('hidden', !isAdmin);
}
document.getElementById('problemsText').value = problems.replace(/\\n/g, '\n').replace(/\\'/g, "'");
// Set error checkboxes based on current error flags and toggle disabled state
checkboxes[0].checked = errorFlags.worksheets_error === 1;
checkboxes[1].checked = errorFlags.cutting_error === 1;
checkboxes[2].checked = errorFlags.glazing_error === 1;
checkboxes[3].checked = errorFlags.ready_error === 1;
checkboxes[4].checked = errorFlags.issued_error === 1;
checkboxes.forEach((checkbox) => {
checkbox.disabled = !isAdmin;
checkbox.classList.toggle('cursor-not-allowed', !isAdmin);
});
// Set error checkboxes based on current error flags
document.getElementById('errorWorksheets').checked = errorFlags.worksheets_error === 1;
document.getElementById('errorCutting').checked = errorFlags.cutting_error === 1;
document.getElementById('errorGlazing').checked = errorFlags.glazing_error === 1;
document.getElementById('errorReady').checked = errorFlags.ready_error === 1;
document.getElementById('errorIssued').checked = errorFlags.issued_error === 1;
// Disable inputs for guest users
const readOnly = !canEditProblems();
document.getElementById('problemsText').readOnly = readOnly;
document.getElementById('errorWorksheets').disabled = readOnly;
document.getElementById('errorCutting').disabled = readOnly;
document.getElementById('errorGlazing').disabled = readOnly;
document.getElementById('errorReady').disabled = readOnly;
document.getElementById('errorIssued').disabled = readOnly;
// Hide save button for guests
const saveBtn = document.querySelector('#problemsModal button[type="submit"]');
if (saveBtn) {
saveBtn.style.display = readOnly ? 'none' : 'inline-block';
}
document.getElementById('problemsModal').classList.add('active');
}
@@ -1360,32 +1372,25 @@ function openProblemsModal(recordId, problems, errorFlags = {}) {
function closeProblemsModal() {
document.getElementById('problemsModal').classList.remove('active');
document.getElementById('problemsRecordId').value = '';
const problemsTextarea = document.getElementById('problemsText');
if (problemsTextarea) {
problemsTextarea.value = '';
problemsTextarea.readOnly = false;
problemsTextarea.classList.remove('bg-gray-100', 'cursor-not-allowed');
}
document.getElementById('problemsText').value = '';
// Clear error checkboxes and restore interactivity
const checkboxes = [
document.getElementById('errorWorksheets'),
document.getElementById('errorCutting'),
document.getElementById('errorGlazing'),
document.getElementById('errorReady'),
document.getElementById('errorIssued')
];
checkboxes.forEach((checkbox) => {
if (!checkbox) return;
checkbox.checked = false;
checkbox.disabled = false;
checkbox.classList.remove('cursor-not-allowed');
});
// Clear error checkboxes
document.getElementById('errorWorksheets').checked = false;
document.getElementById('errorCutting').checked = false;
document.getElementById('errorGlazing').checked = false;
document.getElementById('errorReady').checked = false;
document.getElementById('errorIssued').checked = false;
}
async function saveProblems(event) {
event.preventDefault();
// Check permissions
if (!canEditProblems()) {
alert('Sul pole õigust probleeme muuta. Palun logi sisse.');
return;
}
const recordId = document.getElementById('problemsRecordId').value;
const problems = document.getElementById('problemsText').value;
@@ -1398,12 +1403,8 @@ async function saveProblems(event) {
issued: document.getElementById('errorIssued').checked
};
if (!ensureLoggedIn('salvestada probleemide infot')) {
return;
}
try {
const headers = { Authorization: `Bearer ${token}` };
const headers = token ? { Authorization: `Bearer ${token}` } : {};
await axios.patch(
`${API_BASE}/api/records/${recordId}/problems`,
{ problems, errorFlags },
@@ -1413,12 +1414,13 @@ async function saveProblems(event) {
await loadRecords(); // Reload data first
closeProblemsModal(); // Then close modal
} catch (error) {
if (handleUnauthorizedError(error, 'salvestada probleemide infot')) {
return;
}
console.error('Save problems error:', error);
alert('Viga probleemide salvestamisel');
if (error.response?.status === 401) {
alert('Sessioon on aegunud. Palun logi uuesti sisse.');
logout();
} else {
alert('Viga probleemide salvestamisel');
}
}
}
@@ -1460,8 +1462,9 @@ document.getElementById('settingsForm').addEventListener('submit', async functio
return;
}
if (!currentPassword) {
errorDiv.textContent = 'Praegune parool on kohustuslik';
// If changing password, current password is required
if (newPassword && !currentPassword) {
errorDiv.textContent = 'Praegune parool on kohustuslik parooli muutmiseks';
errorDiv.classList.remove('hidden');
return;
}
@@ -1537,9 +1540,9 @@ function renderPriceCell(recordId, price, pricePaid = 0, arveChecked = 0, arveMa
// Toggle price paid status
async function togglePricePaid(recordId) {
// Check if user is logged in as admin
if (!token || !currentUser || currentUser.role !== 'admin') {
alert('Ainult administraator saab muuta maksestaatust. Palun logige sisse.');
// Check permissions - only admin
if (!canEditRecords()) {
alert('Sul pole õigust maksestaatust muuta. Palun logi sisse administraatorina.');
return;
}
@@ -1573,17 +1576,18 @@ async function togglePricePaid(recordId) {
// Toggle material confirmed status
async function toggleMaterialConfirmed(recordId) {
// Check if user is logged in as admin
if (!token || !currentUser || currentUser.role !== 'admin') {
alert('Ainult administraator saab kinnitada materjali kättesaamist. Palun logige sisse.');
// Check permissions - only admin
if (!canEditRecords()) {
alert('Sul pole õigust kinnitust muuta. Palun logi sisse administraatorina.');
return;
}
try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await axios.patch(
`${API_BASE}/api/records/${recordId}/material-confirmed`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
{ headers }
);
if (response.data.success) {
@@ -1609,17 +1613,18 @@ async function toggleMaterialConfirmed(recordId) {
// Toggle material2_confirmed
async function toggleMaterial2Confirmed(recordId) {
// Check if user is logged in as admin
if (!token || !currentUser || currentUser.role !== 'admin') {
alert('Ainult administraator saab kinnitada materjali kättesaamist. Palun logige sisse.');
// Check permissions - only admin
if (!canEditRecords()) {
alert('Sul pole õigust kinnitust muuta. Palun logi sisse administraatorina.');
return;
}
try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await axios.patch(
`${API_BASE}/api/records/${recordId}/material2-confirmed`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
{ headers }
);
if (response.data.success) {
@@ -1645,6 +1650,12 @@ async function toggleMaterial2Confirmed(recordId) {
// Toggle worksheets with 3-step cycle
async function toggleWorksheetsStep(recordId) {
// Check permissions - admin and user can toggle
if (!canToggleDates()) {
alert('Sul pole õigust töölehe staatust muuta. Palun logi sisse.');
return;
}
try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await axios.patch(
@@ -1657,10 +1668,6 @@ async function toggleWorksheetsStep(recordId) {
await loadRecords(); // Refresh table
}
} catch (error) {
if (handleUnauthorizedError(error, 'muuta töölehe staatust')) {
return;
}
console.error('Toggle worksheets step error:', error);
alert('Viga Töölehti staatuse muutmisel');
}
@@ -2208,9 +2215,10 @@ document.getElementById('allowDeleteCheckbox').addEventListener('change', functi
// Toggle visibility of delete buttons
function toggleDeleteButtons() {
// Show delete buttons for all users
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = allowDelete ? 'inline-block' : 'none';
btn.style.display = 'inline-block';
});
}

0
public/static/style.css Executable file → Normal file
View File

46
public/test-click.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>Click Test</title>
<style>
body { font-family: Arial; padding: 50px; }
.box {
width: 200px;
height: 100px;
background: #4F46E5;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 20px 0;
}
#result {
margin-top: 20px;
padding: 20px;
background: #f0f0f0;
}
</style>
</head>
<body>
<h1>Click Test Page</h1>
<div class="box" onclick="handleClick(1)">Click Me (onclick)</div>
<div class="box" id="box2">Click Me (addEventListener)</div>
<div id="result">Waiting for click...</div>
<script>
// Test 1: inline onclick
function handleClick(num) {
document.getElementById('result').innerHTML = '✅ Test ' + num + ': onclick works!';
}
// Test 2: addEventListener
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('box2').addEventListener('click', () => {
document.getElementById('result').innerHTML = '✅ Test 2: addEventListener works!';
});
console.log('✅ DOMContentLoaded fired and event listener attached');
});
</script>
</body>
</html>

112
public/test-datepicker.html Normal file
View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Date Picker Test</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 50px;
max-width: 800px;
margin: 0 auto;
}
.test-section {
margin: 30px 0;
padding: 20px;
border: 2px solid #ccc;
border-radius: 8px;
}
.test-section h2 {
margin-top: 0;
color: #333;
}
.date-cell {
display: inline-block;
padding: 8px 12px;
margin: 10px;
border: 2px solid #4F46E5;
border-radius: 4px;
background: white;
cursor: pointer;
}
.date-cell:hover {
background: #f0f0f0;
}
.hidden-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
pointer-events: none;
}
.result {
margin-top: 20px;
padding: 15px;
background: #f0f0f0;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>📅 Date Picker Test Page</h1>
<!-- Test 1: Label approach (current v4.0.11) -->
<div class="test-section">
<h2>✅ Test 1: &lt;label for&gt; approach (v4.0.11)</h2>
<input type="date" id="date1" class="hidden-input" value="2025-01-15" onchange="updateResult(1, this.value)">
<label for="date1" class="date-cell">
Click me: 15.01.2025
</label>
<div class="result" id="result1">Selected: 2025-01-15</div>
</div>
<!-- Test 2: Direct visible input -->
<div class="test-section">
<h2>✅ Test 2: Direct visible input (baseline)</h2>
<input type="date" id="date2" value="2025-01-15" onchange="updateResult(2, this.value)" style="padding: 8px;">
<div class="result" id="result2">Selected: 2025-01-15</div>
</div>
<!-- Test 3: Label with onclick fallback -->
<div class="test-section">
<h2>✅ Test 3: &lt;label&gt; with onclick fallback</h2>
<input type="date" id="date3" class="hidden-input" value="2025-01-15" onchange="updateResult(3, this.value)">
<label for="date3" class="date-cell" onclick="document.getElementById('date3').showPicker()">
Click me: 15.01.2025
</label>
<div class="result" id="result3">Selected: 2025-01-15</div>
</div>
<!-- Test 4: Button triggers input.click() -->
<div class="test-section">
<h2>✅ Test 4: Button with .click()</h2>
<input type="date" id="date4" class="hidden-input" value="2025-01-15" onchange="updateResult(4, this.value)">
<button class="date-cell" onclick="document.getElementById('date4').click()">
Click me: 15.01.2025
</button>
<div class="result" id="result4">Selected: 2025-01-15</div>
</div>
<!-- Test 5: Inline style hidden input -->
<div class="test-section">
<h2>✅ Test 5: Inline style (exactly like app.js)</h2>
<input type="date" id="date5" style="position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;" value="2025-01-15" onchange="updateResult(5, this.value)">
<label for="date5" class="date-cell">
Click me: 15.01.2025
</label>
<div class="result" id="result5">Selected: 2025-01-15</div>
</div>
<script>
function updateResult(testNum, newDate) {
document.getElementById('result' + testNum).innerHTML =
'✅ Date picker worked! Selected: ' + newDate;
console.log('Test ' + testNum + ' changed to:', newDate);
}
console.log('✅ Test page loaded');
console.log('Browser:', navigator.userAgent);
</script>
</body>
</html>

13
public/test.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Test JS</title>
</head>
<body>
<h1 id="result">Waiting for JavaScript...</h1>
<script>
document.getElementById('result').textContent = 'JavaScript works!';
console.log('✅ JavaScript is executing correctly');
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.