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:
109
src/lib/i18n.js
109
src/lib/i18n.js
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
`);
|
||||
}
|
||||
|
||||
94
src/lib/queries/fieldHistory.js
Normal file
94
src/lib/queries/fieldHistory.js
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
79
src/lib/serverTranslations.js
Normal file
79
src/lib/serverTranslations.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user