From 95ef1398436f8e9cc5805f304e3e376613f0e9c0 Mon Sep 17 00:00:00 2001 From: RKWojs Date: Thu, 11 Sep 2025 16:19:46 +0200 Subject: [PATCH] feat: Add support for project cancellation status across the application --- src/app/api/projects/[id]/route.js | 111 +++++++++++------- src/app/projects/map/page.js | 6 + src/app/projects/page.js | 1 + src/components/ProjectStatusDropdown.js | 16 ++- src/components/ProjectStatusDropdownDebug.js | 4 + src/components/ProjectStatusDropdownSimple.js | 4 + src/components/ui/ProjectCalendarWidget.js | 2 + src/components/ui/ProjectMap.js | 1 + src/lib/i18n.js | 2 + src/lib/init-db.js | 4 +- src/lib/queries/notes.js | 8 +- src/lib/queries/projects.js | 5 +- src/lib/utils.js | 2 + 13 files changed, 116 insertions(+), 50 deletions(-) diff --git a/src/app/api/projects/[id]/route.js b/src/app/api/projects/[id]/route.js index c7f436e..ba23e47 100644 --- a/src/app/api/projects/[id]/route.js +++ b/src/app/api/projects/[id]/route.js @@ -8,6 +8,7 @@ import { deleteProject, } from "@/lib/queries/projects"; import { logFieldChange } from "@/lib/queries/fieldHistory"; +import { addNoteToProject } from "@/lib/queries/notes"; import initializeDatabase from "@/lib/init-db"; import { NextResponse } from "next/server"; import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; @@ -42,57 +43,85 @@ async function getProjectHandler(req, { params }) { } async function updateProjectHandler(req, { params }) { - const { id } = await params; - const data = await req.json(); + try { + const { id } = await params; + const data = await req.json(); - // Get user ID from authenticated request - const userId = req.user?.id; + // Get user ID from authenticated request + const userId = req.user?.id; - // Get original project data for audit log and field tracking - const originalProject = getProjectById(parseInt(id)); + // Get original project data for audit log and field tracking + const originalProject = getProjectById(parseInt(id)); - if (!originalProject) { - return NextResponse.json({ error: "Project not found" }, { status: 404 }); - } + if (!originalProject) { + return NextResponse.json({ error: "Project not found" }, { status: 404 }); + } - // Track field changes for specific fields we want to monitor - const fieldsToTrack = ['finish_date', 'project_status', 'assigned_to', 'contract_id']; - - for (const fieldName of fieldsToTrack) { - if (data.hasOwnProperty(fieldName)) { - const oldValue = originalProject[fieldName]; - const newValue = data[fieldName]; - - if (oldValue !== newValue) { - try { - logFieldChange('projects', parseInt(id), fieldName, oldValue, newValue, userId); - } catch (error) { - console.error(`Failed to log field change for ${fieldName}:`, error); + // Track field changes for specific fields we want to monitor + const fieldsToTrack = ['finish_date', 'project_status', 'assigned_to', 'contract_id']; + + for (const fieldName of fieldsToTrack) { + if (data.hasOwnProperty(fieldName)) { + const oldValue = originalProject[fieldName]; + const newValue = data[fieldName]; + + if (oldValue !== newValue) { + try { + logFieldChange('projects', parseInt(id), fieldName, oldValue, newValue, userId); + } catch (error) { + console.error(`Failed to log field change for ${fieldName}:`, error); + } } } } - } - updateProject(parseInt(id), data, userId); - - // Get updated project - const updatedProject = getProjectById(parseInt(id)); - - // Log project update - await logApiActionSafe( - req, - AUDIT_ACTIONS.PROJECT_UPDATE, - RESOURCE_TYPES.PROJECT, - id, - req.auth, // Use req.auth instead of req.session - { - originalData: originalProject, - updatedData: data, - changedFields: Object.keys(data), + // Special handling for project cancellation + if (data.project_status === 'cancelled' && originalProject.project_status !== 'cancelled') { + const now = new Date(); + const cancellationDate = now.toLocaleDateString('pl-PL', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + + const cancellationNote = `Projekt został wycofany w dniu ${cancellationDate}`; + + try { + addNoteToProject(parseInt(id), cancellationNote, userId, true); // true for is_system + } catch (error) { + console.error('Failed to log project cancellation:', error); + } } - ); - return NextResponse.json(updatedProject); + updateProject(parseInt(id), data, userId); + + // Get updated project + const updatedProject = getProjectById(parseInt(id)); + + // Log project update + await logApiActionSafe( + req, + AUDIT_ACTIONS.PROJECT_UPDATE, + RESOURCE_TYPES.PROJECT, + id, + req.auth, // Use req.auth instead of req.session + { + originalData: originalProject, + updatedData: data, + changedFields: Object.keys(data), + } + ); + + return NextResponse.json(updatedProject); + } catch (error) { + console.error("Error in updateProjectHandler:", error); + return NextResponse.json( + { error: "Internal server error", details: error.message }, + { status: 500 } + ); + } } async function deleteProjectHandler(req, { params }) { diff --git a/src/app/projects/map/page.js b/src/app/projects/map/page.js index 23026dd..f308312 100644 --- a/src/app/projects/map/page.js +++ b/src/app/projects/map/page.js @@ -29,6 +29,7 @@ function ProjectsMapPageContent() { in_progress_design: true, in_progress_construction: true, fulfilled: true, + cancelled: true, }); const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap"); const [activeOverlays, setActiveOverlays] = useState([]); @@ -57,6 +58,11 @@ function ProjectsMapPageContent() { label: "Completed", shortLabel: "Zakończony", }, + cancelled: { + color: "#EF4444", + label: "Cancelled", + shortLabel: "Wycofany", + }, }; // Toggle all status filters diff --git a/src/app/projects/page.js b/src/app/projects/page.js index a339f11..0795b91 100644 --- a/src/app/projects/page.js +++ b/src/app/projects/page.js @@ -119,6 +119,7 @@ export default function ProjectListPage() { case "in_progress_design": return t('projectStatus.in_progress_design'); case "in_progress_construction": return t('projectStatus.in_progress_construction'); case "fulfilled": return t('projectStatus.fulfilled'); + case "cancelled": return t('projectStatus.cancelled'); default: return "-"; } }; diff --git a/src/components/ProjectStatusDropdown.js b/src/components/ProjectStatusDropdown.js index 0203efc..01e5d41 100644 --- a/src/components/ProjectStatusDropdown.js +++ b/src/components/ProjectStatusDropdown.js @@ -38,6 +38,10 @@ export default function ProjectStatusDropdown({ label: t("projectStatus.fulfilled"), variant: "success", }, + cancelled: { + label: t("projectStatus.cancelled"), + variant: "danger", + }, }; const handleChange = async (newStatus) => { if (newStatus === status) { @@ -50,11 +54,19 @@ export default function ProjectStatusDropdown({ setIsOpen(false); try { - await fetch(`/api/projects/${project.project_id}`, { + const updateData = { ...project, project_status: newStatus }; + + const response = await fetch(`/api/projects/${project.project_id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ...project, project_status: newStatus }), + body: JSON.stringify(updateData), }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Update failed:', errorData); + } + window.location.reload(); } catch (error) { console.error("Failed to update status:", error); diff --git a/src/components/ProjectStatusDropdownDebug.js b/src/components/ProjectStatusDropdownDebug.js index 9f3eec8..0608e7f 100644 --- a/src/components/ProjectStatusDropdownDebug.js +++ b/src/components/ProjectStatusDropdownDebug.js @@ -29,6 +29,10 @@ export default function ProjectStatusDropdownDebug({ label: "Completed", variant: "success", }, + cancelled: { + label: "Cancelled", + variant: "danger", + }, }; const handleChange = async (newStatus) => { diff --git a/src/components/ProjectStatusDropdownSimple.js b/src/components/ProjectStatusDropdownSimple.js index 2cfaa48..1cec5b5 100644 --- a/src/components/ProjectStatusDropdownSimple.js +++ b/src/components/ProjectStatusDropdownSimple.js @@ -29,6 +29,10 @@ export default function ProjectStatusDropdownSimple({ label: "Completed", variant: "success", }, + cancelled: { + label: "Cancelled", + variant: "danger", + }, }; const handleChange = async (newStatus) => { diff --git a/src/components/ui/ProjectCalendarWidget.js b/src/components/ui/ProjectCalendarWidget.js index 7fd5e60..3830739 100644 --- a/src/components/ui/ProjectCalendarWidget.js +++ b/src/components/ui/ProjectCalendarWidget.js @@ -31,6 +31,7 @@ const statusColors = { pending: "bg-yellow-100 text-yellow-800", in_progress: "bg-orange-100 text-orange-800", fulfilled: "bg-gray-100 text-gray-800", + cancelled: "bg-red-100 text-red-800", }; const statusTranslations = { @@ -39,6 +40,7 @@ const statusTranslations = { pending: "Oczekujący", in_progress: "W trakcie", fulfilled: "Zakończony", + cancelled: "Wycofany", }; export default function ProjectCalendarWidget({ diff --git a/src/components/ui/ProjectMap.js b/src/components/ui/ProjectMap.js index 18f8491..0283969 100644 --- a/src/components/ui/ProjectMap.js +++ b/src/components/ui/ProjectMap.js @@ -32,6 +32,7 @@ export default function ProjectMap({ label: "In Progress (Construction)", }, fulfilled: { color: "#10B981", label: "Completed" }, + cancelled: { color: "#EF4444", label: "Cancelled" }, }; useEffect(() => { diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 475b351..f0940cc 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -105,6 +105,7 @@ const translations = { in_progress_design: "W realizacji (projektowanie)", in_progress_construction: "W realizacji (realizacja)", fulfilled: "Zakończony", + cancelled: "Wycofany", unknown: "Nieznany" }, @@ -541,6 +542,7 @@ const translations = { in_progress_design: "In Progress (Design)", in_progress_construction: "In Progress (Construction)", fulfilled: "Completed", + cancelled: "Cancelled", unknown: "Unknown" }, diff --git a/src/lib/init-db.js b/src/lib/init-db.js index 7443310..9521b23 100644 --- a/src/lib/init-db.js +++ b/src/lib/init-db.js @@ -31,7 +31,7 @@ export default function initializeDatabase() { contact TEXT, notes TEXT, project_type TEXT CHECK(project_type IN ('design', 'construction', 'design+construction')) DEFAULT 'design', - project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled')) DEFAULT 'registered', + project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled', 'cancelled')) DEFAULT 'registered', FOREIGN KEY (contract_id) REFERENCES contracts(contract_id) ); @@ -113,7 +113,7 @@ export default function initializeDatabase() { // Migration: Add project_status column to projects table try { db.exec(` - ALTER TABLE projects ADD COLUMN project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled')) DEFAULT 'registered'; + ALTER TABLE projects ADD COLUMN project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled', 'cancelled')) DEFAULT 'registered'; `); } catch (e) { // Column already exists, ignore error diff --git a/src/lib/queries/notes.js b/src/lib/queries/notes.js index cbd5216..903efa8 100644 --- a/src/lib/queries/notes.js +++ b/src/lib/queries/notes.js @@ -16,13 +16,13 @@ export function getNotesByProjectId(project_id) { .all(project_id); } -export function addNoteToProject(project_id, note, created_by = null) { +export function addNoteToProject(project_id, note, created_by = null, is_system = false) { db.prepare( ` - INSERT INTO notes (project_id, note, created_by, note_date) - VALUES (?, ?, ?, CURRENT_TIMESTAMP) + INSERT INTO notes (project_id, note, created_by, is_system, note_date) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ` - ).run(project_id, note, created_by); + ).run(project_id, note, created_by, is_system ? 1 : 0); } export function getNotesByTaskId(task_id) { diff --git a/src/lib/queries/projects.js b/src/lib/queries/projects.js index 3526382..6bd59e9 100644 --- a/src/lib/queries/projects.js +++ b/src/lib/queries/projects.js @@ -110,7 +110,7 @@ export function updateProject(id, data, userId = null) { coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP WHERE project_id = ? `); - stmt.run( + const result = stmt.run( data.contract_id, data.project_name, data.project_number, @@ -130,6 +130,9 @@ export function updateProject(id, data, userId = null) { data.assigned_to || null, id ); + + console.log('Update result:', result); + return result; } export function deleteProject(id) { diff --git a/src/lib/utils.js b/src/lib/utils.js index 61495f5..f7ff393 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -15,6 +15,8 @@ export const formatProjectStatus = (status) => { return "W realizacji (realizacja)"; case "fulfilled": return "Zakończony"; + case "cancelled": + return "Wycofany"; default: return "-"; }