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:
2025-09-11 15:49:07 +02:00
parent 50adc50a24
commit 0dd988730f
24 changed files with 1945 additions and 114 deletions

View File

@@ -12,6 +12,7 @@ const translations = {
navigation: {
dashboard: "Panel główny",
projects: "Projekty",
calendar: "Kalendarz",
taskTemplates: "Szablony zadań",
projectTasks: "Zadania projektów",
contracts: "Umowy",
@@ -62,7 +63,14 @@ const translations = {
addNote: "Dodaj notatkę",
addNoteSuccess: "Notatka została dodana",
addNoteError: "Nie udało się zapisać notatki",
addNotePlaceholder: "Dodaj nową notatkę..."
addNotePlaceholder: "Dodaj nową notatkę...",
status: "Status",
type: "Typ",
actions: "Akcje",
view: "Wyświetl",
clearSearch: "Wyczyść wyszukiwanie",
clearAllFilters: "Wyczyść wszystkie filtry",
sortBy: "Sortuj według"
},
// Dashboard
@@ -127,9 +135,10 @@ const translations = {
city: "Miasto",
address: "Adres",
plot: "Działka",
district: "Dzielnica",
unit: "Jednostka",
district: "Jednostka ewidencyjna",
unit: "Obręb",
finishDate: "Data zakończenia",
type: "Typ",
contact: "Kontakt",
coordinates: "Współrzędne",
notes: "Notatki",
@@ -144,19 +153,44 @@ const translations = {
searchPlaceholder: "Szukaj projektów po nazwie, mieście lub adresie...",
noProjects: "Brak projektów",
noProjectsMessage: "Rozpocznij od utworzenia swojego pierwszego projektu.",
notFinished: "Nie zakończone",
projects: "projektów",
mapView: "Widok mapy",
createFirstProject: "Utwórz pierwszy projekt",
noMatchingResults: "Brak projektów pasujących do kryteriów wyszukiwania. Spróbuj zmienić wyszukiwane frazy.",
showingResults: "Wyświetlono {shown} z {total} projektów",
deleteConfirm: "Czy na pewno chcesz usunąć ten projekt?",
deleteSuccess: "Projekt został pomyślnie usunięty",
createSuccess: "Projekt został pomyślnie utworzony",
updateSuccess: "Projekt został pomyślnie zaktualizowany",
saveError: "Nie udało się zapisać projektu",
enterProjectName: "Wprowadź nazwę projektu",
enterCity: "Wprowadź miasto",
enterAddress: "Wprowadź adres",
enterPlotNumber: "Wprowadź numer działki",
enterDistrict: "Wprowadź dzielnicę",
enterUnit: "Wprowadź jednostkę",
enterDistrict: "Wprowadź jednostkę ewidencyjną",
enterUnit: "Wprowadź obręb",
enterContactDetails: "Wprowadź dane kontaktowe",
coordinatesPlaceholder: "np. 49.622958,20.629562",
enterNotes: "Wprowadź notatki..."
enterNotes: "Wprowadź notatki...",
createProject: "Utwórz projekt",
updateProject: "Aktualizuj projekt",
creating: "Tworzenie...",
updating: "Aktualizowanie...",
projectDetails: "Szczegóły projektu",
editProjectDetails: "Edytuj szczegóły projektu",
contract: "Umowa",
selectContract: "Wybierz umowę",
investmentNumber: "Numer inwestycji",
enterInvestmentNumber: "Wprowadź numer inwestycji",
enterWP: "Wprowadź WP",
placeholders: {
contact: "Wprowadź dane kontaktowe",
coordinates: "np. 49.622958,20.629562",
notes: "Wprowadź notatki...",
investmentNumber: "Wprowadź numer inwestycji",
wp: "Wprowadź WP"
}
},
// Contracts
@@ -240,6 +274,19 @@ const translations = {
actions: "Działania",
urgent: "Pilne",
updateTask: "Aktualizuj zadanie",
taskType: "Typ zadania",
fromTemplate: "Z szablonu",
customTask: "Zadanie niestandardowe",
selectTemplate: "Wybierz szablon zadania",
chooseTemplate: "Wybierz szablon zadania...",
enterTaskName: "Wprowadź nazwę zadania...",
enterDescription: "Wprowadź opis zadania (opcjonalnie)...",
addTask: "Dodaj zadanie",
adding: "Dodawanie...",
addTaskError: "Nie udało się dodać zadania do projektu",
notes: "notatki",
deleteNote: "Usuń notatkę",
deleteError: "Błąd usuwania zadania",
notStarted: "Nie rozpoczęte",
days: "dni"
},
@@ -454,7 +501,14 @@ const translations = {
addNote: "Add Note",
addNoteSuccess: "Note added",
addNoteError: "Failed to save note",
addNotePlaceholder: "Add a new note..."
addNotePlaceholder: "Add a new note...",
status: "Status",
type: "Type",
actions: "Actions",
view: "View",
clearSearch: "Clear search",
clearAllFilters: "Clear all filters",
sortBy: "Sort by"
},
dashboard: {
@@ -517,6 +571,7 @@ const translations = {
district: "District",
unit: "Unit",
finishDate: "Finish Date",
type: "Type",
contact: "Contact",
coordinates: "Coordinates",
notes: "Notes",
@@ -531,10 +586,17 @@ const translations = {
searchPlaceholder: "Search projects by name, city or address...",
noProjects: "No projects",
noProjectsMessage: "Get started by creating your first project.",
notFinished: "Not finished",
projects: "projects",
mapView: "Map View",
createFirstProject: "Create first project",
noMatchingResults: "No projects match the search criteria. Try changing your search terms.",
showingResults: "Showing {shown} of {total} projects",
deleteConfirm: "Are you sure you want to delete this project?",
deleteSuccess: "Project deleted successfully",
createSuccess: "Project created successfully",
updateSuccess: "Project updated successfully",
saveError: "Failed to save project",
enterProjectName: "Enter project name",
enterCity: "Enter city",
enterAddress: "Enter address",
@@ -543,7 +605,25 @@ const translations = {
enterUnit: "Enter unit",
enterContactDetails: "Enter contact details",
coordinatesPlaceholder: "e.g., 49.622958,20.629562",
enterNotes: "Enter notes..."
enterNotes: "Enter notes...",
createProject: "Create Project",
updateProject: "Update Project",
creating: "Creating...",
updating: "Updating...",
projectDetails: "Project Details",
editProjectDetails: "Edit Project Details",
contract: "Contract",
selectContract: "Select Contract",
investmentNumber: "Investment Number",
enterInvestmentNumber: "Enter investment number",
enterWP: "Enter WP",
placeholders: {
contact: "Enter contact details",
coordinates: "e.g., 49.622958,20.629562",
notes: "Enter notes...",
investmentNumber: "Enter investment number",
wp: "Enter WP"
}
},
contracts: {
@@ -625,6 +705,19 @@ const translations = {
actions: "Actions",
urgent: "Urgent",
updateTask: "Update Task",
taskType: "Task Type",
fromTemplate: "From Template",
customTask: "Custom Task",
selectTemplate: "Select Task Template",
chooseTemplate: "Choose a task template...",
enterTaskName: "Enter custom task name...",
enterDescription: "Enter task description (optional)...",
addTask: "Add Task",
adding: "Adding...",
addTaskError: "Failed to add task to project",
notes: "notes",
deleteNote: "Delete note",
deleteError: "Error deleting task",
notStarted: "Not started",
days: "days"
},

View File

@@ -362,5 +362,24 @@ export default function initializeDatabase() {
-- Create indexes for file attachments
CREATE INDEX IF NOT EXISTS idx_file_attachments_entity ON file_attachments(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_file_attachments_uploaded_by ON file_attachments(uploaded_by);
-- Generic field change history table
CREATE TABLE IF NOT EXISTS field_change_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
record_id INTEGER NOT NULL,
field_name TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
changed_by INTEGER,
changed_at TEXT DEFAULT CURRENT_TIMESTAMP,
change_reason TEXT,
FOREIGN KEY (changed_by) REFERENCES users(id)
);
-- Create indexes for field change history
CREATE INDEX IF NOT EXISTS idx_field_history_table_record ON field_change_history(table_name, record_id);
CREATE INDEX IF NOT EXISTS idx_field_history_field ON field_change_history(table_name, record_id, field_name);
CREATE INDEX IF NOT EXISTS idx_field_history_changed_by ON field_change_history(changed_by);
`);
}

View File

@@ -0,0 +1,94 @@
import db from "../db.js";
/**
* Log a field change to the history table
*/
export function logFieldChange(tableName, recordId, fieldName, oldValue, newValue, changedBy = null, reason = null) {
// Don't log if values are the same
if (oldValue === newValue) return null;
const stmt = db.prepare(`
INSERT INTO field_change_history
(table_name, record_id, field_name, old_value, new_value, changed_by, change_reason)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
return stmt.run(
tableName,
recordId,
fieldName,
oldValue || null,
newValue || null,
changedBy,
reason
);
}
/**
* Get field change history for a specific field
*/
export function getFieldHistory(tableName, recordId, fieldName) {
const stmt = db.prepare(`
SELECT
fch.*,
u.name as changed_by_name,
u.username as changed_by_username
FROM field_change_history fch
LEFT JOIN users u ON fch.changed_by = u.id
WHERE fch.table_name = ? AND fch.record_id = ? AND fch.field_name = ?
ORDER BY fch.changed_at DESC
`);
return stmt.all(tableName, recordId, fieldName);
}
/**
* Get all field changes for a specific record
*/
export function getAllFieldHistory(tableName, recordId) {
const stmt = db.prepare(`
SELECT
fch.*,
u.name as changed_by_name,
u.username as changed_by_username
FROM field_change_history fch
LEFT JOIN users u ON fch.changed_by = u.id
WHERE fch.table_name = ? AND fch.record_id = ?
ORDER BY fch.changed_at DESC, fch.field_name ASC
`);
return stmt.all(tableName, recordId);
}
/**
* Check if a field has any change history
*/
export function hasFieldHistory(tableName, recordId, fieldName) {
const stmt = db.prepare(`
SELECT COUNT(*) as count
FROM field_change_history
WHERE table_name = ? AND record_id = ? AND field_name = ?
`);
const result = stmt.get(tableName, recordId, fieldName);
return result.count > 0;
}
/**
* Get the most recent change for a field
*/
export function getLatestFieldChange(tableName, recordId, fieldName) {
const stmt = db.prepare(`
SELECT
fch.*,
u.name as changed_by_name,
u.username as changed_by_username
FROM field_change_history fch
LEFT JOIN users u ON fch.changed_by = u.id
WHERE fch.table_name = ? AND fch.record_id = ? AND fch.field_name = ?
ORDER BY fch.changed_at DESC
LIMIT 1
`);
return stmt.get(tableName, recordId, fieldName);
}

View File

@@ -6,7 +6,7 @@ export function getNotesByProjectId(project_id) {
`
SELECT n.*,
u.name as created_by_name,
u.email as created_by_email
u.username as created_by_username
FROM notes n
LEFT JOIN users u ON n.created_by = u.id
WHERE n.project_id = ?
@@ -31,7 +31,7 @@ export function getNotesByTaskId(task_id) {
`
SELECT n.*,
u.name as created_by_name,
u.email as created_by_email
u.username as created_by_username
FROM notes n
LEFT JOIN users u ON n.created_by = u.id
WHERE n.task_id = ?
@@ -64,7 +64,7 @@ export function getAllNotesWithUsers() {
`
SELECT n.*,
u.name as created_by_name,
u.email as created_by_email,
u.username as created_by_username,
p.project_name,
COALESCE(pt.custom_task_name, t.name) as task_name
FROM notes n
@@ -85,7 +85,7 @@ export function getNotesByCreator(userId) {
`
SELECT n.*,
u.name as created_by_name,
u.email as created_by_email,
u.username as created_by_username,
p.project_name,
COALESCE(pt.custom_task_name, t.name) as task_name
FROM notes n

View File

@@ -1,5 +1,6 @@
import db from "../db.js";
import { addNoteToTask } from "./notes.js";
import { getUserLanguage, serverT, translateStatus, translatePriority } from "../serverTranslations.js";
// Get all task templates (for dropdown selection)
export function getAllTaskTemplates() {
@@ -182,8 +183,10 @@ export function updateProjectTaskStatus(taskId, status, userId = null) {
// Add system note for status change (only if status actually changed)
if (result.changes > 0 && oldStatus !== status) {
const taskName = currentTask.task_name || "Unknown task";
const logMessage = `Status changed from "${oldStatus}" to "${status}"`;
const language = getUserLanguage(); // Default to Polish for now
const fromStatus = translateStatus(oldStatus, language);
const toStatus = translateStatus(status, language);
const logMessage = `${serverT("Status changed from", language)} "${fromStatus}" ${serverT("to", language)} "${toStatus}"`;
addNoteToTask(taskId, logMessage, true, userId);
}
@@ -359,41 +362,48 @@ export function updateProjectTask(taskId, updates, userId = null) {
// Log the update
if (userId) {
const language = getUserLanguage(); // Default to Polish for now
const changes = [];
if (
updates.priority !== undefined &&
updates.priority !== currentTask.priority
) {
const oldPriority = translatePriority(currentTask.priority, language) || serverT("None", language);
const newPriority = translatePriority(updates.priority, language) || serverT("None", language);
changes.push(
`Priority: ${currentTask.priority || "None"}${
updates.priority || "None"
}`
`${serverT("Priority", language)}: ${oldPriority}${newPriority}`
);
}
if (updates.status !== undefined && updates.status !== currentTask.status) {
const oldStatus = translateStatus(currentTask.status, language) || serverT("None", language);
const newStatus = translateStatus(updates.status, language) || serverT("None", language);
changes.push(
`Status: ${currentTask.status || "None"} ${updates.status || "None"}`
`${serverT("Status", language)}: ${oldStatus}${newStatus}`
);
}
if (
updates.assigned_to !== undefined &&
updates.assigned_to !== currentTask.assigned_to
) {
changes.push(`Assignment updated`);
changes.push(serverT("Assignment updated", language));
}
if (
updates.date_started !== undefined &&
updates.date_started !== currentTask.date_started
) {
const oldDate = currentTask.date_started || serverT("None", language);
const newDate = updates.date_started || serverT("None", language);
changes.push(
`Date started: ${currentTask.date_started || "None"}${
updates.date_started || "None"
}`
`${serverT("Date started", language)}: ${oldDate}${newDate}`
);
}
if (changes.length > 0) {
const logMessage = `Task updated: ${changes.join(", ")}`;
const logMessage = `${serverT("Task updated", language)}: ${changes.join(", ")}`;
addNoteToTask(taskId, logMessage, true, userId);
}
}

View File

@@ -0,0 +1,79 @@
// Server-side translations for system messages
// This is separate from the client-side translation system
const serverTranslations = {
pl: {
"Status changed from": "Status zmieniony z",
"to": "na",
"Priority": "Priorytet",
"Status": "Status",
"Assignment updated": "Przypisanie zaktualizowane",
"Date started": "Data rozpoczęcia",
"None": "Brak",
"Task updated": "Zadanie zaktualizowane",
"pending": "oczekujące",
"in_progress": "w trakcie",
"completed": "ukończone",
"cancelled": "anulowane",
"on_hold": "wstrzymane",
"low": "niski",
"normal": "normalny",
"medium": "średni",
"high": "wysoki",
"urgent": "pilny"
},
en: {
"Status changed from": "Status changed from",
"to": "to",
"Priority": "Priority",
"Status": "Status",
"Assignment updated": "Assignment updated",
"Date started": "Date started",
"None": "None",
"Task updated": "Task updated",
"pending": "pending",
"in_progress": "in_progress",
"completed": "completed",
"cancelled": "cancelled",
"on_hold": "on_hold",
"low": "low",
"normal": "normal",
"medium": "medium",
"high": "high",
"urgent": "urgent"
}
};
// Get user's preferred language from request headers or default to Polish
export function getUserLanguage(req = null) {
// For now, default to Polish. In the future, this could be determined from:
// - Request headers (Accept-Language)
// - User profile settings
// - Session data
return 'pl';
}
// Translate a key for server-side use
export function serverT(key, language = 'pl') {
if (serverTranslations[language] && serverTranslations[language][key]) {
return serverTranslations[language][key];
}
// Fallback to English if Polish not found
if (language !== 'en' && serverTranslations.en[key]) {
return serverTranslations.en[key];
}
// Return the key itself if no translation found
return key;
}
// Helper function to translate status values
export function translateStatus(status, language = 'pl') {
return serverT(status, language);
}
// Helper function to translate priority values
export function translatePriority(priority, language = 'pl') {
return serverT(priority, language);
}