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:
2
public/AKNAPROFF Tootmine_files/axios.min.js
vendored
2
public/AKNAPROFF Tootmine_files/axios.min.js
vendored
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
0
public/favicon.ico
Normal file
35
public/index.html → public/original.html
Executable file → Normal file
35
public/index.html → public/original.html
Executable file → Normal 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>
|
||||
0
public/AKNAPROFF Tootmine_files/all.min.css → public/static/all.min.css
vendored
Executable file → Normal file
0
public/AKNAPROFF Tootmine_files/all.min.css → public/static/all.min.css
vendored
Executable file → Normal file
624
public/AKNAPROFF Tootmine_files/app.js → public/static/app.js
Executable file → Normal file
624
public/AKNAPROFF Tootmine_files/app.js → public/static/app.js
Executable file → Normal 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, '"').replace(/'/g, ''').replace(/\n/g, ' ') : '';
|
||||
|
||||
// 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, '"').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, '"').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, '"').replace(/'/g, ''').replace(/\n/g, ' ') : '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, '"').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, '"');
|
||||
|
||||
// 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, '"').replace(/'/g, ''').replace(/\n/g, ' ') : '';
|
||||
|
||||
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, '"').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, '"').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, '"').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
0
public/static/style.css
Executable file → Normal file
46
public/test-click.html
Normal file
46
public/test-click.html
Normal 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
112
public/test-datepicker.html
Normal 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: <label for> 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: <label> 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
13
public/test.html
Normal 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.
Reference in New Issue
Block a user