feat: Add support for project cancellation status across the application

This commit is contained in:
2025-09-11 16:19:46 +02:00
parent 2735d46552
commit 95ef139843
13 changed files with 116 additions and 50 deletions

View File

@@ -8,6 +8,7 @@ import {
deleteProject, deleteProject,
} from "@/lib/queries/projects"; } from "@/lib/queries/projects";
import { logFieldChange } from "@/lib/queries/fieldHistory"; import { logFieldChange } from "@/lib/queries/fieldHistory";
import { addNoteToProject } from "@/lib/queries/notes";
import initializeDatabase from "@/lib/init-db"; import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
@@ -42,57 +43,85 @@ async function getProjectHandler(req, { params }) {
} }
async function updateProjectHandler(req, { params }) { async function updateProjectHandler(req, { params }) {
const { id } = await params; try {
const data = await req.json(); const { id } = await params;
const data = await req.json();
// Get user ID from authenticated request // Get user ID from authenticated request
const userId = req.user?.id; const userId = req.user?.id;
// Get original project data for audit log and field tracking // Get original project data for audit log and field tracking
const originalProject = getProjectById(parseInt(id)); const originalProject = getProjectById(parseInt(id));
if (!originalProject) { if (!originalProject) {
return NextResponse.json({ error: "Project not found" }, { status: 404 }); return NextResponse.json({ error: "Project not found" }, { status: 404 });
} }
// Track field changes for specific fields we want to monitor // Track field changes for specific fields we want to monitor
const fieldsToTrack = ['finish_date', 'project_status', 'assigned_to', 'contract_id']; const fieldsToTrack = ['finish_date', 'project_status', 'assigned_to', 'contract_id'];
for (const fieldName of fieldsToTrack) { for (const fieldName of fieldsToTrack) {
if (data.hasOwnProperty(fieldName)) { if (data.hasOwnProperty(fieldName)) {
const oldValue = originalProject[fieldName]; const oldValue = originalProject[fieldName];
const newValue = data[fieldName]; const newValue = data[fieldName];
if (oldValue !== newValue) { if (oldValue !== newValue) {
try { try {
logFieldChange('projects', parseInt(id), fieldName, oldValue, newValue, userId); logFieldChange('projects', parseInt(id), fieldName, oldValue, newValue, userId);
} catch (error) { } catch (error) {
console.error(`Failed to log field change for ${fieldName}:`, error); console.error(`Failed to log field change for ${fieldName}:`, error);
}
} }
} }
} }
}
updateProject(parseInt(id), data, userId); // Special handling for project cancellation
if (data.project_status === 'cancelled' && originalProject.project_status !== 'cancelled') {
// Get updated project const now = new Date();
const updatedProject = getProjectById(parseInt(id)); const cancellationDate = now.toLocaleDateString('pl-PL', {
year: 'numeric',
// Log project update month: '2-digit',
await logApiActionSafe( day: '2-digit',
req, hour: '2-digit',
AUDIT_ACTIONS.PROJECT_UPDATE, minute: '2-digit'
RESOURCE_TYPES.PROJECT, });
id,
req.auth, // Use req.auth instead of req.session const cancellationNote = `Projekt został wycofany w dniu ${cancellationDate}`;
{
originalData: originalProject, try {
updatedData: data, addNoteToProject(parseInt(id), cancellationNote, userId, true); // true for is_system
changedFields: Object.keys(data), } 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 }) { async function deleteProjectHandler(req, { params }) {

View File

@@ -29,6 +29,7 @@ function ProjectsMapPageContent() {
in_progress_design: true, in_progress_design: true,
in_progress_construction: true, in_progress_construction: true,
fulfilled: true, fulfilled: true,
cancelled: true,
}); });
const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap"); const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap");
const [activeOverlays, setActiveOverlays] = useState([]); const [activeOverlays, setActiveOverlays] = useState([]);
@@ -57,6 +58,11 @@ function ProjectsMapPageContent() {
label: "Completed", label: "Completed",
shortLabel: "Zakończony", shortLabel: "Zakończony",
}, },
cancelled: {
color: "#EF4444",
label: "Cancelled",
shortLabel: "Wycofany",
},
}; };
// Toggle all status filters // Toggle all status filters

View File

@@ -119,6 +119,7 @@ export default function ProjectListPage() {
case "in_progress_design": return t('projectStatus.in_progress_design'); case "in_progress_design": return t('projectStatus.in_progress_design');
case "in_progress_construction": return t('projectStatus.in_progress_construction'); case "in_progress_construction": return t('projectStatus.in_progress_construction');
case "fulfilled": return t('projectStatus.fulfilled'); case "fulfilled": return t('projectStatus.fulfilled');
case "cancelled": return t('projectStatus.cancelled');
default: return "-"; default: return "-";
} }
}; };

View File

