feat: Add support for project cancellation status across the application
This commit is contained in:
@@ -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') {
|
||||||
|
const now = new Date();
|
||||||
|
const cancellationDate = now.toLocaleDateString('pl-PL', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
// Get updated project
|
const cancellationNote = `Projekt został wycofany w dniu ${cancellationDate}`;
|
||||||
const updatedProject = getProjectById(parseInt(id));
|
|
||||||
|
|
||||||
// Log project update
|
try {
|
||||||
await logApiActionSafe(
|
addNoteToProject(parseInt(id), cancellationNote, userId, true); // true for is_system
|
||||||
req,
|
} catch (error) {
|
||||||
AUDIT_ACTIONS.PROJECT_UPDATE,
|
console.error('Failed to log project cancellation:', error);
|
||||||
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);
|
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 }) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 "-";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 "-";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user