import { Hono } from 'hono' import { cors } from 'hono/cors' import { serveStatic } from 'hono/cloudflare-workers' import { authMiddleware, optionalAuthMiddleware } from './middleware/auth' import { generateToken, verifyPassword, hashPassword } from './utils/auth' import { ORIGINAL_HTML } from './original-html' type Bindings = { DB: D1Database; } type Variables = { userId?: number; username?: string; role?: string; } const app = new Hono<{ Bindings: Bindings; Variables: Variables }>() // Enable CORS app.use('/api/*', cors()) // Serve static files app.use('/static/*', serveStatic({ root: './public' })) // Serve favicon (empty response to avoid 404) app.get('/favicon.ico', (c) => { return new Response(null, { status: 204 }) }) // ==================== AUTH ROUTES ==================== // Login endpoint app.post('/api/auth/login', async (c) => { try { const { username, password } = await c.req.json() const user = await c.env.DB.prepare( 'SELECT id, username, password_hash, full_name, role FROM users WHERE username = ? AND deleted_at IS NULL' ).bind(username).first() if (!user || !await verifyPassword(password, user.password_hash as string)) { return c.json({ error: 'Invalid credentials' }, 401) } const token = generateToken(user.id as number, user.username as string) return c.json({ success: true, token, user: { username: user.username, fullName: user.full_name, role: user.role } }) } catch (error) { console.error('Login error:', error) return c.json({ error: 'Login failed' }, 500) } }) // Update user profile (password change) app.patch('/api/users/profile', authMiddleware, async (c) => { try { const body = await c.req.json() const fullName = body.full_name || body.fullName const currentPassword = body.current_password || body.currentPassword const newPassword = body.new_password || body.newPassword const userId = c.get('userId') console.log('[PROFILE UPDATE]', { userId, fullName, hasCurrentPwd: !!currentPassword, hasNewPwd: !!newPassword }) // Get user from database const user = await c.env.DB.prepare( 'SELECT password_hash, full_name FROM users WHERE id = ?' ).bind(userId).first() if (!user) { return c.json({ error: 'Kasutajat ei leitud' }, 404) } // If changing password if (newPassword) { // Verify current password is provided if (!currentPassword) { return c.json({ error: 'Praegune parool on kohustuslik parooli muutmiseks' }, 400) } // Verify current password if (!await verifyPassword(currentPassword, user.password_hash as string)) { return c.json({ error: 'Vale praegune parool' }, 400) } // Update password and full name const newHash = await hashPassword(newPassword) await c.env.DB.prepare( 'UPDATE users SET password_hash = ?, full_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ).bind(newHash, fullName, userId).run() } else { // Only update full name (no password change) await c.env.DB.prepare( 'UPDATE users SET full_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ).bind(fullName, userId).run() } return c.json({ success: true, message: 'Profiil uuendatud', user: { full_name: fullName } }) } catch (error) { console.error('Profile update error:', error) return c.json({ error: 'Profiili uuendamine ebaõnnestus' }, 500) } }) // ==================== DATA ROUTES ==================== // Get years for dropdown (with optional auth) app.get('/api/years', optionalAuthMiddleware, async (c) => { try { const result = await c.env.DB.prepare( 'SELECT MIN(year) as min_year FROM production_records WHERE deleted_at IS NULL' ).first() const minYear = result?.min_year || new Date().getFullYear() const maxYear = new Date().getFullYear() + 1 // Create array of years from minYear to maxYear const years = [] for (let year = minYear; year <= maxYear; year++) { years.push(year) } return c.json({ years }) } catch (error) { console.error('Error fetching years:', error) return c.json({ error: 'Failed to fetch years' }, 500) } }) // Get records (with optional auth for token refresh) app.get('/api/records', optionalAuthMiddleware, async (c) => { try { const month = c.req.query('month') const year = c.req.query('year') if (!month || !year) { return c.json({ error: 'Month and year required' }, 400) } const records = await c.env.DB.prepare(` SELECT pr.*, sc.material_date, sc.material2_date, sc.package_date, sc.worksheets_date, sc.cutting_date, sc.glazing_date, sc.ready_date, sc.issued_date, sc.worksheets_error, sc.cutting_error, sc.glazing_error, sc.ready_error, sc.issued_error, sc.material_confirmed, sc.material2_confirmed, sc.worksheets_confirmed FROM production_records pr LEFT JOIN status_checkboxes sc ON pr.id = sc.record_id WHERE pr.month = ? AND pr.year = ? AND pr.deleted_at IS NULL ORDER BY pr.created_at DESC `).bind(month, year).all() return c.json(records.results || []) } catch (error) { console.error('Error fetching records:', error) return c.json({ error: 'Failed to fetch records' }, 500) } }) // Create new record app.post('/api/records', optionalAuthMiddleware, async (c) => { try { const data = await c.req.json() const userId = c.get('userId') // Validate and convert numeric fields const quantity = data.quantity ? parseInt(data.quantity, 10) : 0 const price = data.price ? parseFloat(data.price) : 0 const result = await c.env.DB.prepare(` INSERT INTO production_records ( month, year, client_name, type, offer_number, work_number, quantity, color, notes, problems, installer, price, created_by, updated_by ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).bind( data.month, data.year, data.client_name, data.type || null, data.offer_number, data.work_number, quantity, data.color || null, data.notes || null, data.problems || null, data.installer || null, price, userId, userId ).run() // Create status checkboxes entry await c.env.DB.prepare(` INSERT INTO status_checkboxes (record_id) VALUES (?) `).bind(result.meta.last_row_id).run() return c.json({ success: true, id: result.meta.last_row_id }) } catch (error) { console.error('Error creating record:', error) return c.json({ error: 'Failed to create record' }, 500) } }) // Update record app.put('/api/records/:id', optionalAuthMiddleware, async (c) => { try { const id = c.req.param('id') const data = await c.req.json() const userId = c.get('userId') // Validate and convert numeric fields const quantity = data.quantity ? parseInt(data.quantity, 10) : 0 const price = data.price ? parseFloat(data.price) : 0 await c.env.DB.prepare(` UPDATE production_records SET client_name = ?, type = ?, offer_number = ?, work_number = ?, quantity = ?, color = ?, notes = ?, problems = ?, installer = ?, price = ?, updated_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL `).bind( data.client_name, data.type || null, data.offer_number, data.work_number, quantity, data.color || null, data.notes || null, data.problems || null, data.installer || null, price, userId, id ).run() // Update status_checkboxes dates if provided if (data.material_date !== undefined || data.material2_date !== undefined || data.package_date !== undefined) { // Convert empty strings and "null" strings to actual NULL const materialDate = (data.material_date && data.material_date !== 'null') ? data.material_date : null const material2Date = (data.material2_date && data.material2_date !== 'null') ? data.material2_date : null const packageDate = (data.package_date && data.package_date !== 'null') ? data.package_date : null await c.env.DB.prepare(` UPDATE status_checkboxes SET material_date = ?, material2_date = ?, package_date = ?, updated_at = CURRENT_TIMESTAMP WHERE record_id = ? `).bind(materialDate, material2Date, packageDate, id).run() } return c.json({ success: true }) } catch (error) { console.error('Error updating record:', error) return c.json({ error: 'Failed to update record' }, 500) } }) // Get single record app.get('/api/records/:id', optionalAuthMiddleware, async (c) => { try { const id = c.req.param('id') const record = await c.env.DB.prepare(` SELECT * FROM production_records WHERE id = ? AND deleted_at IS NULL `).bind(id).first() if (!record) { return c.json({ error: 'Record not found' }, 404) } return c.json(record) } catch (error) { console.error('Error fetching record:', error) return c.json({ error: 'Failed to fetch record' }, 500) } }) // Delete record (soft delete) app.delete('/api/records/:id', optionalAuthMiddleware, async (c) => { try { const id = c.req.param('id') const userId = c.get('userId') console.log('[DELETE] Deleting record:', id, 'by user:', userId) await c.env.DB.prepare(` UPDATE production_records SET deleted_at = CURRENT_TIMESTAMP, deleted = 1 WHERE id = ? `).bind(id).run() console.log('[DELETE] Record deleted successfully:', id) return c.json({ success: true }) } catch (error) { console.error('Error deleting record:', error) return c.json({ error: 'Failed to delete record' }, 500) } }) // ==================== STATUS CHECKBOX ROUTES ==================== // Toggle date - simplified endpoint for frontend compatibility app.patch('/api/records/:id/status', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') const { field, date } = await c.req.json() const userId = c.get('userId') console.log(`[TOGGLE] recordId=${recordId}, field=${field}, date=${JSON.stringify(date)}`) // Field name with _date suffix for database column const dbField = `${field}_date` // Get old date for audit const oldRecord = await c.env.DB.prepare( `SELECT ${dbField} FROM status_checkboxes WHERE record_id = ?` ).bind(recordId).first() // Check if ready or issued fields are blocked by error flags if (field === 'ready' || field === 'issued') { const statusCheckbox = await c.env.DB.prepare( 'SELECT worksheets_error, cutting_error, glazing_error, ready_error, issued_error FROM status_checkboxes WHERE record_id = ?' ).bind(recordId).first() const hasErrorFlags = statusCheckbox && ( statusCheckbox.worksheets_error || statusCheckbox.cutting_error || statusCheckbox.glazing_error || statusCheckbox.ready_error || statusCheckbox.issued_error ) if (hasErrorFlags) { return c.json({ error: 'blocked', message: 'Vigade märked on seatud (punased kolmnurgad)' }, 403) } } // Toggle logic: // 1. If date is null/empty → check if cell is empty → add today's date OR clear // 2. If date matches current date → toggle off (clear) // 3. Otherwise → use provided date let newDate: string | null if (!date || date === 'null') { // null/empty clicked if (oldRecord?.[dbField]) { // Cell has date → clear it newDate = null } else { // Cell is empty → add today's date newDate = new Date().toISOString().split('T')[0] } } else if (date === oldRecord?.[dbField]) { // Same date as current → toggle off (clear) newDate = null } else { // Different date provided → use it newDate = date } await c.env.DB.prepare( `UPDATE status_checkboxes SET ${dbField} = ? WHERE record_id = ?` ).bind(newDate, recordId).run() // Log to audit await c.env.DB.prepare(` INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action) VALUES (?, ?, ?, ?, ?, 'toggle_status') `).bind(userId || null, recordId, field, oldRecord?.[dbField] || null, newDate).run() return c.json({ success: true }) } catch (error) { console.error('Error toggling status:', error) return c.json({ error: 'Failed to toggle status' }, 500) } }) // Update status checkbox date app.patch('/api/status/:recordId/:field', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') const field = c.req.param('field') const { date } = await c.req.json() const userId = c.get('userId') // Get old date for audit const oldRecord = await c.env.DB.prepare( `SELECT ${field}_date FROM status_checkboxes WHERE record_id = ?` ).bind(recordId).first() // Check if ready or issued fields are blocked by error flags if (field === 'ready' || field === 'issued') { const statusCheckbox = await c.env.DB.prepare( 'SELECT worksheets_error, cutting_error, glazing_error, ready_error, issued_error FROM status_checkboxes WHERE record_id = ?' ).bind(recordId).first() const hasErrorFlags = statusCheckbox && ( statusCheckbox.worksheets_error || statusCheckbox.cutting_error || statusCheckbox.glazing_error || statusCheckbox.ready_error || statusCheckbox.issued_error ) if (hasErrorFlags) { return c.json({ error: 'Väli blokeeritud', blocked: true, reason: 'Vigade märked on seatud (punased kolmnurgad)' }, 400) } } // Update the date await c.env.DB.prepare( `UPDATE status_checkboxes SET ${field}_date = ? WHERE record_id = ?` ).bind(date, recordId).run() // Log to audit await c.env.DB.prepare(` INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action) VALUES (?, ?, ?, ?, ?, 'update_status') `).bind(userId || null, recordId, field, oldRecord?.[`${field}_date`] || null, date).run() return c.json({ success: true }) } catch (error) { console.error('Error updating status:', error) return c.json({ error: 'Failed to update status' }, 500) } }) // Update error flag app.patch('/api/status/:recordId/:field/error', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') const field = c.req.param('field') const { value } = await c.req.json() const userId = c.get('userId') await c.env.DB.prepare( `UPDATE status_checkboxes SET ${field}_error = ? WHERE record_id = ?` ).bind(value ? 1 : 0, recordId).run() return c.json({ success: true }) } catch (error) { console.error('Error updating error flag:', error) return c.json({ error: 'Failed to update error flag' }, 500) } }) // Update confirmation flag app.patch('/api/status/:recordId/:field/confirm', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') const field = c.req.param('field') const { value } = await c.req.json() const userId = c.get('userId') await c.env.DB.prepare( `UPDATE status_checkboxes SET ${field}_confirmed = ? WHERE record_id = ?` ).bind(value ? 1 : 0, recordId).run() return c.json({ success: true }) } catch (error) { console.error('Error updating confirmation flag:', error) return c.json({ error: 'Failed to update confirmation flag' }, 500) } }) // ==================== ADDITIONAL RECORD ROUTES ==================== // Worksheets cycle (3-step: empty -> confirmed -> with date -> empty) app.patch('/api/records/:id/worksheets-cycle', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') const userId = c.get('userId') // Get current worksheets state const statusRecord = await c.env.DB.prepare( 'SELECT worksheets_date, worksheets_confirmed FROM status_checkboxes WHERE record_id = ?' ).bind(recordId).first() let newDate = null let newConfirmed = 0 // 3-step cycle logic: // Step 1: empty (null date, confirmed=0) -> gray with date (date, confirmed=0) // Step 2: gray with date (date, confirmed=0) -> green with date (date, confirmed=1) // Step 3: green with date (date, confirmed=1) -> empty (null date, confirmed=0) if (!statusRecord?.worksheets_date) { // Step 1: empty -> gray with date newConfirmed = 0 newDate = new Date().toISOString().split('T')[0] } else if (statusRecord.worksheets_confirmed === 0) { // Step 2: gray with date -> green with date newConfirmed = 1 newDate = statusRecord.worksheets_date // Keep existing date } else { // Step 3: green with date -> empty newConfirmed = 0 newDate = null } await c.env.DB.prepare( 'UPDATE status_checkboxes SET worksheets_date = ?, worksheets_confirmed = ? WHERE record_id = ?' ).bind(newDate, newConfirmed, recordId).run() // Log to audit await c.env.DB.prepare(` INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action) VALUES (?, ?, ?, ?, ?, 'worksheets_cycle') `).bind(userId || null, recordId, 'worksheets', statusRecord?.worksheets_date || '', newDate || '').run() return c.json({ success: true, date: newDate, confirmed: newConfirmed }) } catch (error) { console.error('Error cycling worksheets:', error) return c.json({ error: 'Failed to cycle worksheets' }, 500) } }) // Update notes app.patch('/api/records/:id/notes', authMiddleware, async (c) => { try { const recordId = c.req.param('id') const { notes } = await c.req.json() const userId = c.get('userId') const userRole = c.get('role') // Only admin can edit notes if (userRole !== 'admin') { return c.json({ error: 'Permission denied. Only admin can edit notes.' }, 403) } await c.env.DB.prepare( 'UPDATE production_records SET notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ).bind(notes, recordId).run() // Log to audit await c.env.DB.prepare(` INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action) VALUES (?, ?, ?, ?, ?, 'update_notes') `).bind(userId || null, recordId, 'notes', '', notes).run() return c.json({ success: true }) } catch (error) { console.error('Error updating notes:', error) return c.json({ error: 'Failed to update notes' }, 500) } }) // Update problems and error flags app.patch('/api/records/:id/problems', authMiddleware, async (c) => { try { const recordId = c.req.param('id') const { problems, errorFlags } = await c.req.json() const userId = c.get('userId') const userRole = c.get('role') // User and admin can edit problems if (userRole !== 'admin' && userRole !== 'user') { return c.json({ error: 'Permission denied. Only admin and user can edit problems.' }, 403) } // Update problems text await c.env.DB.prepare( 'UPDATE production_records SET problems = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ).bind(problems, recordId).run() // Update error flags await c.env.DB.prepare(` UPDATE status_checkboxes SET worksheets_error = ?, cutting_error = ?, glazing_error = ?, ready_error = ?, issued_error = ? WHERE record_id = ? `).bind( errorFlags.worksheets ? 1 : 0, errorFlags.cutting ? 1 : 0, errorFlags.glazing ? 1 : 0, errorFlags.ready ? 1 : 0, errorFlags.issued ? 1 : 0, recordId ).run() // Log to audit await c.env.DB.prepare(` INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action) VALUES (?, ?, ?, ?, ?, 'update_problems') `).bind(userId || null, recordId, 'problems', '', problems).run() return c.json({ success: true }) } catch (error) { console.error('Error updating problems:', error) return c.json({ error: 'Failed to update problems' }, 500) } }) // Toggle material confirmed app.patch('/api/records/:id/material-confirmed', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') console.log('[MAT1] Toggle request for record:', recordId) // Get current value const current = await c.env.DB.prepare( 'SELECT material_confirmed FROM status_checkboxes WHERE record_id = ?' ).bind(recordId).first() console.log('[MAT1] Current value:', current?.material_confirmed) // Toggle value const newValue = current?.material_confirmed === 1 ? 0 : 1 console.log('[MAT1] New value:', newValue) await c.env.DB.prepare( 'UPDATE status_checkboxes SET material_confirmed = ? WHERE record_id = ?' ).bind(newValue, recordId).run() console.log('[MAT1] Update completed successfully') return c.json({ success: true, newValue }) } catch (error) { console.error('[MAT1] Error updating material confirmed:', error) return c.json({ error: 'Failed to update material confirmed' }, 500) } }) // Toggle material2 confirmed app.patch('/api/records/:id/material2-confirmed', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') console.log('[MAT2] Toggle request for record:', recordId) // Get current value const current = await c.env.DB.prepare( 'SELECT material2_confirmed FROM status_checkboxes WHERE record_id = ?' ).bind(recordId).first() console.log('[MAT2] Current value:', current?.material2_confirmed) // Toggle value const newValue = current?.material2_confirmed === 1 ? 0 : 1 console.log('[MAT2] New value:', newValue) await c.env.DB.prepare( 'UPDATE status_checkboxes SET material2_confirmed = ? WHERE record_id = ?' ).bind(newValue, recordId).run() console.log('[MAT2] Update completed successfully') return c.json({ success: true, newValue }) } catch (error) { console.error('[MAT2] Error updating material2 confirmed:', error) return c.json({ error: 'Failed to update material2 confirmed' }, 500) } }) // Update price paid status (for invoice tracking) app.patch('/api/records/:id/price-paid', optionalAuthMiddleware, async (c) => { try { const recordId = c.req.param('id') const { paid } = await c.req.json() // You might want to add a 'paid' field to production_records table // For now, we'll just return success // TODO: Add paid field to schema if needed return c.json({ success: true }) } catch (error) { console.error('Error updating price paid:', error) return c.json({ error: 'Failed to update price paid' }, 500) } }) // ==================== DEFAULT ROUTE ==================== // ==================== MAIN PAGE - ORIGINAL HTML FROM ARCHIVE ==================== app.get('/', (c) => { return c.html(ORIGINAL_HTML) }) // Test page for debugging clicks app.get('/test-click', (c) => { return c.html(`