@@ -38,6 +38,10 @@ export default function ProjectStatusDropdown({
label: t("projectStatus.fulfilled"), label: t("projectStatus.fulfilled"),
variant: "success", variant: "success",
}, },
cancelled: {
label: t("projectStatus.cancelled"),
variant: "danger",
},
}; };
const handleChange = async (newStatus) => { const handleChange = async (newStatus) => {
if (newStatus === status) { if (newStatus === status) {
@@ -50,11 +54,19 @@ export default function ProjectStatusDropdown({
setIsOpen(false); setIsOpen(false);
try { 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", method: "PUT",
headers: { "Content-Type": "application/json" }, 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(); window.location.reload();
} catch (error) { } catch (error) {
console.error("Failed to update status:", error); console.error("Failed to update status:", error);

View File

@@ -29,6 +29,10 @@ export default function ProjectStatusDropdownDebug({
label: "Completed", label: "Completed",
variant: "success", variant: "success",
}, },
cancelled: {
label: "Cancelled",
variant: "danger",
},
}; };
const handleChange = async (newStatus) => { const handleChange = async (newStatus) => {

View File

@@ -29,6 +29,10 @@ export default function ProjectStatusDropdownSimple({
label: "Completed", label: "Completed",
variant: "success", variant: "success",
}, },
cancelled: {
label: "Cancelled",
variant: "danger",
},
}; };
const handleChange = async (newStatus) => { const handleChange = async (newStatus) => {

View File

@@ -31,6 +31,7 @@ const statusColors = {
pending: "bg-yellow-100 text-yellow-800", pending: "bg-yellow-100 text-yellow-800",
in_progress: "bg-orange-100 text-orange-800", in_progress: "bg-orange-100 text-orange-800",
fulfilled: "bg-gray-100 text-gray-800", fulfilled: "bg-gray-100 text-gray-800",
cancelled: "bg-red-100 text-red-800",
}; };
const statusTranslations = { const statusTranslations = {
@@ -39,6 +40,7 @@ const statusTranslations = {
pending: "Oczekujący", pending: "Oczekujący",
in_progress: "W trakcie", in_progress: "W trakcie",
fulfilled: "Zakończony", fulfilled: "Zakończony",
cancelled: "Wycofany",
}; };
export default function ProjectCalendarWidget({ export default function ProjectCalendarWidget({

View File

@@ -32,6 +32,7 @@ export default function ProjectMap({
label: "In Progress (Construction)", label: "In Progress (Construction)",
}, },
fulfilled: { color: "#10B981", label: "Completed" }, fulfilled: { color: "#10B981", label: "Completed" },
cancelled: { color: "#EF4444", label: "Cancelled" },
}; };
useEffect(() => { useEffect(() => {

View File

@@ -105,6 +105,7 @@ const translations = {
in_progress_design: "W realizacji (projektowanie)", in_progress_design: "W realizacji (projektowanie)",
in_progress_construction: "W realizacji (realizacja)", in_progress_construction: "W realizacji (realizacja)",
fulfilled: "Zakończony", fulfilled: "Zakończony",
cancelled: "Wycofany",
unknown: "Nieznany" unknown: "Nieznany"
}, },
@@ -541,6 +542,7 @@ const translations = {
in_progress_design: "In Progress (Design)", in_progress_design: "In Progress (Design)",
in_progress_construction: "In Progress (Construction)", in_progress_construction: "In Progress (Construction)",
fulfilled: "Completed", fulfilled: "Completed",
cancelled: "Cancelled",
unknown: "Unknown" unknown: "Unknown"
}, },

View File

@@ -31,7 +31,7 @@ export default function initializeDatabase() {
contact TEXT, contact TEXT,
notes TEXT, notes TEXT,
project_type TEXT CHECK(project_type IN ('design', 'construction', 'design+construction')) DEFAULT 'design', 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) FOREIGN KEY (contract_id) REFERENCES contracts(contract_id)
); );
@@ -113,7 +113,7 @@ export default function initializeDatabase() {
// Migration: Add project_status column to projects table // Migration: Add project_status column to projects table
try { try {
db.exec(` 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) { } catch (e) {
// Column already exists, ignore error // Column already exists, ignore error

View File

@@ -16,13 +16,13 @@ export function getNotesByProjectId(project_id) {
.all(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( db.prepare(
` `
INSERT INTO notes (project_id, note, created_by, note_date) INSERT INTO notes (project_id, note, created_by, is_system, note_date)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
` `
).run(project_id, note, created_by); ).run(project_id, note, created_by, is_system ? 1 : 0);
} }
export function getNotesByTaskId(task_id) { export function getNotesByTaskId(task_id) {

View File

@@ -110,7 +110,7 @@ export function updateProject(id, data, userId = null) {
coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ? WHERE project_id = ?
`); `);
stmt.run( const result = stmt.run(
data.contract_id, data.contract_id,
data.project_name, data.project_name,
data.project_number, data.project_number,
@@ -130,6 +130,9 @@ export function updateProject(id, data, userId = null) {
data.assigned_to || null, data.assigned_to || null,
id id
); );
console.log('Update result:', result);
return result;
} }
export function deleteProject(id) { export function deleteProject(id) {

View File

@@ -15,6 +15,8 @@ export const formatProjectStatus = (status) => {
return "W realizacji (realizacja)"; return "W realizacji (realizacja)";
case "fulfilled": case "fulfilled":
return "Zakończony"; return "Zakończony";
case "cancelled":
return "Wycofany";
default: default:
return "-"; return "-";
} }