diff --git a/Dockerfile b/Dockerfile index 0df481a..ce077c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,9 @@ RUN npm run build COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh +# Make scripts executable +RUN chmod +x backup-db.mjs send-due-date-reminders.mjs + # Expose the default Next.js port EXPOSE 3000 diff --git a/check-project-dates.mjs b/check-project-dates.mjs new file mode 100644 index 0000000..afe1278 --- /dev/null +++ b/check-project-dates.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import db from "./src/lib/db.js"; +import { parseISO, isAfter, startOfDay, addDays } from "date-fns"; + +const today = startOfDay(new Date()); +const threeDaysFromNow = addDays(today, 3); +const oneDayFromNow = addDays(today, 1); + +console.log(`Today: ${today.toISOString().split('T')[0]}`); +console.log(`3 days from now: ${threeDaysFromNow.toISOString().split('T')[0]}`); +console.log(`1 day from now: ${oneDayFromNow.toISOString().split('T')[0]}`); + +const projects = db.prepare(` + SELECT project_name, finish_date, project_status + FROM projects + WHERE finish_date IS NOT NULL + AND project_status != 'fulfilled' + AND project_status != 'cancelled' + ORDER BY finish_date ASC +`).all(); + +console.log(`\nFound ${projects.length} active projects with due dates:`); + +projects.forEach(project => { + try { + const finishDate = parseISO(project.finish_date); + const finishDateStart = startOfDay(finishDate); + + const isDueIn3Days = finishDateStart.getTime() === threeDaysFromNow.getTime(); + const isDueIn1Day = finishDateStart.getTime() === oneDayFromNow.getTime(); + const isOverdue = isAfter(today, finishDateStart); + + let status = ''; + if (isDueIn3Days) status = '⚠️ DUE IN 3 DAYS'; + else if (isDueIn1Day) status = '🚨 DUE IN 1 DAY'; + else if (isOverdue) status = '❌ OVERDUE'; + else status = '📅 Future'; + + console.log(`${status} - ${project.project_name}: ${project.finish_date.split('T')[0]} (${project.project_status})`); + } catch (error) { + console.log(`❌ Error parsing date for ${project.project_name}: ${project.finish_date}`); + } +}); \ No newline at end of file diff --git a/docker-entrypoint-dev.sh b/docker-entrypoint-dev.sh index a281ccb..a324776 100644 --- a/docker-entrypoint-dev.sh +++ b/docker-entrypoint-dev.sh @@ -25,6 +25,13 @@ echo "⏰ Setting up daily backup cron job..." echo "0 2 * * * cd /app && node backup-db.mjs >> /app/data/backup.log 2>&1" > /etc/cron.d/backup-cron chmod 0644 /etc/cron.d/backup-cron crontab /etc/cron.d/backup-cron + +# Set up daily due date reminders cron job (runs at 3 AM daily) +echo "⏰ Setting up daily due date reminders cron job..." +echo "0 3 * * * cd /app && node send-due-date-reminders.mjs >> /app/data/reminders.log 2>&1" > /etc/cron.d/reminders-cron +chmod 0644 /etc/cron.d/reminders-cron +crontab -l | cat - /etc/cron.d/reminders-cron > /tmp/crontab.tmp && crontab /tmp/crontab.tmp + service cron start # Start the development server diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index dc3bafb..a576fca 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -29,7 +29,12 @@ echo "⏰ Setting up daily backup cron job..." echo "0 2 * * * cd /app && /usr/local/bin/node backup-db.mjs >> /app/data/backup.log 2>&1" > /etc/cron.d/backup-cron chmod 0644 /etc/cron.d/backup-cron crontab /etc/cron.d/backup-cron -service cron start + +# Set up daily due date reminders cron job (runs at 3 AM daily) +echo "⏰ Setting up daily due date reminders cron job..." +echo "0 3 * * * cd /app && /usr/local/bin/node send-due-date-reminders.mjs >> /app/data/reminders.log 2>&1" > /etc/cron.d/reminders-cron +chmod 0644 /etc/cron.d/reminders-cron +crontab -l | cat - /etc/cron.d/reminders-cron > /tmp/crontab.tmp && crontab /tmp/crontab.tmp # Start the application echo "✅ Starting production server..." diff --git a/package.json b/package.json index bef862f..af1e113 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "lint": "next lint", "create-admin": "node scripts/create-admin.js", "export-projects": "node export-projects-to-excel.mjs", + "send-due-date-reminders": "node send-due-date-reminders.mjs", + "test-due-date-reminders": "node test-due-date-reminders.mjs", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", diff --git a/send-due-date-reminders.mjs b/send-due-date-reminders.mjs new file mode 100644 index 0000000..05b5b02 --- /dev/null +++ b/send-due-date-reminders.mjs @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Daily script to send due date reminders for projects + * Runs nightly to check for projects due in 3 days and 1 day + */ + +import db from "./src/lib/db.js"; +import { createNotification, NOTIFICATION_TYPES } from "./src/lib/notifications.js"; +import { addDays, isBefore, parseISO, startOfDay } from "date-fns"; + +async function sendDueDateReminders() { + try { + console.log("🔍 Checking for projects with upcoming due dates..."); + + const today = startOfDay(new Date()); + const threeDaysFromNow = addDays(today, 3); + const oneDayFromNow = addDays(today, 1); + + // Get projects that are not fulfilled and have finish dates + const projects = db.prepare(` + SELECT + p.project_id, + p.project_name, + p.finish_date, + p.address, + p.project_status, + c.customer + FROM projects p + LEFT JOIN contracts c ON p.contract_id = c.contract_id + WHERE + p.finish_date IS NOT NULL + AND p.project_status != 'fulfilled' + AND p.project_status != 'cancelled' + `).all(); + + console.log(`📋 Found ${projects.length} active projects with due dates`); + + let remindersSent = 0; + + for (const project of projects) { + try { + const finishDate = parseISO(project.finish_date); + const finishDateStart = startOfDay(finishDate); + + // Check if due in 3 days + if (finishDateStart.getTime() === threeDaysFromNow.getTime()) { + await sendReminder(project, 3); + remindersSent++; + } + // Check if due in 1 day + else if (finishDateStart.getTime() === oneDayFromNow.getTime()) { + await sendReminder(project, 1); + remindersSent++; + } + } catch (error) { + console.error(`❌ Error processing project ${project.project_id}:`, error); + } + } + + console.log(`✅ Sent ${remindersSent} due date reminders`); + + } catch (error) { + console.error("❌ Error in due date reminder script:", error); + } +} + +async function sendReminder(project, daysUntilDue) { + try { + // Get users who should receive notifications (admins and team leads) + const recipients = db.prepare(` + SELECT id, name, role + FROM users + WHERE role IN ('admin', 'team_lead') AND is_active = 1 + `).all(); + + if (recipients.length === 0) { + console.log("⚠️ No active admin or team lead users found to notify"); + return; + } + + const dayText = daysUntilDue === 1 ? "dzień" : "dni"; + const title = `Projekt kończy się za ${daysUntilDue} ${dayText}`; + const message = `Projekt "${project.project_name}" (${project.customer || 'Brak klienta'}) kończy się ${new Date(project.finish_date).toLocaleDateString('pl-PL')}. Adres: ${project.address || 'Brak adresu'}.`; + + for (const user of recipients) { + await createNotification({ + userId: user.id, + type: NOTIFICATION_TYPES.DUE_DATE_REMINDER, + title, + message, + resourceType: "project", + resourceId: project.project_id.toString(), + actionUrl: `/projects/${project.project_id}`, + priority: daysUntilDue === 1 ? "urgent" : "high" + }); + + console.log(`📢 Reminder sent to ${user.name} (${user.role}) for project: ${project.project_name}`); + } + + } catch (error) { + console.error(`❌ Failed to send reminder for project ${project.project_id}:`, error); + } +} + +// Run the script +sendDueDateReminders(); \ No newline at end of file diff --git a/test-due-date-reminders.mjs b/test-due-date-reminders.mjs new file mode 100644 index 0000000..1751350 --- /dev/null +++ b/test-due-date-reminders.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +/** + * Test script to simulate due date reminders + * Creates a test project due in 3 days and runs the reminder script + */ + +import db from "./src/lib/db.js"; +import { addDays, format } from "date-fns"; + +async function createTestProject() { + try { + console.log("🧪 Creating test project due in 3 days..."); + + // Create a test contract first + const contractResult = db.prepare(` + INSERT INTO contracts (contract_number, contract_name, customer, date_signed) + VALUES (?, ?, ?, ?) + `).run('TEST-001', 'Test Contract', 'Test Customer', new Date().toISOString()); + + const contractId = contractResult.lastInsertRowid; + + // Create a test project due in 3 days + const dueDate = addDays(new Date(), 3); + const projectResult = db.prepare(` + INSERT INTO projects ( + contract_id, project_name, project_number, address, + finish_date, project_status + ) VALUES (?, ?, ?, ?, ?, ?) + `).run( + contractId, + 'Test Project - Due Soon', + '1/TEST-001', + 'Test Address 123', + dueDate.toISOString(), + 'in_progress_design' + ); + + console.log(`✅ Created test project due on ${format(dueDate, 'yyyy-MM-dd')}`); + console.log("🔄 Running due date reminders script..."); + + // Run the reminders script + const { execSync } = await import('child_process'); + execSync('node send-due-date-reminders.mjs', { stdio: 'inherit' }); + + // Check if notifications were created + const notifications = db.prepare(` + SELECT * FROM notifications + WHERE type = 'due_date_reminder' + ORDER BY created_at DESC + LIMIT 5 + `).all(); + + console.log(`📢 Found ${notifications.length} due date reminder notifications:`); + notifications.forEach(notif => { + console.log(` - ${notif.title}: ${notif.message.substring(0, 100)}...`); + }); + + // Clean up test data + console.log("🧹 Cleaning up test data..."); + db.prepare('DELETE FROM projects WHERE project_name = ?').run('Test Project - Due Soon'); + db.prepare('DELETE FROM contracts WHERE contract_number = ?').run('TEST-001'); + db.prepare('DELETE FROM notifications WHERE type = ? AND title LIKE ?').run('due_date_reminder', 'Projekt kończy się za%'); + + console.log("✅ Test completed successfully!"); + + } catch (error) { + console.error("❌ Test failed:", error); + } +} + +createTestProject(); \ No newline at end of file