diff --git a/backend/src/db/api/schools.js b/backend/src/db/api/schools.js index d6259a7..6b0ab6e 100644 --- a/backend/src/db/api/schools.js +++ b/backend/src/db/api/schools.js @@ -1,7 +1,5 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); @@ -25,6 +23,14 @@ module.exports = class SchoolsDBApi { || null , + nif: data.nif || null, + phone: data.phone || null, + email: data.email || null, + province: data.province || null, + municipality: data.municipality || null, + address: data.address || null, + logoUrl: data.logoUrl || null, + status: data.status || 'active', importHash: data.importHash || null, createdById: currentUser.id, @@ -55,6 +61,14 @@ module.exports = class SchoolsDBApi { || null , + nif: item.nif || null, + phone: item.phone || null, + email: item.email || null, + province: item.province || null, + municipality: item.municipality || null, + address: item.address || null, + logoUrl: item.logoUrl || null, + status: item.status || 'active', importHash: item.importHash || null, createdById: currentUser.id, @@ -74,8 +88,6 @@ module.exports = class SchoolsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const schools = await db.schools.findByPk(id, {}, {transaction}); @@ -84,6 +96,14 @@ module.exports = class SchoolsDBApi { const updatePayload = {}; if (data.name !== undefined) updatePayload.name = data.name; + if (data.nif !== undefined) updatePayload.nif = data.nif; + if (data.phone !== undefined) updatePayload.phone = data.phone; + if (data.email !== undefined) updatePayload.email = data.email; + if (data.province !== undefined) updatePayload.province = data.province; + if (data.municipality !== undefined) updatePayload.municipality = data.municipality; + if (data.address !== undefined) updatePayload.address = data.address; + if (data.logoUrl !== undefined) updatePayload.logoUrl = data.logoUrl; + if (data.status !== undefined) updatePayload.status = data.status; updatePayload.updatedById = currentUser.id; @@ -286,10 +306,6 @@ module.exports = class SchoolsDBApi { offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ @@ -316,6 +332,24 @@ module.exports = class SchoolsDBApi { }; } + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.province) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'schools', + 'province', + filter.province, + ), + }; + } + diff --git a/backend/src/db/migrations/20260623001000-enhance-schools-tenant-profile.js b/backend/src/db/migrations/20260623001000-enhance-schools-tenant-profile.js new file mode 100644 index 0000000..1cab861 --- /dev/null +++ b/backend/src/db/migrations/20260623001000-enhance-schools-tenant-profile.js @@ -0,0 +1,50 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + await queryInterface.addColumn('schools', 'nif', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('schools', 'phone', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('schools', 'email', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('schools', 'province', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('schools', 'municipality', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('schools', 'address', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('schools', 'logoUrl', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn( + 'schools', + 'status', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'active', + }, + { transaction }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, + + async down(queryInterface) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + await queryInterface.removeColumn('schools', 'status', { transaction }); + await queryInterface.removeColumn('schools', 'logoUrl', { transaction }); + await queryInterface.removeColumn('schools', 'address', { transaction }); + await queryInterface.removeColumn('schools', 'municipality', { transaction }); + await queryInterface.removeColumn('schools', 'province', { transaction }); + await queryInterface.removeColumn('schools', 'email', { transaction }); + await queryInterface.removeColumn('schools', 'phone', { transaction }); + await queryInterface.removeColumn('schools', 'nif', { transaction }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, +}; diff --git a/backend/src/db/models/schools.js b/backend/src/db/models/schools.js index 6d26cfd..38d30a0 100644 --- a/backend/src/db/models/schools.js +++ b/backend/src/db/models/schools.js @@ -1,9 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); - module.exports = function(sequelize, DataTypes) { const schools = sequelize.define( 'schools', @@ -19,6 +13,63 @@ name: { + }, + +nif: { + type: DataTypes.TEXT, + + + + }, + +phone: { + type: DataTypes.TEXT, + + + + }, + +email: { + type: DataTypes.TEXT, + + + + }, + +province: { + type: DataTypes.TEXT, + + + + }, + +municipality: { + type: DataTypes.TEXT, + + + + }, + +address: { + type: DataTypes.TEXT, + + + + }, + +logoUrl: { + type: DataTypes.TEXT, + + + + }, + +status: { + type: DataTypes.TEXT, + allowNull: false, + defaultValue: 'active', + + }, importHash: { diff --git a/backend/src/routes/schools.js b/backend/src/routes/schools.js index 1b43420..13f49bd 100644 --- a/backend/src/routes/schools.js +++ b/backend/src/routes/schools.js @@ -31,9 +31,22 @@ router.use(checkCrudPermissions('schools')); * name: * type: string * default: name - - - + * nif: + * type: string + * phone: + * type: string + * email: + * type: string + * province: + * type: string + * municipality: + * type: string + * address: + * type: string + * logoUrl: + * type: string + * status: + * type: string */ /** @@ -292,11 +305,7 @@ router.get('/', wrapAsync(async (req, res) => { req.query, globalAccess, { currentUser } ); if (filetype && filetype === 'csv') { - const fields = ['id','name', - - - - ]; + const fields = ['id','name','nif','phone','email','province','municipality','address','logoUrl','status']; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/services/schools.js b/backend/src/services/schools.js index cb120b5..4787552 100644 --- a/backend/src/services/schools.js +++ b/backend/src/services/schools.js @@ -3,20 +3,50 @@ const SchoolsDBApi = require('../db/api/schools'); const processFile = require("../middlewares/upload"); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); const stream = require('stream'); +const SCHOOL_STATUSES = new Set(['active', 'setup', 'suspended']); +function cleanString(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return text || null; +} +function normalizeSchoolData(data = {}) { + const normalized = { + name: cleanString(data.name), + nif: cleanString(data.nif), + phone: cleanString(data.phone), + email: cleanString(data.email), + province: cleanString(data.province), + municipality: cleanString(data.municipality), + address: cleanString(data.address), + logoUrl: cleanString(data.logoUrl), + status: cleanString(data.status) || 'active', + }; + if (!normalized.name) { + throw new ValidationError('schoolsNameRequired'); + } + + if (normalized.email && !normalized.email.includes('@')) { + throw new ValidationError('schoolsEmailInvalid'); + } + + if (!SCHOOL_STATUSES.has(normalized.status)) { + throw new ValidationError('schoolsStatusInvalid'); + } + + return normalized; +} module.exports = class SchoolsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { await SchoolsDBApi.create( - data, + normalizeSchoolData(data), { currentUser, transaction, @@ -28,9 +58,9 @@ module.exports = class SchoolsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { @@ -81,7 +111,7 @@ module.exports = class SchoolsService { const updatedSchools = await SchoolsDBApi.update( id, - data, + normalizeSchoolData(data), { currentUser, transaction, @@ -95,7 +125,7 @@ module.exports = class SchoolsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 8d45685..54b1c07 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -4,34 +4,66 @@ "pageTitle": "Dashboard", "overview": "Overview", "loadingWidgets": "Loading widgets...", - "loading": "Loading..." + "loading": "Loading...", + "roleWidgets": "{{role}}'s widgets" }, "login": { "pageTitle": "Login", - "form": { - "loginLabel": "Login", - "loginHelp": "Please enter your login", - "passwordLabel": "Password", - "passwordHelp": "Please enter your password", - "remember": "Remember", - "forgotPassword": "Forgot password?", - "loginButton": "Login", - "loading": "Loading...", - "noAccountYet": "Don’t have an account yet?", - "newAccount": "New Account" + "loginLabel": "Login", + "loginHelp": "Please enter your login", + "passwordLabel": "Password", + "passwordHelp": "Please enter your password", + "remember": "Remember", + "forgotPassword": "Forgot password?", + "loginButton": "Login", + "loading": "Loading...", + "noAccountYet": "Don’t have an account yet?", + "newAccount": "New Account" }, - "pexels": { "photoCredit": "Photo by {{photographer}} on Pexels", "videoCredit": "Video by {{name}} on Pexels", "videoUnsupported": "Your browser does not support the video tag." }, - "footer": { "copyright": "© {{year}} {{title}}. All rights reserved", - "privacy": "Privacy Policy" - } + "privacy": "Privacy Policy" + }, + "sampleCredentialsSuperAdmin": "Use to login as Super Admin:", + "sampleCredentialsAdmin": "Use to login as Admin:", + "sampleCredentialsUser": "Use to login as User:" + }, + "auth": { + "emailLabel": "Email", + "emailHelp": "Please enter your email", + "passwordLabel": "Password", + "passwordHelp": "Please enter your password", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordHelp": "Please confirm your password", + "loading": "Loading...", + "login": "Login", + "checkEmailVerification": "Please check your email for verification link", + "genericError": "Something was wrong. Try again" + }, + "register": { + "pageTitle": "Register", + "organization": "Organization", + "selectOrganization": "Select organization...", + "submit": "Register" + }, + "forgot": { + "pageTitle": "Forgot password", + "submit": "Submit" + }, + "verifyEmail": { + "pageTitle": "Verify Email", + "success": "Your email was verified" + }, + "password": { + "setTitle": "Set Password", + "resetTitle": "Reset Password", + "enterNewPassword": "Enter your new password" } }, "components": { @@ -41,12 +73,114 @@ "settingsTitle": "Widget Creator Settings", "settingsDescription": "What role are we showing and creating widgets for?", "doneButton": "Done", - "loading": "Loading..." + "loading": "Loading...", + "creationError": "Error with widget creation" }, "search": { "placeholder": "Search", "required": "Required", "minLength": "Minimum length: {{count}} characters" } + }, + "navigation": { + "aside": { + "dashboard": "Dashboard", + "users": "Users", + "roles": "Roles", + "permissions": "Permissions", + "schoolOnboarding": "School Onboarding", + "academicMvp": "Academic MVP", + "schoolsCrud": "Schools CRUD", + "students": "Students", + "guardians": "Guardians", + "studentGuardians": "Student guardians", + "teachers": "Teachers", + "courses": "Courses", + "grades": "Grades", + "classes": "Classes", + "subjects": "Subjects", + "enrollments": "Enrollments", + "assessments": "Assessments", + "attendance": "Attendance", + "invoices": "Invoices", + "payments": "Payments", + "employees": "Employees", + "products": "Products", + "books": "Books", + "bookLoans": "Book loans", + "profile": "Profile", + "swaggerApi": "Swagger API" + }, + "nav": { + "myProfile": "My Profile", + "logOut": "Log Out", + "lightDark": "Light/Dark" + } + }, + "layout": { + "footer": { + "madeWith": "Hand-crafted & Made with ❤️" + } + }, + "entities": { + "users": "Users", + "roles": "Roles", + "permissions": "Permissions", + "schools": "Schools", + "students": "Students", + "guardians": "Guardians", + "studentGuardians": "Student guardians", + "teachers": "Teachers", + "courses": "Courses", + "grades": "Grades", + "classes": "Classes", + "subjects": "Subjects", + "enrollments": "Enrollments", + "assessments": "Assessments", + "attendance": "Attendance", + "invoices": "Invoices", + "payments": "Payments", + "employees": "Employees", + "products": "Products", + "books": "Books", + "bookLoans": "Book loans" + }, + "common": { + "actions": { + "newItem": "New Item", + "addInviteUser": "Add/Invite User", + "filter": "Filter", + "downloadCsv": "Download CSV", + "uploadCsv": "Upload CSV", + "confirm": "Confirm", + "cancel": "Cancel", + "delete": "Delete", + "apply": "Apply", + "deleting": "Deleting...", + "view": "View", + "edit": "Edit" + }, + "upload": { + "allowedFormats": "Allowed formats: {{formats}}", + "clickToUpload": "Click to upload", + "dragAndDrop": "or drag and drop" + }, + "filters": { + "value": "Value", + "selectValue": "Select Value", + "from": "From", + "to": "To", + "contains": "Contains", + "contained": "Contained", + "action": "Action" + }, + "confirm": { + "title": "Please confirm", + "deleteItem": "Are you sure you want to delete this item?" + }, + "table": { + "row": "Row", + "rows": "Rows" + } } } diff --git a/frontend/public/locales/pt/common.json b/frontend/public/locales/pt/common.json new file mode 100644 index 0000000..74bbdf5 --- /dev/null +++ b/frontend/public/locales/pt/common.json @@ -0,0 +1,186 @@ +{ + "pages": { + "dashboard": { + "pageTitle": "Painel", + "overview": "Visão geral", + "loadingWidgets": "A carregar widgets...", + "loading": "A carregar...", + "roleWidgets": "Widgets de {{role}}" + }, + "login": { + "pageTitle": "Entrar", + "sampleCredentialsSuperAdmin": "Usar para entrar como Super Admin:", + "sampleCredentialsAdmin": "Usar para entrar como Administrador:", + "sampleCredentialsUser": "Usar para entrar como Utilizador:", + "form": { + "loginLabel": "Login", + "loginHelp": "Introduza o seu login", + "passwordLabel": "Palavra-passe", + "passwordHelp": "Introduza a sua palavra-passe", + "remember": "Lembrar-me", + "forgotPassword": "Esqueceu a palavra-passe?", + "loginButton": "Entrar", + "loading": "A carregar...", + "noAccountYet": "Ainda não tem uma conta?", + "newAccount": "Nova conta" + }, + "pexels": { + "photoCredit": "Foto de {{photographer}} no Pexels", + "videoCredit": "Vídeo de {{name}} no Pexels", + "videoUnsupported": "O seu navegador não suporta a tag de vídeo." + }, + "footer": { + "copyright": "© {{year}} {{title}}. Todos os direitos reservados", + "privacy": "Política de Privacidade" + } + }, + "auth": { + "emailLabel": "Email", + "emailHelp": "Introduza o seu email", + "passwordLabel": "Palavra-passe", + "passwordHelp": "Introduza a sua palavra-passe", + "confirmPasswordLabel": "Confirmar palavra-passe", + "confirmPasswordHelp": "Confirme a sua palavra-passe", + "loading": "A carregar...", + "login": "Entrar", + "checkEmailVerification": "Verifique o seu email para aceder ao link de verificação", + "genericError": "Algo correu mal. Tente novamente" + }, + "register": { + "pageTitle": "Registar", + "organization": "Organização", + "selectOrganization": "Selecione a organização...", + "submit": "Registar" + }, + "forgot": { + "pageTitle": "Recuperar palavra-passe", + "submit": "Submeter" + }, + "verifyEmail": { + "pageTitle": "Verificar email", + "success": "O seu email foi verificado" + }, + "password": { + "setTitle": "Definir palavra-passe", + "resetTitle": "Repor palavra-passe", + "enterNewPassword": "Introduza a nova palavra-passe" + } + }, + "components": { + "widgetCreator": { + "title": "Criar gráfico ou widget", + "helpText": "Descreva o novo widget ou gráfico em linguagem natural. Por exemplo: \"Número de utilizadores administradores\" ou \"gráfico vermelho com contratos fechados agrupados por mês\"", + "settingsTitle": "Definições do criador de widgets", + "settingsDescription": "Para que função estamos a mostrar e criar widgets?", + "doneButton": "Concluído", + "loading": "A carregar...", + "creationError": "Erro ao criar widget" + }, + "search": { + "placeholder": "Pesquisar", + "required": "Obrigatório", + "minLength": "Comprimento mínimo: {{count}} caracteres" + } + }, + "navigation": { + "aside": { + "dashboard": "Painel", + "users": "Utilizadores", + "roles": "Funções", + "permissions": "Permissões", + "schoolOnboarding": "Integração de escola", + "academicMvp": "MVP Académico", + "schoolsCrud": "Escolas CRUD", + "students": "Alunos", + "guardians": "Encarregados", + "studentGuardians": "Alunos e encarregados", + "teachers": "Professores", + "courses": "Cursos", + "grades": "Notas", + "classes": "Turmas", + "subjects": "Disciplinas", + "enrollments": "Matrículas", + "assessments": "Avaliações", + "attendance": "Presenças", + "invoices": "Faturas", + "payments": "Pagamentos", + "employees": "Funcionários", + "products": "Produtos", + "books": "Livros", + "bookLoans": "Empréstimos de livros", + "profile": "Perfil", + "swaggerApi": "Swagger API" + }, + "nav": { + "myProfile": "O meu perfil", + "logOut": "Sair", + "lightDark": "Claro/Escuro" + } + }, + "layout": { + "footer": { + "madeWith": "Criado manualmente e feito com ❤️" + } + }, + "entities": { + "users": "Utilizadores", + "roles": "Funções", + "permissions": "Permissões", + "schools": "Escolas", + "students": "Alunos", + "guardians": "Encarregados", + "studentGuardians": "Alunos e encarregados", + "teachers": "Professores", + "courses": "Cursos", + "grades": "Notas", + "classes": "Turmas", + "subjects": "Disciplinas", + "enrollments": "Matrículas", + "assessments": "Avaliações", + "attendance": "Presenças", + "invoices": "Faturas", + "payments": "Pagamentos", + "employees": "Funcionários", + "products": "Produtos", + "books": "Livros", + "bookLoans": "Empréstimos de livros" + }, + "common": { + "actions": { + "newItem": "Novo item", + "addInviteUser": "Adicionar/convidar utilizador", + "filter": "Filtrar", + "downloadCsv": "Descarregar CSV", + "uploadCsv": "Carregar CSV", + "confirm": "Confirmar", + "cancel": "Cancelar", + "delete": "Eliminar", + "apply": "Aplicar", + "deleting": "A eliminar...", + "view": "Ver", + "edit": "Editar" + }, + "upload": { + "allowedFormats": "Formatos permitidos: {{formats}}", + "clickToUpload": "Clique para carregar", + "dragAndDrop": "ou arraste e largue" + }, + "filters": { + "value": "Valor", + "selectValue": "Selecionar valor", + "from": "De", + "to": "Até", + "contains": "Contém", + "contained": "Contido", + "action": "Ação" + }, + "confirm": { + "title": "Confirme, por favor", + "deleteItem": "Tem a certeza de que pretende eliminar este item?" + }, + "table": { + "row": "linha", + "rows": "linhas" + } + } +} diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx index dbb09b2..acaf16c 100644 --- a/frontend/src/components/AsideMenuItem.tsx +++ b/frontend/src/components/AsideMenuItem.tsx @@ -7,6 +7,7 @@ import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' import { useAppSelector } from '../stores/hooks' import { useRouter } from 'next/router' +import { useTranslation } from 'react-i18next' type Props = { item: MenuAsideItem @@ -16,6 +17,8 @@ type Props = { const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const [isLinkActive, setIsLinkActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false) + const [isMounted, setIsMounted] = useState(false) + const { t } = useTranslation('common') const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle) const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle) @@ -28,6 +31,10 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const { asPath, isReady } = useRouter() + useEffect(() => { + setIsMounted(true) + }, []) + useEffect(() => { if (item.href && isReady) { const linkPathName = new URL(item.href, location.href).pathname + '/'; @@ -40,6 +47,10 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { } }, [item.href, isReady, asPath]) + const translatedLabel = isMounted && item.labelKey + ? t(item.labelKey, { defaultValue: item.label }) + : item.label + const asideMenuItemInnerContents = ( <> {item.icon && ( @@ -50,7 +61,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { item.menu ? '' : 'pr-12' } ${activeClassAddon}`} > - {item.label} + {translatedLabel} {item.menu && ( { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
@@ -429,14 +436,14 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Attendance/TableAttendance.tsx b/frontend/src/components/Attendance/TableAttendance.tsx index 5a6dccf..c810c08 100644 --- a/frontend/src/components/Attendance/TableAttendance.tsx +++ b/frontend/src/components/Attendance/TableAttendance.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
@@ -429,14 +436,14 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Book_loans/TableBook_loans.tsx b/frontend/src/components/Book_loans/TableBook_loans.tsx index 8ae8e54..0abe830 100644 --- a/frontend/src/components/Book_loans/TableBook_loans.tsx +++ b/frontend/src/components/Book_loans/TableBook_loans.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -25,6 +26,16 @@ import { SlotInfo } from 'react-big-calendar'; const perPage = 100 const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -286,7 +297,7 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -335,22 +344,22 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -422,13 +429,13 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
@@ -438,14 +445,14 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -476,7 +483,7 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Books/TableBooks.tsx b/frontend/src/components/Books/TableBooks.tsx index b25722b..ba44ff8 100644 --- a/frontend/src/components/Books/TableBooks.tsx +++ b/frontend/src/components/Books/TableBooks.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) => return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
filter.title === filterItem?.fields?.selectedField )?.type === 'enum' ? (
-
- Value -
+
{translate('common.filters.value', 'Value')}
value={filterItem?.fields?.filterValue || ''} onChange={handleChange(filterItem.id)} > - + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) => )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
)?.date ? (
-
- From -
+
{translate('common.filters.from', 'From')}
/>
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
@@ -429,14 +436,14 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) => : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) => onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Classes/TableClasses.tsx b/frontend/src/components/Classes/TableClasses.tsx index 6a3404f..396ad9c 100644 --- a/frontend/src/components/Classes/TableClasses.tsx +++ b/frontend/src/components/Classes/TableClasses.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid }) return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid }) )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
@@ -429,14 +436,14 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid }) : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid }) onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Courses/TableCourses.tsx b/frontend/src/components/Courses/TableCourses.tsx index 89fbddf..ba5f751 100644 --- a/frontend/src/components/Courses/TableCourses.tsx +++ b/frontend/src/components/Courses/TableCourses.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
@@ -429,14 +436,14 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/DragDropFilePicker.tsx b/frontend/src/components/DragDropFilePicker.tsx index 821570d..1ee20f1 100644 --- a/frontend/src/components/DragDropFilePicker.tsx +++ b/frontend/src/components/DragDropFilePicker.tsx @@ -1,4 +1,5 @@ import React, { ChangeEvent, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import BaseIcon from './BaseIcon'; import { mdiFileUploadOutline } from '@mdi/js'; @@ -9,10 +10,19 @@ type Props = { }; const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string, options = {}): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback, ...options })) : fallback + ); const [highlight, setHighlight] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const fileInput = React.createRef(); + useEffect(() => { + setIsTranslationMounted(true); + }, []); + useEffect(() => { if (!file && fileInput) fileInput.current.value = ''; }, [file, fileInput]); @@ -26,7 +36,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { setFile(newFile); setErrorMessage(''); } else { - setErrorMessage(`Allowed formats: ${formats}`); + setErrorMessage(translate('common.upload.allowedFormats', `Allowed formats: ${formats}`, { formats })); } } } @@ -97,8 +107,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { ) : ( <>

- Click to upload or drag - and drop + {translate('common.upload.clickToUpload', 'Click to upload')} {translate('common.upload.dragAndDrop', 'or drag and drop')}

{formats && (

diff --git a/frontend/src/components/Employees/TableEmployees.tsx b/frontend/src/components/Employees/TableEmployees.tsx index 5f72897..3431019 100644 --- a/frontend/src/components/Employees/TableEmployees.tsx +++ b/frontend/src/components/Employees/TableEmployees.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid } return (

-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid } )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
@@ -429,14 +436,14 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid } : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid } onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Enrollments/TableEnrollments.tsx b/frontend/src/components/Enrollments/TableEnrollments.tsx index f585542..2e62519 100644 --- a/frontend/src/components/Enrollments/TableEnrollments.tsx +++ b/frontend/src/components/Enrollments/TableEnrollments.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
@@ -429,14 +436,14 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Grades/TableGrades.tsx b/frontend/src/components/Grades/TableGrades.tsx index 78eebe7..3665d9f 100644 --- a/frontend/src/components/Grades/TableGrades.tsx +++ b/frontend/src/components/Grades/TableGrades.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -24,6 +25,16 @@ import ListGrades from './ListGrades'; const perPage = 10 const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -279,7 +290,7 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) = return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -328,22 +337,22 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) = )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -415,13 +422,13 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
@@ -431,14 +438,14 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) = : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -463,7 +470,7 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) = onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Guardians/TableGuardians.tsx b/frontend/src/components/Guardians/TableGuardians.tsx index 29e0ad0..9b1caf4 100644 --- a/frontend/src/components/Guardians/TableGuardians.tsx +++ b/frontend/src/components/Guardians/TableGuardians.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles"; const perPage = 10 const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -277,7 +288,7 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid } return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -326,22 +335,22 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid } )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -413,13 +420,13 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
@@ -429,14 +436,14 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid } : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -450,7 +457,7 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid } onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/Invoices/TableInvoices.tsx b/frontend/src/components/Invoices/TableInvoices.tsx index 4bc2e9f..917fedf 100644 --- a/frontend/src/components/Invoices/TableInvoices.tsx +++ b/frontend/src/components/Invoices/TableInvoices.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import React, { useEffect, useState, useMemo } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; @@ -25,6 +26,16 @@ import axios from 'axios'; const perPage = 10 const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid }) => { + const { t } = useTranslation('common'); + const [isTranslationMounted, setIsTranslationMounted] = useState(false); + const translate = (key: string, fallback: string): string => ( + isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback + ); + + useEffect(() => { + setIsTranslationMounted(true); + }, []); + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -312,7 +323,7 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid }) return (
-
Filter
+
{translate('common.actions.filter', 'Filter')}
-
- Value -
+
{translate('common.filters.value', 'Value')}
- + {filters.find((filter) => filter.title === filterItem?.fields?.selectedField )?.options?.map((option) => ( @@ -361,22 +370,22 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid }) )?.number ? (
-
From
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
-
- From -
+
{translate('common.filters.from', 'From')}
-
To
+
{translate('common.filters.to', 'To')}
) : (
-
Contains
+
{translate('common.filters.contains', 'Contains')}
)}
-
Action
+
{translate('common.filters.action', 'Action')}
{ deleteFilter(filterItem.id) }} @@ -448,13 +455,13 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
@@ -464,14 +471,14 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid }) : null } -

Are you sure you want to delete this item?

+

{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}

@@ -498,7 +505,7 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid }) onDeleteRows(selectedRows)} />, document.getElementById('delete-rows-button'), diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index f2f373a..d74264d 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -1,13 +1,18 @@ import React, { useEffect, useState } from 'react'; import Select, { components, SingleValueProps, OptionProps } from 'react-select'; +import { useTranslation } from 'react-i18next'; type LanguageOption = { label: string; value: string }; +type Props = { + className?: string; + menuPlacement?: 'auto' | 'bottom' | 'top'; + width?: number; +}; + const LANGS: LanguageOption[] = [ + { value: 'pt', label: '🇵🇹 PT' }, { value: 'en', label: '🇬🇧 EN' }, - { value: 'fr', label: '🇫🇷 FR' }, - { value: 'es', label: '🇪🇸 ES' }, - { value: 'de', label: '🇩🇪 DE' }, ]; const Option = (props: OptionProps) => ( @@ -22,29 +27,33 @@ const SingleVal = (props: SingleValueProps) => ( ); -const LanguageSwitcher: React.FC = () => { +const LanguageSwitcher: React.FC = ({ className = '', menuPlacement = 'bottom', width = 88 }) => { + const { i18n } = useTranslation(); const [mounted, setMounted] = useState(false); const [selected, setSelected] = useState(LANGS[0]); useEffect(() => { + const currentLanguage = i18n.resolvedLanguage || i18n.language || 'pt'; + setSelected(LANGS.find((lang) => lang.value === currentLanguage) || LANGS[0]); setMounted(true); - }, []); + }, [i18n.language, i18n.resolvedLanguage]); const handleChange = (opt: LanguageOption | null) => { if (!opt) return; setSelected(opt); + i18n.changeLanguage(opt.value); }; if (!mounted) return null; return ( -
+
+ + {schools.map((school) => ( + + ))} + + + +
+ + + +
+ + + +
+ +
+ + +
+ +