feat: Implement internationalization for task management components
- Added translation support for task-related strings in ProjectTaskForm and ProjectTasksSection components. - Integrated translation for navigation items in the Navigation component. - Created ProjectCalendarWidget component with Polish translations for project statuses and deadlines. - Developed Tooltip component for enhanced user experience with tooltips. - Established a field change history logging system in the database with associated queries. - Enhanced task update logging to include translated status and priority changes. - Introduced server-side translations for system messages to improve localization.
This commit is contained in:
46
src/app/api/field-history/route.js
Normal file
46
src/app/api/field-history/route.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// Force this API route to use Node.js runtime for database access
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { getFieldHistory, hasFieldHistory } from "@/lib/queries/fieldHistory";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withReadAuth } from "@/lib/middleware/auth";
|
||||
import initializeDatabase from "@/lib/init-db";
|
||||
|
||||
// Make sure the DB is initialized before queries run
|
||||
initializeDatabase();
|
||||
|
||||
async function getFieldHistoryHandler(req) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const tableName = searchParams.get("table_name");
|
||||
const recordId = searchParams.get("record_id");
|
||||
const fieldName = searchParams.get("field_name");
|
||||
const checkOnly = searchParams.get("check_only") === "true";
|
||||
|
||||
if (!tableName || !recordId || !fieldName) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required parameters: table_name, record_id, field_name" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (checkOnly) {
|
||||
// Just check if history exists
|
||||
const exists = hasFieldHistory(tableName, parseInt(recordId), fieldName);
|
||||
return NextResponse.json({ hasHistory: exists });
|
||||
} else {
|
||||
// Get full history
|
||||
const history = getFieldHistory(tableName, parseInt(recordId), fieldName);
|
||||
return NextResponse.json(history);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching field history:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch field history" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected route - require read authentication
|
||||
export const GET = withReadAuth(getFieldHistoryHandler);
|
||||
72
src/app/api/notes/[id]/route.js
Normal file
72
src/app/api/notes/[id]/route.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// Force this API route to use Node.js runtime for database access
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import db from "@/lib/db";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withUserAuth } from "@/lib/middleware/auth";
|
||||
import {
|
||||
logApiActionSafe,
|
||||
AUDIT_ACTIONS,
|
||||
RESOURCE_TYPES,
|
||||
} from "@/lib/auditLogSafe.js";
|
||||
import initializeDatabase from "@/lib/init-db";
|
||||
|
||||
// Make sure the DB is initialized before queries run
|
||||
initializeDatabase();
|
||||
|
||||
async function deleteNoteHandler(req, { params }) {
|
||||
const { id } = await params;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "Note ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get note data before deletion for audit log
|
||||
const note = db.prepare("SELECT * FROM notes WHERE note_id = ?").get(id);
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json({ error: "Note not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if user has permission to delete this note
|
||||
// Users can delete their own notes, or admins can delete any note
|
||||
const userRole = req.user?.role;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (userRole !== 'admin' && note.created_by !== userId) {
|
||||
return NextResponse.json({ error: "Unauthorized to delete this note" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Delete the note
|
||||
db.prepare("DELETE FROM notes WHERE note_id = ?").run(id);
|
||||
|
||||
// Log note deletion
|
||||
await logApiActionSafe(
|
||||
req,
|
||||
AUDIT_ACTIONS.NOTE_DELETE,
|
||||
RESOURCE_TYPES.NOTE,
|
||||
id,
|
||||
req.auth,
|
||||
{
|
||||
deletedNote: {
|
||||
project_id: note?.project_id,
|
||||
task_id: note?.task_id,
|
||||
note_length: note?.note?.length || 0,
|
||||
created_by: note?.created_by,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting note:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete note", details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected route - require user authentication
|
||||
export const DELETE = withUserAuth(deleteNoteHandler);
|
||||
@@ -3,13 +3,59 @@ export const runtime = "nodejs";
|
||||
|
||||
import db from "@/lib/db";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withUserAuth } from "@/lib/middleware/auth";
|
||||
import { withUserAuth, withReadAuth } from "@/lib/middleware/auth";
|
||||
import {
|
||||
logApiActionSafe,
|
||||
AUDIT_ACTIONS,
|
||||
RESOURCE_TYPES,
|
||||
} from "@/lib/auditLogSafe.js";
|
||||
|
||||
async function getNotesHandler(req) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const projectId = searchParams.get("project_id");
|
||||
const taskId = searchParams.get("task_id");
|
||||
|
||||
let query;
|
||||
let params;
|
||||
|
||||
if (projectId) {
|
||||
query = `
|
||||
SELECT n.*,
|
||||
u.name as created_by_name,
|
||||
u.username as created_by_username
|
||||
FROM notes n
|
||||
LEFT JOIN users u ON n.created_by = u.id
|
||||
WHERE n.project_id = ?
|
||||
ORDER BY n.note_date DESC
|
||||
`;
|
||||
params = [projectId];
|
||||
} else if (taskId) {
|
||||
query = `
|
||||
SELECT n.*,
|
||||
u.name as created_by_name,
|
||||
u.username as created_by_username
|
||||
FROM notes n
|
||||
LEFT JOIN users u ON n.created_by = u.id
|
||||
WHERE n.task_id = ?
|
||||
ORDER BY n.note_date DESC
|
||||
`;
|
||||
params = [taskId];
|
||||
} else {
|
||||
return NextResponse.json({ error: "project_id or task_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const notes = db.prepare(query).all(...params);
|
||||
return NextResponse.json(notes);
|
||||
} catch (error) {
|
||||
console.error("Error fetching notes:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch notes" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNoteHandler(req) {
|
||||
const { project_id, task_id, note } = await req.json();
|
||||
|
||||
@@ -118,6 +164,7 @@ async function updateNoteHandler(req, { params }) {
|
||||
}
|
||||
|
||||
// Protected routes - require authentication
|
||||
export const GET = withReadAuth(getNotesHandler);
|
||||
export const POST = withUserAuth(createNoteHandler);
|
||||
export const DELETE = withUserAuth(deleteNoteHandler);
|
||||
export const PUT = withUserAuth(updateNoteHandler);
|
||||
|
||||
24
src/app/api/projects/[id]/finish-date-updates/route.js
Normal file
24
src/app/api/projects/[id]/finish-date-updates/route.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Force this API route to use Node.js runtime for database access
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { getFinishDateUpdates } from "@/lib/queries/projects";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withReadAuth } from "@/lib/middleware/auth";
|
||||
|
||||
async function getFinishDateUpdatesHandler(req, { params }) {
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const updates = getFinishDateUpdates(parseInt(id));
|
||||
return NextResponse.json(updates);
|
||||
} catch (error) {
|
||||
console.error("Error fetching finish date updates:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch finish date updates" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected route - require authentication
|
||||
export const GET = withReadAuth(getFinishDateUpdatesHandler);
|
||||
@@ -3,9 +3,11 @@ export const runtime = "nodejs";
|
||||
|
||||
import {
|
||||
getProjectById,
|
||||
getProjectWithContract,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
} from "@/lib/queries/projects";
|
||||
import { logFieldChange } from "@/lib/queries/fieldHistory";
|
||||
import initializeDatabase from "@/lib/init-db";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
||||
@@ -20,7 +22,7 @@ initializeDatabase();
|
||||
|
||||
async function getProjectHandler(req, { params }) {
|
||||
const { id } = await params;
|
||||
const project = getProjectById(parseInt(id));
|
||||
const project = getProjectWithContract(parseInt(id));
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
||||
@@ -46,9 +48,31 @@ async function updateProjectHandler(req, { params }) {
|
||||
// Get user ID from authenticated request
|
||||
const userId = req.user?.id;
|
||||
|
||||
// Get original project data for audit log
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
48
src/app/api/task-notes/[id]/route.js
Normal file
48
src/app/api/task-notes/[id]/route.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { deleteNote } from "@/lib/queries/notes";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withUserAuth } from "@/lib/middleware/auth";
|
||||
import db from "@/lib/db";
|
||||
|
||||
// DELETE: Delete a specific task note
|
||||
async function deleteTaskNoteHandler(req, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "Note ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get note data before deletion for permission checking
|
||||
const note = db.prepare("SELECT * FROM notes WHERE note_id = ?").get(id);
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json({ error: "Note not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if user has permission to delete this note
|
||||
// Users can delete their own notes, or admins can delete any note
|
||||
const userRole = req.user?.role;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (userRole !== 'admin' && note.created_by !== userId) {
|
||||
return NextResponse.json({ error: "Unauthorized to delete this note" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Don't allow deletion of system notes by regular users
|
||||
if (note.is_system && userRole !== 'admin') {
|
||||
return NextResponse.json({ error: "Cannot delete system notes" }, { status: 403 });
|
||||
}
|
||||
|
||||
deleteNote(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting task note:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete task note" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected route - require user authentication
|
||||
export const DELETE = withUserAuth(deleteTaskNoteHandler);
|
||||
Reference in New Issue
Block a user