Files
Aknaproff/src/index.tsx
Deploy Bot bf43461559 v4.1.22: Исправлено удаление записей (HOTFIX)
- FIX: DELETE /api/records/:id возвращал 500 (no such column: deleted_by)
- Было: SET deleted_at = CURRENT_TIMESTAMP, deleted_by = ?
- Стало: SET deleted_at = CURRENT_TIMESTAMP, deleted = 1
- Добавлены логи: console.log('[DELETE]...')
- Docker: убран docker-compose.yml из v4.1.21
- Добавлен fix-docker.sh и docker-compose-simple.yml
- Cache version: app.js?v=4.1.22
2026-01-14 23:34:00 +02:00

748 lines
24 KiB
TypeScript

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(`<!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>`)
})
export default app