Autosave: 20260623-140927

This commit is contained in:
Flatlogic Bot 2026-06-23 14:09:24 +00:00
parent 27e732ea5f
commit 8156349b96
74 changed files with 3325 additions and 1053 deletions

View File

@ -1,7 +1,5 @@
const db = require('../models'); const db = require('../models');
const FileDBApi = require('./file');
const crypto = require('crypto');
const Utils = require('../utils'); const Utils = require('../utils');
@ -25,6 +23,14 @@ module.exports = class SchoolsDBApi {
|| ||
null 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, importHash: data.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
@ -55,6 +61,14 @@ module.exports = class SchoolsDBApi {
|| ||
null 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, importHash: item.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
@ -74,8 +88,6 @@ module.exports = class SchoolsDBApi {
static async update(id, data, options) { static async update(id, data, options) {
const currentUser = (options && options.currentUser) || {id: null}; const currentUser = (options && options.currentUser) || {id: null};
const transaction = (options && options.transaction) || undefined; const transaction = (options && options.transaction) || undefined;
const globalAccess = currentUser.app_role?.globalAccess;
const schools = await db.schools.findByPk(id, {}, {transaction}); const schools = await db.schools.findByPk(id, {}, {transaction});
@ -84,6 +96,14 @@ module.exports = class SchoolsDBApi {
const updatePayload = {}; const updatePayload = {};
if (data.name !== undefined) updatePayload.name = data.name; 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; updatePayload.updatedById = currentUser.id;
@ -286,10 +306,6 @@ module.exports = class SchoolsDBApi {
offset = currentPage * limit; offset = currentPage * limit;
const orderBy = null;
const transaction = (options && options.transaction) || undefined;
let include = [ 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,
),
};
}

View File

@ -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;
}
},
};

View File

@ -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) { module.exports = function(sequelize, DataTypes) {
const schools = sequelize.define( const schools = sequelize.define(
'schools', '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: { importHash: {

View File

@ -31,9 +31,22 @@ router.use(checkCrudPermissions('schools'));
* name: * name:
* type: string * type: string
* default: name * 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 } req.query, globalAccess, { currentUser }
); );
if (filetype && filetype === 'csv') { if (filetype && filetype === 'csv') {
const fields = ['id','name', const fields = ['id','name','nif','phone','email','province','municipality','address','logoUrl','status'];
];
const opts = { fields }; const opts = { fields };
try { try {
const csv = parse(payload.rows, opts); const csv = parse(payload.rows, opts);

View File

@ -3,20 +3,50 @@ const SchoolsDBApi = require('../db/api/schools');
const processFile = require("../middlewares/upload"); const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation'); const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser'); const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream'); 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 { module.exports = class SchoolsService {
static async create(data, currentUser) { static async create(data, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await SchoolsDBApi.create( await SchoolsDBApi.create(
data, normalizeSchoolData(data),
{ {
currentUser, currentUser,
transaction, transaction,
@ -28,9 +58,9 @@ module.exports = class SchoolsService {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
} }
}; }
static async bulkImport(req, res, sendInvitationEmails = true, host) { static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
@ -81,7 +111,7 @@ module.exports = class SchoolsService {
const updatedSchools = await SchoolsDBApi.update( const updatedSchools = await SchoolsDBApi.update(
id, id,
data, normalizeSchoolData(data),
{ {
currentUser, currentUser,
transaction, transaction,
@ -95,7 +125,7 @@ module.exports = class SchoolsService {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
} }
}; }
static async deleteByIds(ids, currentUser) { static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();

View File

@ -4,34 +4,66 @@
"pageTitle": "Dashboard", "pageTitle": "Dashboard",
"overview": "Overview", "overview": "Overview",
"loadingWidgets": "Loading widgets...", "loadingWidgets": "Loading widgets...",
"loading": "Loading..." "loading": "Loading...",
"roleWidgets": "{{role}}'s widgets"
}, },
"login": { "login": {
"pageTitle": "Login", "pageTitle": "Login",
"form": { "form": {
"loginLabel": "Login", "loginLabel": "Login",
"loginHelp": "Please enter your login", "loginHelp": "Please enter your login",
"passwordLabel": "Password", "passwordLabel": "Password",
"passwordHelp": "Please enter your password", "passwordHelp": "Please enter your password",
"remember": "Remember", "remember": "Remember",
"forgotPassword": "Forgot password?", "forgotPassword": "Forgot password?",
"loginButton": "Login", "loginButton": "Login",
"loading": "Loading...", "loading": "Loading...",
"noAccountYet": "Dont have an account yet?", "noAccountYet": "Dont have an account yet?",
"newAccount": "New Account" "newAccount": "New Account"
}, },
"pexels": { "pexels": {
"photoCredit": "Photo by {{photographer}} on Pexels", "photoCredit": "Photo by {{photographer}} on Pexels",
"videoCredit": "Video by {{name}} on Pexels", "videoCredit": "Video by {{name}} on Pexels",
"videoUnsupported": "Your browser does not support the video tag." "videoUnsupported": "Your browser does not support the video tag."
}, },
"footer": { "footer": {
"copyright": "© {{year}} {{title}}. All rights reserved", "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": { "components": {
@ -41,12 +73,114 @@
"settingsTitle": "Widget Creator Settings", "settingsTitle": "Widget Creator Settings",
"settingsDescription": "What role are we showing and creating widgets for?", "settingsDescription": "What role are we showing and creating widgets for?",
"doneButton": "Done", "doneButton": "Done",
"loading": "Loading..." "loading": "Loading...",
"creationError": "Error with widget creation"
}, },
"search": { "search": {
"placeholder": "Search", "placeholder": "Search",
"required": "Required", "required": "Required",
"minLength": "Minimum length: {{count}} characters" "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"
}
} }
} }

View File

@ -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"
}
}
}

View File

@ -7,6 +7,7 @@ import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces' import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks' import { useAppSelector } from '../stores/hooks'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'react-i18next'
type Props = { type Props = {
item: MenuAsideItem item: MenuAsideItem
@ -16,6 +17,8 @@ type Props = {
const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const [isLinkActive, setIsLinkActive] = useState(false) const [isLinkActive, setIsLinkActive] = useState(false)
const [isDropdownActive, setIsDropdownActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false)
const [isMounted, setIsMounted] = useState(false)
const { t } = useTranslation('common')
const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle) const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle)
const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle) const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle)
@ -28,6 +31,10 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const { asPath, isReady } = useRouter() const { asPath, isReady } = useRouter()
useEffect(() => {
setIsMounted(true)
}, [])
useEffect(() => { useEffect(() => {
if (item.href && isReady) { if (item.href && isReady) {
const linkPathName = new URL(item.href, location.href).pathname + '/'; const linkPathName = new URL(item.href, location.href).pathname + '/';
@ -40,6 +47,10 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
} }
}, [item.href, isReady, asPath]) }, [item.href, isReady, asPath])
const translatedLabel = isMounted && item.labelKey
? t(item.labelKey, { defaultValue: item.label })
: item.label
const asideMenuItemInnerContents = ( const asideMenuItemInnerContents = (
<> <>
{item.icon && ( {item.icon && (
@ -50,7 +61,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
item.menu ? '' : 'pr-12' item.menu ? '' : 'pr-12'
} ${activeClassAddon}`} } ${activeClassAddon}`}
> >
{item.label} {translatedLabel}
</span> </span>
{item.menu && ( {item.menu && (
<BaseIcon <BaseIcon

View File

@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon' import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList' import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces' import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks' import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Link from 'next/link'; import Link from 'next/link';
import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios'; import axios from 'axios';

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleAssessments = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleAssessments = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleAttendance = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -25,6 +26,16 @@ import { SlotInfo } from 'react-big-calendar';
const perPage = 100 const perPage = 100
const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -286,7 +297,7 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -309,9 +320,7 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -320,7 +329,7 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -335,22 +344,22 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -364,13 +373,11 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -378,11 +385,11 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -392,11 +399,11 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -404,12 +411,12 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -422,13 +429,13 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -438,14 +445,14 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -476,7 +483,7 @@ const TableSampleBook_loans = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleBooks = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleClasses = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,4 +1,5 @@
import React, { ChangeEvent, useEffect, useState } from 'react'; import React, { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import BaseIcon from './BaseIcon'; import BaseIcon from './BaseIcon';
import { mdiFileUploadOutline } from '@mdi/js'; import { mdiFileUploadOutline } from '@mdi/js';
@ -9,10 +10,19 @@ type Props = {
}; };
const DragDropFilePicker = ({ file, setFile, formats = '' }: 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 [highlight, setHighlight] = useState(false);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const fileInput = React.createRef<HTMLInputElement>(); const fileInput = React.createRef<HTMLInputElement>();
useEffect(() => {
setIsTranslationMounted(true);
}, []);
useEffect(() => { useEffect(() => {
if (!file && fileInput) fileInput.current.value = ''; if (!file && fileInput) fileInput.current.value = '';
}, [file, fileInput]); }, [file, fileInput]);
@ -26,7 +36,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
setFile(newFile); setFile(newFile);
setErrorMessage(''); setErrorMessage('');
} else { } else {
setErrorMessage(`Allowed formats: ${formats}`); setErrorMessage(translate('common.upload.allowedFormats', `Allowed formats: ${formats}`, { formats }));
} }
} }
} }
@ -97,8 +107,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
) : ( ) : (
<> <>
<p className='mb-2 text-sm text-gray-500 dark:text-gray-400'> <p className='mb-2 text-sm text-gray-500 dark:text-gray-400'>
<span className='font-semibold'>Click to upload</span> or drag <span className='font-semibold'>{translate('common.upload.clickToUpload', 'Click to upload')}</span> {translate('common.upload.dragAndDrop', 'or drag and drop')}
and drop
</p> </p>
{formats && ( {formats && (
<p className='text-xs text-gray-500 dark:text-gray-400'> <p className='text-xs text-gray-500 dark:text-gray-400'>

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleEnrollments = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -24,6 +25,16 @@ import ListGrades from './ListGrades';
const perPage = 10 const perPage = 10
const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -279,7 +290,7 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -302,9 +313,7 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -313,7 +322,7 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -328,22 +337,22 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -357,13 +366,11 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -371,11 +378,11 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -385,11 +392,11 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -397,12 +404,12 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -415,13 +422,13 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -431,14 +438,14 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -463,7 +470,7 @@ const TableSampleGrades = ({ filterItems, setFilterItems, filters, showGrid }) =
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleGuardians = ({ filterItems, setFilterItems, filters, showGrid }
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -25,6 +26,16 @@ import axios from 'axios';
const perPage = 10 const perPage = 10
const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid }) => { 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -312,7 +323,7 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -335,9 +346,7 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -346,7 +355,7 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -361,22 +370,22 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -390,13 +399,11 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -404,11 +411,11 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -418,11 +425,11 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -430,12 +437,12 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -448,13 +455,13 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -464,14 +471,14 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -498,7 +505,7 @@ const TableSampleInvoices = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,13 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Select, { components, SingleValueProps, OptionProps } from 'react-select'; import Select, { components, SingleValueProps, OptionProps } from 'react-select';
import { useTranslation } from 'react-i18next';
type LanguageOption = { label: string; value: string }; type LanguageOption = { label: string; value: string };
type Props = {
className?: string;
menuPlacement?: 'auto' | 'bottom' | 'top';
width?: number;
};
const LANGS: LanguageOption[] = [ const LANGS: LanguageOption[] = [
{ value: 'pt', label: '🇵🇹 PT' },
{ value: 'en', label: '🇬🇧 EN' }, { value: 'en', label: '🇬🇧 EN' },
{ value: 'fr', label: '🇫🇷 FR' },
{ value: 'es', label: '🇪🇸 ES' },
{ value: 'de', label: '🇩🇪 DE' },
]; ];
const Option = (props: OptionProps<LanguageOption, false>) => ( const Option = (props: OptionProps<LanguageOption, false>) => (
@ -22,29 +27,33 @@ const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
</components.SingleValue> </components.SingleValue>
); );
const LanguageSwitcher: React.FC = () => { const LanguageSwitcher: React.FC<Props> = ({ className = '', menuPlacement = 'bottom', width = 88 }) => {
const { i18n } = useTranslation();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [selected, setSelected] = useState<LanguageOption>(LANGS[0]); const [selected, setSelected] = useState<LanguageOption>(LANGS[0]);
useEffect(() => { useEffect(() => {
const currentLanguage = i18n.resolvedLanguage || i18n.language || 'pt';
setSelected(LANGS.find((lang) => lang.value === currentLanguage) || LANGS[0]);
setMounted(true); setMounted(true);
}, []); }, [i18n.language, i18n.resolvedLanguage]);
const handleChange = (opt: LanguageOption | null) => { const handleChange = (opt: LanguageOption | null) => {
if (!opt) return; if (!opt) return;
setSelected(opt); setSelected(opt);
i18n.changeLanguage(opt.value);
}; };
if (!mounted) return null; if (!mounted) return null;
return ( return (
<div style={{ width: 88 }}> <div className={className} style={{ width }}>
<Select <Select
value={selected} value={selected}
options={LANGS} options={LANGS}
onChange={handleChange} onChange={handleChange}
isSearchable={false} isSearchable={false}
menuPlacement='top' menuPlacement={menuPlacement}
components={{ components={{
Option, Option,
SingleValue: SingleVal, SingleValue: SingleVal,

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import Link from 'next/link'; import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import BaseIcon from './BaseIcon'; import BaseIcon from './BaseIcon';
import { import {
@ -31,6 +31,16 @@ const ListActionsPopover = ({
pathEdit, pathEdit,
pathView, pathView,
}: Props) => { }: Props) => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => { const handleClick = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@ -81,7 +91,7 @@ const ListActionsPopover = ({
href={linkView} href={linkView}
sx={{ justifyContent: "start" }} sx={{ justifyContent: "start" }}
> >
View {translate('common.actions.view', 'View')}
</Button> </Button>
{hasUpdatePermission && ( {hasUpdatePermission && (
<Button <Button
@ -90,7 +100,7 @@ const ListActionsPopover = ({
href={linkEdit} href={linkEdit}
sx={{ justifyContent: "start" }} sx={{ justifyContent: "start" }}
> >
Edit {translate('common.actions.edit', 'Edit')}
</Button> </Button>
)} )}
{hasUpdatePermission && ( {hasUpdatePermission && (
@ -103,7 +113,7 @@ const ListActionsPopover = ({
}} }}
sx={{ justifyContent: "start" }} sx={{ justifyContent: "start" }}
> >
Delete {translate('common.actions.delete', 'Delete')}
</Button> </Button>
)} )}
</div> </div>

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react' import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider' import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon' import BaseIcon from './BaseIcon'
@ -12,6 +11,7 @@ import { setDarkMode } from '../stores/styleSlice'
import { logoutUser } from '../stores/authSlice' import { logoutUser } from '../stores/authSlice'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside"; import ClickOutside from "./ClickOutside";
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
item: MenuNavBarItem item: MenuNavBarItem
@ -33,6 +33,12 @@ export default function NavBarItem({ item }: Props) {
const userName = `${currentUser?.firstName ? currentUser?.firstName : ""} ${currentUser?.lastName ? currentUser?.lastName : ""}`; const userName = `${currentUser?.firstName ? currentUser?.firstName : ""} ${currentUser?.lastName ? currentUser?.lastName : ""}`;
const [isDropdownActive, setIsDropdownActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false)
const [isMounted, setIsMounted] = useState(false)
const { t } = useTranslation('common')
useEffect(() => {
setIsMounted(true)
}, [])
useEffect(() => { useEffect(() => {
return () => setIsDropdownActive(false); return () => setIsDropdownActive(false);
@ -47,7 +53,11 @@ export default function NavBarItem({ item }: Props) {
item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '', item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '',
].join(' ') ].join(' ')
const itemLabel = item.isCurrentUser ? userName : item.label const fallbackLabel = item.label ?? ''
const translatedLabel = isMounted && item.labelKey
? t(item.labelKey, { defaultValue: fallbackLabel })
: fallbackLabel
const itemLabel = item.isCurrentUser ? userName : translatedLabel
const handleMenuClick = () => { const handleMenuClick = () => {
if (item.menu) { if (item.menu) {
@ -64,21 +74,16 @@ export default function NavBarItem({ item }: Props) {
} }
} }
const getItemId = (label) => { const getItemId = () => {
switch (label) { if (item.isToggleLightDark) return 'themeToggle';
case 'Light/Dark': if (item.isLogout) return 'logout';
return 'themeToggle'; return undefined;
case 'Log out':
return 'logout';
default:
return undefined;
}
}; };
const NavBarItemComponentContents = ( const NavBarItemComponentContents = (
<> <>
<div <div
id={getItemId(itemLabel)} id={getItemId()}
className={`flex items-center ${ className={`flex items-center ${
item.menu item.menu
? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0' ? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0'

View File

@ -13,8 +13,14 @@ import BaseButtons from '../components/BaseButtons';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
import { passwordReset } from '../stores/authSlice'; import { passwordReset } from '../stores/authSlice';
import {useAppDispatch} from '../stores/hooks'; import {useAppDispatch} from '../stores/hooks';
import { useTranslation } from 'react-i18next';
export default function PasswordSetOrReset() { export default function PasswordSetOrReset() {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [isInvitation, setIsInvitation] = React.useState(false); const [isInvitation, setIsInvitation] = React.useState(false);
const router = useRouter(); const router = useRouter();
@ -24,6 +30,10 @@ export default function PasswordSetOrReset() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (invitation) { if (invitation) {
setIsInvitation(true); setIsInvitation(true);
@ -49,16 +59,16 @@ export default function PasswordSetOrReset() {
return ( return (
<> <>
<Head> <Head>
{isInvitation && <title>{getPageTitle('Set Password')}</title>} {isInvitation && <title>{getPageTitle(translate('pages.password.setTitle', 'Set Password'))}</title>}
{!isInvitation && <title>{getPageTitle('Reset Password')}</title>} {!isInvitation && <title>{getPageTitle(translate('pages.password.resetTitle', 'Reset Password'))}</title>}
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
<div className='w-full flex flex-col items-center justify-center'> <div className='w-full flex flex-col items-center justify-center'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'> <CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
{isInvitation && <p className='text-xl mb-2'>Set Password</p>} {isInvitation && <p className='text-xl mb-2'>{translate('pages.password.setTitle', 'Set Password')}</p>}
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>} {!isInvitation && <p className='text-xl mb-2'>{translate('pages.password.resetTitle', 'Reset Password')}</p>}
<p className='text-base mb-4'>Enter your new password</p> <p className='text-base mb-4'>{translate('pages.password.enterNewPassword', 'Enter your new password')}</p>
<Formik <Formik
initialValues={{ initialValues={{
@ -74,7 +84,7 @@ export default function PasswordSetOrReset() {
<Field <Field
type='password' type='password'
name='password' name='password'
placeholder='Password' placeholder={translate('pages.auth.passwordLabel', 'Password')}
/> />
</FormField> </FormField>
<FormField <FormField
@ -82,7 +92,7 @@ export default function PasswordSetOrReset() {
<Field <Field
type='password' type='password'
name='confirm' name='confirm'
placeholder='Confirm Password' placeholder={translate('pages.auth.confirmPasswordLabel', 'Confirm Password')}
/> />
</FormField> </FormField>
@ -93,10 +103,10 @@ export default function PasswordSetOrReset() {
disabled={loading} disabled={loading}
label={ label={
loading loading
? 'Loading...' ? translate('pages.auth.loading', 'Loading...')
: isInvitation : isInvitation
? 'Set Password' ? translate('pages.password.setTitle', 'Set Password')
: 'Reset Password' : translate('pages.password.resetTitle', 'Reset Password')
} }
color='info' color='info'
/> />

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSamplePayments = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSamplePermissions = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleProducts = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleRoles = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleSchools = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleSchools = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -43,9 +43,9 @@ export const loadColumns = async (
{ {
field: 'name', field: 'name',
headerName: 'Name', headerName: 'Escola',
flex: 1, flex: 1,
minWidth: 120, minWidth: 180,
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
@ -55,6 +55,46 @@ export const loadColumns = async (
}, },
{
field: 'nif',
headerName: 'NIF',
flex: 0.7,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
},
{
field: 'province',
headerName: 'Província',
flex: 0.8,
minWidth: 140,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
},
{
field: 'municipality',
headerName: 'Município',
flex: 0.8,
minWidth: 140,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
},
{
field: 'status',
headerName: 'Estado',
flex: 0.7,
minWidth: 130,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
},
{ {
field: 'actions', field: 'actions',

View File

@ -1,19 +1,26 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useAppSelector } from '../stores/hooks'; import { useAppSelector } from '../stores/hooks';
import { useTranslation } from 'react-i18next';
const Search = () => { const Search = () => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('common');
const [isMounted, setIsMounted] = useState(false);
const focusRing = useAppSelector((state) => state.style.focusRingColor); const focusRing = useAppSelector((state) => state.style.focusRingColor);
const corners = useAppSelector((state) => state.style.corners); const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
useEffect(() => {
setIsMounted(true);
}, []);
const validateSearch = (value) => { const validateSearch = (value) => {
let error; let error;
if (!value) { if (!value) {
error = 'Required'; error = isMounted ? t('components.search.required') : 'Required';
} else if (value.length < 2) { } else if (value.length < 2) {
error = 'Minimum length: 2 characters'; error = isMounted ? t('components.search.minLength', { count: 2 }) : 'Minimum length: 2 characters';
} }
return error; return error;
}; };
@ -36,7 +43,7 @@ const Search = () => {
id='search' id='search'
name='search' name='search'
validate={validateSearch} validate={validateSearch}
placeholder='Search' placeholder={isMounted ? t('components.search.placeholder') : 'Search'}
className={` ${corners} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-2 relative ml-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`} className={` ${corners} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-2 relative ml-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`}
/> />
{errors.search && touched.search && values.search.length < 2 ? ( {errors.search && touched.search && values.search.length < 2 ? (

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleStudent_guardians = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleStudents = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleStudents = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -24,6 +25,16 @@ import ListSubjects from './ListSubjects';
const perPage = 10 const perPage = 10
const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleSubjects = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -279,7 +290,7 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -302,9 +313,7 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -313,7 +322,7 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -328,22 +337,22 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -357,13 +366,11 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -371,11 +378,11 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -385,11 +392,11 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -397,12 +404,12 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -415,13 +422,13 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -431,14 +438,14 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -463,7 +470,7 @@ const TableSampleSubjects = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleTeachers = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleTeachers = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useState, useMemo } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10 const perPage = 10
const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) => { const TableSampleUsers = ({ 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
return ( return (
<div key={filterItem.id} className="flex mb-4"> <div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div> <div className=" text-gray-500 font-bold">{translate('common.actions.filter', 'Filter')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='selectedField' name='selectedField'
@ -300,9 +311,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? ( )?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold"> <div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
Value
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name="filterValue" name="filterValue"
@ -311,7 +320,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
> >
<option value="">Select Value</option> <option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) => {filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => ( )?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? ( )?.number ? (
<div className="flex flex-row w-full mr-3"> <div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div> <div className=" text-gray-500 font-bold">{translate('common.filters.from', 'From')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
/> />
</div> </div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div> <div className=" text-gray-500 font-bold">{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.date ? ( )?.date ? (
<div className='flex flex-row w-full mr-3'> <div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'> <div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'> <div className=' text-gray-500 font-bold'>{translate('common.filters.from', 'From')}</div>
From
</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueFrom' name='filterValueFrom'
placeholder='From' placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom' id='filterValueFrom'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''} value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
/> />
</div> </div>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div> <div className=' text-gray-500 font-bold'>{translate('common.filters.to', 'To')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValueTo' name='filterValueTo'
placeholder='to' placeholder={translate('common.filters.to', 'to')}
id='filterValueTo' id='filterValueTo'
type='datetime-local' type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''} value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div> </div>
) : ( ) : (
<div className="flex flex-col w-full mr-3"> <div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div> <div className=" text-gray-500 font-bold">{translate('common.filters.contains', 'Contains')}</div>
<Field <Field
className={controlClasses} className={controlClasses}
name='filterValue' name='filterValue'
placeholder='Contained' placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue' id='filterValue'
value={filterItem?.fields?.filterValue || ''} value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)} onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div> <div className=" text-gray-500 font-bold">{translate('common.filters.action', 'Action')}</div>
<BaseButton <BaseButton
className="my-2" className="my-2"
type='reset' type='reset'
color='danger' color='danger'
label='Delete' label={translate('common.actions.delete', 'Delete')}
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
@ -413,13 +420,13 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton <BaseButton
className="my-2 mr-3" className="my-2 mr-3"
color="success" color="success"
label='Apply' label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" className="my-2"
color='info' color='info'
label='Cancel' label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
@ -429,14 +436,14 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null </CardBox> : null
} }
<CardBoxModal <CardBoxModal
title="Please confirm" title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info" buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'} buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive} isActive={isModalTrashActive}
onConfirm={handleDeleteAction} onConfirm={handleDeleteAction}
onCancel={handleModalAction} onCancel={handleModalAction}
> >
<p>Are you sure you want to delete this item?</p> <p>{translate('common.confirm.deleteItem', 'Are you sure you want to delete this item?')}</p>
</CardBoxModal> </CardBoxModal>
@ -450,7 +457,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton <BaseButton
className='me-4' className='me-4'
color='danger' color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`} label={`${translate('common.actions.delete', 'Delete')} ${selectedRows.length === 1 ? translate('common.table.row', 'Row') : translate('common.table.rows', 'Rows')}`}
onClick={() => onDeleteRows(selectedRows)} onClick={() => onDeleteRows(selectedRows)}
/>, />,
document.getElementById('delete-rows-button'), document.getElementById('delete-rows-button'),

View File

@ -4,6 +4,7 @@ import { Field, Form, Formik } from 'formik';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
import FormField from '../FormField'; import FormField from '../FormField';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { import {
aiPrompt, aiPrompt,
setErrorNotification, setErrorNotification,
@ -24,10 +25,19 @@ export const WidgetCreator = ({
widgetsRole, widgetsRole,
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const [isModalOpen, setIsModalOpen] = React.useState(false); const [isModalOpen, setIsModalOpen] = React.useState(false);
const { notify: openAiNotify } = useAppSelector((state) => state.openAi); const { notify: openAiNotify } = useAppSelector((state) => state.openAi);
const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' });
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (openAiNotify.showNotification) { if (openAiNotify.showNotification) {
notify(openAiNotify.typeNotification, openAiNotify.textNotification); notify(openAiNotify.typeNotification, openAiNotify.textNotification);
@ -67,7 +77,7 @@ export const WidgetCreator = ({
const errorMessage = const errorMessage =
responcePayload.data?.error?.message || error?.message; responcePayload.data?.error?.message || error?.message;
await dispatch( await dispatch(
setErrorNotification(errorMessage || 'Error with widget creation'), setErrorNotification(errorMessage || translate('components.widgetCreator.creationError', 'Error with widget creation')),
); );
} }
}; };
@ -90,11 +100,11 @@ export const WidgetCreator = ({
> >
<Form> <Form>
<FormField <FormField
label='Create Chart or Widget' label={translate('components.widgetCreator.title', 'Create Chart or Widget')}
help={ help={
isFetchingQuery ? isFetchingQuery ?
'Loading...' : translate('components.widgetCreator.loading', 'Loading...') :
'Describe your new widget or chart in natural language. For example: "Number of admin users" OR "red chart with number of closed contracts grouped by month"' translate('components.widgetCreator.helpText', 'Describe your new widget or chart in natural language. For example: "Number of admin users" OR "red chart with number of closed contracts grouped by month"')
} }
> >
<Field type='input' name='description' disabled={isFetchingQuery} /> <Field type='input' name='description' disabled={isFetchingQuery} />
@ -110,14 +120,14 @@ export const WidgetCreator = ({
> >
{({ submitForm }) => ( {({ submitForm }) => (
<CardBoxModal <CardBoxModal
title='Widget Creator Settings' title={translate('components.widgetCreator.settingsTitle', 'Widget Creator Settings')}
buttonColor='info' buttonColor='info'
buttonLabel='Done' buttonLabel={translate('components.widgetCreator.doneButton', 'Done')}
isActive={isModalOpen} isActive={isModalOpen}
onConfirm={submitForm} onConfirm={submitForm}
onCancel={() => setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)}
> >
<p>What role are we showing and creating widgets for?</p> <p>{translate('components.widgetCreator.settingsDescription', 'What role are we showing and creating widgets for?')}</p>
<Form> <Form>
<FormField> <FormField>

View File

@ -8,9 +8,13 @@ i18n
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: 'en', fallbackLng: 'pt',
supportedLngs: ['pt', 'en'],
load: 'languageOnly',
defaultNS: 'common',
ns: ['common'],
detection: { detection: {
order: ['localStorage', 'navigator'], order: ['localStorage'],
lookupLocalStorage: 'app_lang_', lookupLocalStorage: 'app_lang_',
caches: ['localStorage'], caches: ['localStorage'],
}, },

View File

@ -6,6 +6,7 @@ export type UserPayloadObject = {
export type MenuAsideItem = { export type MenuAsideItem = {
label: string label: string
labelKey?: string
icon?: string icon?: string
href?: string href?: string
target?: string target?: string
@ -18,6 +19,7 @@ export type MenuAsideItem = {
export type MenuNavBarItem = { export type MenuNavBarItem = {
label?: string label?: string
labelKey?: string
icon?: string icon?: string
href?: string href?: string
target?: string target?: string

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react' import React, { ReactNode, useEffect, useState } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside' import menuAside from '../menuAside'
@ -13,6 +12,8 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Search from '../components/Search'; import Search from '../components/Search';
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice"; import {findMe, logoutUser} from "../stores/authSlice";
import LanguageSwitcher from '../components/LanguageSwitcher';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../helpers/userPermissions"; import {hasPermission} from "../helpers/userPermissions";
@ -32,6 +33,7 @@ export default function LayoutAuthenticated({
}: Props) { }: Props) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common')
const { token, currentUser } = useAppSelector((state) => state.auth) const { token, currentUser } = useAppSelector((state) => state.auth)
const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
let localToken let localToken
@ -69,6 +71,11 @@ export default function LayoutAuthenticated({
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false) const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
const [isAsideLgActive, setIsAsideLgActive] = useState(false) const [isAsideLgActive, setIsAsideLgActive] = useState(false)
const [isTranslationMounted, setIsTranslationMounted] = useState(false)
useEffect(() => {
setIsTranslationMounted(true)
}, [])
useEffect(() => { useEffect(() => {
const handleRouteChangeStart = () => { const handleRouteChangeStart = () => {
@ -114,6 +121,9 @@ export default function LayoutAuthenticated({
<NavBarItemPlain useMargin> <NavBarItemPlain useMargin>
<Search /> <Search />
</NavBarItemPlain> </NavBarItemPlain>
<NavBarItemPlain display="hidden md:flex" useMargin>
<LanguageSwitcher menuPlacement="bottom" />
</NavBarItemPlain>
</NavBar> </NavBar>
<AsideMenu <AsideMenu
isAsideMobileExpanded={isAsideMobileExpanded} isAsideMobileExpanded={isAsideMobileExpanded}
@ -122,7 +132,7 @@ export default function LayoutAuthenticated({
onAsideLgClose={() => setIsAsideLgActive(false)} onAsideLgClose={() => setIsAsideLgActive(false)}
/> />
{children} {children}
<FooterBar>Hand-crafted & Made with </FooterBar> <FooterBar>{isTranslationMounted ? t('layout.footer.madeWith') : 'Hand-crafted & Made with ❤️'}</FooterBar>
</div> </div>
</div> </div>
) )

View File

@ -1,5 +1,6 @@
import React, { ReactNode } from 'react' import React, { ReactNode } from 'react'
import { useAppSelector } from '../stores/hooks' import { useAppSelector } from '../stores/hooks'
import LanguageSwitcher from '../components/LanguageSwitcher'
type Props = { type Props = {
children: ReactNode children: ReactNode
@ -11,7 +12,12 @@ export default function LayoutGuest({ children }: Props) {
return ( return (
<div className={darkMode ? 'dark' : ''}> <div className={darkMode ? 'dark' : ''}>
<div className={`${bgColor} dark:bg-slate-800 dark:text-slate-100`}>{children}</div> <div className={`${bgColor} dark:bg-slate-800 dark:text-slate-100`}>
<div className="fixed right-4 top-4 z-50">
<LanguageSwitcher menuPlacement="bottom" />
</div>
{children}
</div>
</div> </div>
) )
} }

View File

@ -6,11 +6,13 @@ const menuAside: MenuAsideItem[] = [
href: '/dashboard', href: '/dashboard',
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Dashboard', label: 'Dashboard',
labelKey: 'navigation.aside.dashboard',
}, },
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'Users', label: 'Users',
labelKey: 'navigation.aside.users',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable, icon: icon.mdiAccountGroup ?? icon.mdiTable,
@ -19,6 +21,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/roles/roles-list', href: '/roles/roles-list',
label: 'Roles', label: 'Roles',
labelKey: 'navigation.aside.roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
@ -27,14 +30,32 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/permissions/permissions-list', href: '/permissions/permissions-list',
label: 'Permissions', label: 'Permissions',
labelKey: 'navigation.aside.permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS' permissions: 'READ_PERMISSIONS'
}, },
{
href: '/schools/onboarding',
label: 'School Onboarding',
labelKey: 'navigation.aside.schoolOnboarding',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSchoolOutline' in icon ? icon['mdiSchoolOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_SCHOOLS'
},
{
href: '/academic/mvp',
label: 'MVP Académico',
labelKey: 'navigation.aside.academicMvp',
icon: icon.mdiBookEducationOutline,
permissions: 'READ_STUDENTS'
},
{ {
href: '/schools/schools-list', href: '/schools/schools-list',
label: 'Schools', label: 'Schools CRUD',
labelKey: 'navigation.aside.schoolsCrud',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiTable ?? icon.mdiTable, icon: icon.mdiTable ?? icon.mdiTable,
@ -43,6 +64,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/students/students-list', href: '/students/students-list',
label: 'Students', label: 'Students',
labelKey: 'navigation.aside.students',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiAccountSchool' in icon ? icon['mdiAccountSchool' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiAccountSchool' in icon ? icon['mdiAccountSchool' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -51,6 +73,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/guardians/guardians-list', href: '/guardians/guardians-list',
label: 'Guardians', label: 'Guardians',
labelKey: 'navigation.aside.guardians',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -59,6 +82,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/student_guardians/student_guardians-list', href: '/student_guardians/student_guardians-list',
label: 'Student guardians', label: 'Student guardians',
labelKey: 'navigation.aside.studentGuardians',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -67,6 +91,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/teachers/teachers-list', href: '/teachers/teachers-list',
label: 'Teachers', label: 'Teachers',
labelKey: 'navigation.aside.teachers',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiAccountTie' in icon ? icon['mdiAccountTie' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiAccountTie' in icon ? icon['mdiAccountTie' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -75,6 +100,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/courses/courses-list', href: '/courses/courses-list',
label: 'Courses', label: 'Courses',
labelKey: 'navigation.aside.courses',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -83,6 +109,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/grades/grades-list', href: '/grades/grades-list',
label: 'Grades', label: 'Grades',
labelKey: 'navigation.aside.grades',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiNumeric' in icon ? icon['mdiNumeric' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiNumeric' in icon ? icon['mdiNumeric' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -91,6 +118,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/classes/classes-list', href: '/classes/classes-list',
label: 'Classes', label: 'Classes',
labelKey: 'navigation.aside.classes',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiGoogleClassroom' in icon ? icon['mdiGoogleClassroom' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiGoogleClassroom' in icon ? icon['mdiGoogleClassroom' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -99,6 +127,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/subjects/subjects-list', href: '/subjects/subjects-list',
label: 'Subjects', label: 'Subjects',
labelKey: 'navigation.aside.subjects',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiBookmarkMultiple' in icon ? icon['mdiBookmarkMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiBookmarkMultiple' in icon ? icon['mdiBookmarkMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -107,6 +136,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/enrollments/enrollments-list', href: '/enrollments/enrollments-list',
label: 'Enrollments', label: 'Enrollments',
labelKey: 'navigation.aside.enrollments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiClipboardText' in icon ? icon['mdiClipboardText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiClipboardText' in icon ? icon['mdiClipboardText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -115,6 +145,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/assessments/assessments-list', href: '/assessments/assessments-list',
label: 'Assessments', label: 'Assessments',
labelKey: 'navigation.aside.assessments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiNotebookEdit' in icon ? icon['mdiNotebookEdit' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiNotebookEdit' in icon ? icon['mdiNotebookEdit' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -123,6 +154,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/attendance/attendance-list', href: '/attendance/attendance-list',
label: 'Attendance', label: 'Attendance',
labelKey: 'navigation.aside.attendance',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiCalendarCheck' in icon ? icon['mdiCalendarCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiCalendarCheck' in icon ? icon['mdiCalendarCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -131,6 +163,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/invoices/invoices-list', href: '/invoices/invoices-list',
label: 'Invoices', label: 'Invoices',
labelKey: 'navigation.aside.invoices',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiFileDocument' in icon ? icon['mdiFileDocument' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiFileDocument' in icon ? icon['mdiFileDocument' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -139,6 +172,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/payments/payments-list', href: '/payments/payments-list',
label: 'Payments', label: 'Payments',
labelKey: 'navigation.aside.payments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiCashCheck' in icon ? icon['mdiCashCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiCashCheck' in icon ? icon['mdiCashCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -147,6 +181,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/employees/employees-list', href: '/employees/employees-list',
label: 'Employees', label: 'Employees',
labelKey: 'navigation.aside.employees',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiBadgeAccount' in icon ? icon['mdiBadgeAccount' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiBadgeAccount' in icon ? icon['mdiBadgeAccount' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -155,6 +190,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/products/products-list', href: '/products/products-list',
label: 'Products', label: 'Products',
labelKey: 'navigation.aside.products',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiPackageVariant' in icon ? icon['mdiPackageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiPackageVariant' in icon ? icon['mdiPackageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -163,6 +199,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/books/books-list', href: '/books/books-list',
label: 'Books', label: 'Books',
labelKey: 'navigation.aside.books',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiLibrary' in icon ? icon['mdiLibrary' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiLibrary' in icon ? icon['mdiLibrary' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -171,6 +208,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/book_loans/book_loans-list', href: '/book_loans/book_loans-list',
label: 'Book loans', label: 'Book loans',
labelKey: 'navigation.aside.bookLoans',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: 'mdiBookClock' in icon ? icon['mdiBookClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiBookClock' in icon ? icon['mdiBookClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -179,6 +217,7 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/profile', href: '/profile',
label: 'Profile', label: 'Profile',
labelKey: 'navigation.aside.profile',
icon: icon.mdiAccountCircle, icon: icon.mdiAccountCircle,
}, },
@ -187,6 +226,7 @@ const menuAside: MenuAsideItem[] = [
href: '/api-docs', href: '/api-docs',
target: '_blank', target: '_blank',
label: 'Swagger API', label: 'Swagger API',
labelKey: 'navigation.aside.swaggerApi',
icon: icon.mdiFileCode, icon: icon.mdiFileCode,
permissions: 'READ_API_DOCS' permissions: 'READ_API_DOCS'
}, },

View File

@ -1,15 +1,7 @@
import { import {
mdiMenu,
mdiClockOutline,
mdiCloud,
mdiCrop,
mdiAccount, mdiAccount,
mdiCogOutline,
mdiEmail,
mdiLogout, mdiLogout,
mdiThemeLightDark, mdiThemeLightDark,
mdiGithub,
mdiVuejs,
} from '@mdi/js' } from '@mdi/js'
import { MenuNavBarItem } from './interfaces' import { MenuNavBarItem } from './interfaces'
@ -20,6 +12,7 @@ const menuNavBar: MenuNavBarItem[] = [
{ {
icon: mdiAccount, icon: mdiAccount,
label: 'My Profile', label: 'My Profile',
labelKey: 'navigation.nav.myProfile',
href: '/profile', href: '/profile',
}, },
{ {
@ -28,6 +21,7 @@ const menuNavBar: MenuNavBarItem[] = [
{ {
icon: mdiLogout, icon: mdiLogout,
label: 'Log Out', label: 'Log Out',
labelKey: 'navigation.nav.logOut',
isLogout: true, isLogout: true,
}, },
], ],
@ -35,12 +29,14 @@ const menuNavBar: MenuNavBarItem[] = [
{ {
icon: mdiThemeLightDark, icon: mdiThemeLightDark,
label: 'Light/Dark', label: 'Light/Dark',
labelKey: 'navigation.nav.lightDark',
isDesktopNoLabel: true, isDesktopNoLabel: true,
isToggleLightDark: true, isToggleLightDark: true,
}, },
{ {
icon: mdiLogout, icon: mdiLogout,
label: 'Log out', label: 'Log out',
labelKey: 'navigation.nav.logOut',
isDesktopNoLabel: true, isDesktopNoLabel: true,
isLogout: true, isLogout: true,
}, },

View File

@ -0,0 +1,729 @@
import {
mdiAccountSchool,
mdiBookEducationOutline,
mdiClipboardCheckOutline,
mdiGoogleClassroom,
mdiRefresh,
mdiSchoolOutline,
} from '@mdi/js'
import axios from 'axios'
import Head from 'next/head'
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
import BaseButton from '../../components/BaseButton'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import { hasPermission } from '../../helpers/userPermissions'
import { useAppSelector } from '../../stores/hooks'
type OptionRecord = {
id: string
label?: string
name?: string
nome?: string
nome_completo?: string
}
type StudentRecord = OptionRecord & {
numero_processo?: string | null
sexo?: string | null
telefone?: string | null
email?: string | null
school?: OptionRecord | null
status?: string | null
}
type ClassRecord = OptionRecord & {
turno?: string | null
sala?: string | null
capacidade?: number | null
school?: OptionRecord | null
status?: string | null
}
type EnrollmentRecord = OptionRecord & {
ano_lectivo?: string | null
data_matricula?: string | null
status?: string | null
school?: OptionRecord | null
student?: StudentRecord | null
class?: ClassRecord | null
}
type SchoolRecord = OptionRecord & {
status?: string | null
province?: string | null
municipality?: string | null
}
type StudentForm = {
school: string
numero_processo: string
nome_completo: string
sexo: 'masculino' | 'feminino' | 'outro'
data_nascimento: string
bi: string
telefone: string
email: string
morada: string
}
type ClassForm = {
school: string
nome: string
turno: 'manha' | 'tarde' | 'noite'
sala: string
capacidade: string
}
type EnrollmentForm = {
school: string
student: string
class: string
ano_lectivo: string
data_matricula: string
}
const currentAcademicYear = '2026'
const today = new Date().toISOString().slice(0, 10)
const emptyStudentForm: StudentForm = {
school: '',
numero_processo: '',
nome_completo: '',
sexo: 'masculino',
data_nascimento: '',
bi: '',
telefone: '',
email: '',
morada: '',
}
const emptyClassForm: ClassForm = {
school: '',
nome: '',
turno: 'manha',
sala: '',
capacidade: '35',
}
const emptyEnrollmentForm: EnrollmentForm = {
school: '',
student: '',
class: '',
ano_lectivo: currentAcademicYear,
data_matricula: today,
}
const inputClass =
'mt-2 w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-100 dark:border-slate-700 dark:bg-slate-900 dark:text-white'
const labelClass = 'text-sm font-semibold text-slate-700 dark:text-slate-200'
function readableError(error: any) {
return error?.response?.data?.message || error?.message || 'Não foi possível concluir a operação.'
}
function recordLabel(record?: OptionRecord | null) {
return record?.label || record?.name || record?.nome || record?.nome_completo || 'Sem nome'
}
function cleanText(value: string) {
const trimmed = value.trim()
return trimmed || null
}
function cleanStudentPayload(form: StudentForm) {
return {
school: cleanText(form.school),
numero_processo: cleanText(form.numero_processo),
nome_completo: cleanText(form.nome_completo),
sexo: form.sexo,
data_nascimento: cleanText(form.data_nascimento),
bi: cleanText(form.bi),
telefone: cleanText(form.telefone),
email: cleanText(form.email),
morada: cleanText(form.morada),
status: 'ativo',
}
}
function cleanClassPayload(form: ClassForm) {
return {
school: cleanText(form.school),
nome: cleanText(form.nome),
turno: form.turno,
sala: cleanText(form.sala),
capacidade: Number(form.capacidade) || null,
status: 'ativa',
}
}
function cleanEnrollmentPayload(form: EnrollmentForm) {
return {
school: cleanText(form.school),
student: cleanText(form.student),
class: cleanText(form.class),
ano_lectivo: cleanText(form.ano_lectivo),
data_matricula: cleanText(form.data_matricula),
status: 'ativa',
}
}
const AcademicMvpPage = () => {
const { currentUser } = useAppSelector((state) => state.auth)
const canCreateStudents = currentUser && hasPermission(currentUser, 'CREATE_STUDENTS')
const canCreateClasses = currentUser && hasPermission(currentUser, 'CREATE_CLASSES')
const canCreateEnrollments = currentUser && hasPermission(currentUser, 'CREATE_ENROLLMENTS')
const [schools, setSchools] = useState<SchoolRecord[]>([])
const [students, setStudents] = useState<StudentRecord[]>([])
const [classes, setClasses] = useState<ClassRecord[]>([])
const [enrollments, setEnrollments] = useState<EnrollmentRecord[]>([])
const [counts, setCounts] = useState({ schools: 0, students: 0, classes: 0, enrollments: 0 })
const [studentForm, setStudentForm] = useState<StudentForm>(emptyStudentForm)
const [classForm, setClassForm] = useState<ClassForm>(emptyClassForm)
const [enrollmentForm, setEnrollmentForm] = useState<EnrollmentForm>(emptyEnrollmentForm)
const [activeWorkflow, setActiveWorkflow] = useState<'student' | 'class' | 'enrollment'>('student')
const [isLoading, setIsLoading] = useState(false)
const [isSavingStudent, setIsSavingStudent] = useState(false)
const [isSavingClass, setIsSavingClass] = useState(false)
const [isSavingEnrollment, setIsSavingEnrollment] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const selectedSchoolId = enrollmentForm.school || studentForm.school || classForm.school || schools[0]?.id || ''
const recentEnrollments = useMemo(() => enrollments.slice(0, 6), [enrollments])
const activeEnrollments = enrollments.filter((enrollment) => (enrollment.status || 'ativa') === 'ativa').length
const occupancy = classes.length ? Math.round((activeEnrollments / classes.length) * 10) / 10 : 0
const loadAcademicData = async () => {
setIsLoading(true)
setError('')
try {
const [schoolsResponse, studentsResponse, classesResponse, enrollmentsResponse] = await Promise.all([
axios.get('/schools?limit=100&page=0'),
axios.get('/students?limit=100&page=0'),
axios.get('/classes?limit=100&page=0'),
axios.get('/enrollments?limit=100&page=0'),
])
const schoolRows = Array.isArray(schoolsResponse.data?.rows) ? schoolsResponse.data.rows : []
const studentRows = Array.isArray(studentsResponse.data?.rows) ? studentsResponse.data.rows : []
const classRows = Array.isArray(classesResponse.data?.rows) ? classesResponse.data.rows : []
const enrollmentRows = Array.isArray(enrollmentsResponse.data?.rows) ? enrollmentsResponse.data.rows : []
setSchools(schoolRows)
setStudents(studentRows)
setClasses(classRows)
setEnrollments(enrollmentRows)
setCounts({
schools: schoolsResponse.data?.count || schoolRows.length,
students: studentsResponse.data?.count || studentRows.length,
classes: classesResponse.data?.count || classRows.length,
enrollments: enrollmentsResponse.data?.count || enrollmentRows.length,
})
const firstSchool = selectedSchoolId || schoolRows[0]?.id || ''
const firstStudent = enrollmentForm.student || studentRows[0]?.id || ''
const firstClass = enrollmentForm.class || classRows[0]?.id || ''
setStudentForm((current) => ({ ...current, school: current.school || firstSchool }))
setClassForm((current) => ({ ...current, school: current.school || firstSchool }))
setEnrollmentForm((current) => ({
...current,
school: current.school || firstSchool,
student: current.student || firstStudent,
class: current.class || firstClass,
}))
} catch (loadError) {
const errorMessage = readableError(loadError)
console.error('Failed to load academic MVP data:', errorMessage)
setError(errorMessage)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
if (!currentUser) return
loadAcademicData()
}, [currentUser])
const handleStudentField = (field: keyof StudentForm) => (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
) => {
setStudentForm((current) => ({ ...current, [field]: event.target.value }))
}
const handleClassField = (field: keyof ClassForm) => (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
setClassForm((current) => ({ ...current, [field]: event.target.value }))
}
const handleEnrollmentField = (field: keyof EnrollmentForm) => (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
setEnrollmentForm((current) => ({ ...current, [field]: event.target.value }))
}
const handleCreateStudent = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setMessage('')
setError('')
if (!studentForm.school) {
setError('Selecione uma escola antes de cadastrar o aluno.')
return
}
if (!studentForm.nome_completo.trim()) {
setError('Informe o nome completo do aluno.')
return
}
if (studentForm.email.trim() && !studentForm.email.includes('@')) {
setError('Informe um email válido para o aluno.')
return
}
setIsSavingStudent(true)
try {
await axios.post('/students', { data: cleanStudentPayload(studentForm) })
setMessage('Aluno criado com sucesso. Agora pode ser matriculado numa turma.')
setStudentForm((current) => ({ ...emptyStudentForm, school: current.school }))
await loadAcademicData()
setActiveWorkflow('enrollment')
} catch (saveError) {
const errorMessage = readableError(saveError)
console.error('Failed to create student from Academic MVP:', errorMessage)
setError(errorMessage)
} finally {
setIsSavingStudent(false)
}
}
const handleCreateClass = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setMessage('')
setError('')
if (!classForm.school) {
setError('Selecione uma escola antes de criar a turma.')
return
}
if (!classForm.nome.trim()) {
setError('Informe o nome da turma.')
return
}
setIsSavingClass(true)
try {
await axios.post('/classes', { data: cleanClassPayload(classForm) })
setMessage('Turma criada com sucesso e pronta para receber matrículas.')
setClassForm((current) => ({ ...emptyClassForm, school: current.school }))
await loadAcademicData()
setActiveWorkflow('enrollment')
} catch (saveError) {
const errorMessage = readableError(saveError)
console.error('Failed to create class from Academic MVP:', errorMessage)
setError(errorMessage)
} finally {
setIsSavingClass(false)
}
}
const handleCreateEnrollment = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setMessage('')
setError('')
if (!enrollmentForm.school || !enrollmentForm.student || !enrollmentForm.class) {
setError('Selecione escola, aluno e turma para concluir a matrícula.')
return
}
if (!enrollmentForm.ano_lectivo.trim()) {
setError('Informe o ano lectivo da matrícula.')
return
}
setIsSavingEnrollment(true)
try {
await axios.post('/enrollments', { data: cleanEnrollmentPayload(enrollmentForm) })
setMessage('Matrícula criada com sucesso. O aluno já está associado à escola e turma.')
setEnrollmentForm((current) => ({ ...current, student: '', class: '', data_matricula: today }))
await loadAcademicData()
} catch (saveError) {
const errorMessage = readableError(saveError)
console.error('Failed to create enrollment from Academic MVP:', errorMessage)
setError(errorMessage)
} finally {
setIsSavingEnrollment(false)
}
}
return (
<>
<Head>
<title>{getPageTitle('MVP Académico')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiBookEducationOutline} title="MVP Académico" main>
<BaseButton href="/students/students-list" color="whiteDark" label="CRUD académico" />
</SectionTitleLineWithButton>
<div className="mb-6 overflow-hidden rounded-3xl bg-slate-950 text-white shadow-2xl shadow-slate-300/40 dark:shadow-none">
<div className="grid gap-8 bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.36),_transparent_34%),linear-gradient(135deg,#06281f_0%,#0f172a_52%,#1e3a8a_100%)] p-8 lg:grid-cols-[1.25fr_0.75fr]">
<div>
<span className="inline-flex rounded-full border border-emerald-300/30 bg-white/10 px-4 py-2 text-xs font-bold uppercase tracking-[0.24em] text-emerald-100">
GB-GESTÃO ESCOLAR S.A · Academic Core
</span>
<h2 className="mt-5 max-w-4xl text-3xl font-black tracking-tight sm:text-4xl lg:text-5xl">
Matrícula rápida, aluno, turma e operação escolar num único console.
</h2>
<p className="mt-5 max-w-3xl text-base leading-7 text-slate-200">
Este MVP académico liga a escola/tenant ao ciclo operacional: cadastrar aluno, preparar turma, efetuar matrícula e acompanhar indicadores essenciais.
</p>
<div className="mt-7 flex flex-wrap gap-3">
<BaseButton color="success" label="Criar aluno" onClick={() => setActiveWorkflow('student')} />
<BaseButton color="info" label="Criar turma" onClick={() => setActiveWorkflow('class')} />
<BaseButton color="warning" label="Matricular" onClick={() => setActiveWorkflow('enrollment')} />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
{[
['Escolas', counts.schools, mdiSchoolOutline],
['Alunos', counts.students, mdiAccountSchool],
['Turmas', counts.classes, mdiGoogleClassroom],
['Matrículas', counts.enrollments, mdiClipboardCheckOutline],
].map(([label, value, icon]) => (
<div key={label as string} className="rounded-3xl border border-white/10 bg-white/10 p-5 backdrop-blur">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-bold uppercase tracking-[0.2em] text-slate-300">{label}</p>
<span className="rounded-2xl bg-emerald-400/20 p-2 text-emerald-100">
<svg viewBox="0 0 24 24" className="h-5 w-5 fill-current" aria-hidden="true">
<path d={icon as string} />
</svg>
</span>
</div>
<p className="mt-2 text-3xl font-black">{value as number}</p>
</div>
))}
</div>
</div>
</div>
{(message || error) && (
<div className={`mb-6 rounded-3xl border p-4 text-sm font-semibold ${error ? 'border-rose-200 bg-rose-50 text-rose-700' : 'border-emerald-200 bg-emerald-50 text-emerald-700'}`}>
{error || message}
</div>
)}
<div className="mb-6 grid gap-4 md:grid-cols-3">
{[
{
key: 'student',
title: '1. Cadastrar aluno',
description: 'Dados mínimos do estudante para iniciar o processo académico.',
},
{
key: 'class',
title: '2. Preparar turma',
description: 'Turno, sala, capacidade e estado operacional da turma.',
},
{
key: 'enrollment',
title: '3. Efetuar matrícula',
description: 'Ligação formal entre escola, aluno, turma e ano lectivo.',
},
].map((step) => (
<button
key={step.key}
type="button"
onClick={() => setActiveWorkflow(step.key as 'student' | 'class' | 'enrollment')}
className={`rounded-3xl border p-5 text-left transition hover:-translate-y-0.5 hover:shadow-lg ${
activeWorkflow === step.key
? 'border-emerald-300 bg-emerald-50 shadow-lg shadow-emerald-100/70 dark:bg-emerald-950/30'
: 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900'
}`}
>
<p className="font-black text-slate-900 dark:text-white">{step.title}</p>
<p className="mt-2 text-sm leading-6 text-slate-500 dark:text-slate-400">{step.description}</p>
</button>
))}
</div>
<div className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-emerald-600">Fluxo operacional</p>
<h3 className="text-2xl font-black text-slate-900 dark:text-white">
{activeWorkflow === 'student' && 'Cadastro rápido de aluno'}
{activeWorkflow === 'class' && 'Criação rápida de turma'}
{activeWorkflow === 'enrollment' && 'Matrícula académica'}
</h3>
</div>
<BaseButton color="whiteDark" small icon={mdiRefresh} label={isLoading ? 'A carregar' : 'Atualizar'} onClick={loadAcademicData} disabled={isLoading} />
</div>
{schools.length === 0 && !isLoading && (
<div className="mb-5 rounded-3xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-800">
Crie primeiro uma escola/tenant no Onboarding Multi-Escola para ativar o MVP académico.
</div>
)}
{activeWorkflow === 'student' && (
<form onSubmit={handleCreateStudent} className="space-y-5">
<div className="grid gap-4 md:grid-cols-2">
<label className={labelClass}>
Escola
<select name="student_school" className={inputClass} value={studentForm.school} onChange={handleStudentField('school')}>
<option value="">Selecionar escola</option>
{schools.map((school) => (
<option key={school.id} value={school.id}>{recordLabel(school)}</option>
))}
</select>
</label>
<label className={labelClass}>
de processo
<input name="numero_processo" className={inputClass} value={studentForm.numero_processo} onChange={handleStudentField('numero_processo')} placeholder="Ex.: GB-ALU-0001" />
</label>
</div>
<label className={labelClass}>
Nome completo do aluno
<input name="nome_completo" className={inputClass} value={studentForm.nome_completo} onChange={handleStudentField('nome_completo')} placeholder="Nome completo" />
</label>
<div className="grid gap-4 md:grid-cols-3">
<label className={labelClass}>
Sexo
<select name="sexo" className={inputClass} value={studentForm.sexo} onChange={handleStudentField('sexo')}>
<option value="masculino">Masculino</option>
<option value="feminino">Feminino</option>
<option value="outro">Outro</option>
</select>
</label>
<label className={labelClass}>
Data de nascimento
<input name="data_nascimento" className={inputClass} type="date" value={studentForm.data_nascimento} onChange={handleStudentField('data_nascimento')} />
</label>
<label className={labelClass}>
BI / Documento
<input name="bi" className={inputClass} value={studentForm.bi} onChange={handleStudentField('bi')} placeholder="BI, passaporte ou cédula" />
</label>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className={labelClass}>
Telefone
<input name="telefone" className={inputClass} value={studentForm.telefone} onChange={handleStudentField('telefone')} placeholder="+244 ..." />
</label>
<label className={labelClass}>
Email
<input name="email" className={inputClass} type="email" value={studentForm.email} onChange={handleStudentField('email')} placeholder="aluno@escola.ao" />
</label>
</div>
<label className={labelClass}>
Morada
<textarea name="morada" className={`${inputClass} min-h-24`} value={studentForm.morada} onChange={handleStudentField('morada')} placeholder="Endereço do aluno" />
</label>
<BaseButton type="submit" color="success" label={isSavingStudent ? 'A criar aluno...' : 'Criar aluno'} disabled={!canCreateStudents || isSavingStudent} />
{!canCreateStudents && <p className="text-sm text-amber-600">O utilizador atual não tem permissão CREATE_STUDENTS.</p>}
</form>
)}
{activeWorkflow === 'class' && (
<form onSubmit={handleCreateClass} className="space-y-5">
<div className="grid gap-4 md:grid-cols-2">
<label className={labelClass}>
Escola
<select name="class_school" className={inputClass} value={classForm.school} onChange={handleClassField('school')}>
<option value="">Selecionar escola</option>
{schools.map((school) => (
<option key={school.id} value={school.id}>{recordLabel(school)}</option>
))}
</select>
</label>
<label className={labelClass}>
Nome da turma
<input name="class_nome" className={inputClass} value={classForm.nome} onChange={handleClassField('nome')} placeholder="Ex.: 10ª Classe A" />
</label>
</div>
<div className="grid gap-4 md:grid-cols-3">
<label className={labelClass}>
Turno
<select name="turno" className={inputClass} value={classForm.turno} onChange={handleClassField('turno')}>
<option value="manha">Manhã</option>
<option value="tarde">Tarde</option>
<option value="noite">Noite</option>
</select>
</label>
<label className={labelClass}>
Sala
<input name="sala" className={inputClass} value={classForm.sala} onChange={handleClassField('sala')} placeholder="Ex.: Sala 12" />
</label>
<label className={labelClass}>
Capacidade
<input name="capacidade" className={inputClass} type="number" min="1" value={classForm.capacidade} onChange={handleClassField('capacidade')} />
</label>
</div>
<BaseButton type="submit" color="info" label={isSavingClass ? 'A criar turma...' : 'Criar turma'} disabled={!canCreateClasses || isSavingClass} />
{!canCreateClasses && <p className="text-sm text-amber-600">O utilizador atual não tem permissão CREATE_CLASSES.</p>}
</form>
)}
{activeWorkflow === 'enrollment' && (
<form onSubmit={handleCreateEnrollment} className="space-y-5">
<label className={labelClass}>
Escola
<select name="enrollment_school" className={inputClass} value={enrollmentForm.school} onChange={handleEnrollmentField('school')}>
<option value="">Selecionar escola</option>
{schools.map((school) => (
<option key={school.id} value={school.id}>{recordLabel(school)}</option>
))}
</select>
</label>
<div className="grid gap-4 md:grid-cols-2">
<label className={labelClass}>
Aluno
<select name="enrollment_student" className={inputClass} value={enrollmentForm.student} onChange={handleEnrollmentField('student')}>
<option value="">Selecionar aluno</option>
{students.map((student) => (
<option key={student.id} value={student.id}>{recordLabel(student)}</option>
))}
</select>
</label>
<label className={labelClass}>
Turma
<select name="enrollment_class" className={inputClass} value={enrollmentForm.class} onChange={handleEnrollmentField('class')}>
<option value="">Selecionar turma</option>
{classes.map((classItem) => (
<option key={classItem.id} value={classItem.id}>{recordLabel(classItem)}</option>
))}
</select>
</label>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className={labelClass}>
Ano lectivo
<input name="ano_lectivo" className={inputClass} value={enrollmentForm.ano_lectivo} onChange={handleEnrollmentField('ano_lectivo')} placeholder="2026" />
</label>
<label className={labelClass}>
Data da matrícula
<input name="data_matricula" className={inputClass} type="date" value={enrollmentForm.data_matricula} onChange={handleEnrollmentField('data_matricula')} />
</label>
</div>
<BaseButton type="submit" color="warning" label={isSavingEnrollment ? 'A matricular...' : 'Criar matrícula'} disabled={!canCreateEnrollments || isSavingEnrollment} />
{!canCreateEnrollments && <p className="text-sm text-amber-600">O utilizador atual não tem permissão CREATE_ENROLLMENTS.</p>}
</form>
)}
</CardBox>
<div className="space-y-6">
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-5">
<p className="text-sm font-bold uppercase tracking-[0.2em] text-blue-600">BI académico inicial</p>
<h3 className="text-2xl font-black text-slate-900 dark:text-white">Saúde operacional</h3>
</div>
<div className="grid gap-4 sm:grid-cols-3 xl:grid-cols-1 2xl:grid-cols-3">
{[
['Matrículas ativas', activeEnrollments],
['Média por turma', occupancy],
['Ano lectivo', currentAcademicYear],
].map(([label, value]) => (
<div key={label as string} className="rounded-3xl bg-slate-50 p-5 dark:bg-slate-900">
<p className="text-xs font-bold uppercase tracking-[0.18em] text-slate-400">{label}</p>
<p className="mt-2 text-2xl font-black text-slate-900 dark:text-white">{value}</p>
</div>
))}
</div>
</CardBox>
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-5 flex items-center justify-between gap-3">
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-emerald-600">Últimas matrículas</p>
<h3 className="text-2xl font-black text-slate-900 dark:text-white">Operação recente</h3>
</div>
<BaseButton href="/enrollments/enrollments-list" color="whiteDark" small label="Ver todas" />
</div>
{isLoading && <p className="rounded-2xl bg-slate-50 p-5 text-sm text-slate-500">A carregar dados académicos...</p>}
{!isLoading && recentEnrollments.length === 0 && (
<div className="rounded-3xl border border-dashed border-slate-300 bg-slate-50 p-8 text-center dark:border-slate-700 dark:bg-slate-900">
<p className="text-lg font-black text-slate-800 dark:text-white">Ainda não matrículas.</p>
<p className="mt-2 text-sm text-slate-500">Use o fluxo à esquerda para criar a primeira matrícula académica.</p>
</div>
)}
<div className="space-y-3">
{recentEnrollments.map((enrollment) => (
<div key={enrollment.id} className="rounded-3xl border border-slate-200 bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<div className="flex items-start justify-between gap-3">
<div>
<p className="font-black text-slate-900 dark:text-white">{recordLabel(enrollment.student)}</p>
<p className="mt-1 text-sm text-slate-500">
{recordLabel(enrollment.class)} · {recordLabel(enrollment.school)}
</p>
</div>
<span className="rounded-full bg-emerald-50 px-3 py-1 text-xs font-bold text-emerald-700 ring-1 ring-emerald-200">
{enrollment.status || 'ativa'}
</span>
</div>
<p className="mt-3 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
Ano lectivo {enrollment.ano_lectivo || '—'} · {enrollment.data_matricula ? new Date(enrollment.data_matricula).toLocaleDateString('pt-PT') : 'sem data'}
</p>
</div>
))}
</div>
</CardBox>
</div>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-4">
{[
['Frequência', '/attendance/attendance-list'],
['Avaliações', '/assessments/assessments-list'],
['Notas/Pautas', '/grades/grades-list'],
['Disciplinas', '/subjects/subjects-list'],
].map(([label, href]) => (
<CardBox key={label} className="border-0 shadow-lg shadow-slate-200/60 dark:shadow-none">
<p className="font-black text-slate-900 dark:text-white">{label}</p>
<p className="mt-2 min-h-12 text-sm leading-6 text-slate-500">Módulo preparado no CRUD para expansão do MVP académico.</p>
<BaseButton href={href} color="whiteDark" small label="Abrir" />
</CardBox>
))}
</div>
</SectionMain>
</>
)
}
AcademicMvpPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_STUDENTS">{page}</LayoutAuthenticated>
}
export default AcademicMvpPage

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/assessments/assessmentsSlice'; import {setRefetch, uploadCsv} from '../../stores/assessments/assessmentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const AssessmentsTablesPage = () => { const AssessmentsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.assessments', 'Assessments');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -100,28 +113,28 @@ const AssessmentsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Assessments')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Assessments" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/assessments/assessments-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/assessments/assessments-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getAssessmentsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getAssessmentsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -143,9 +156,9 @@ const AssessmentsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/attendance/attendanceSlice'; import {setRefetch, uploadCsv} from '../../stores/attendance/attendanceSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const AttendanceTablesPage = () => { const AttendanceTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.attendance', 'Attendance');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const AttendanceTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Attendance')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Attendance" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/attendance/attendance-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/attendance/attendance-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getAttendanceCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getAttendanceCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -135,9 +148,9 @@ const AttendanceTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/book_loans/book_loansSlice'; import {setRefetch, uploadCsv} from '../../stores/book_loans/book_loansSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const Book_loansTablesPage = () => { const Book_loansTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.bookLoans', 'Book_loans');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -96,28 +109,28 @@ const Book_loansTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Book_loans')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Book_loans" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/book_loans/book_loans-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/book_loans/book_loans-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBook_loansCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getBook_loansCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -143,9 +156,9 @@ const Book_loansTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/books/booksSlice'; import {setRefetch, uploadCsv} from '../../stores/books/booksSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const BooksTablesPage = () => { const BooksTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.books', 'Books');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const BooksTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Books')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Books" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/books/books-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/books/books-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBooksCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getBooksCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const BooksTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/classes/classesSlice'; import {setRefetch, uploadCsv} from '../../stores/classes/classesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const ClassesTablesPage = () => { const ClassesTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.classes', 'Classes');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const ClassesTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Classes')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Classes" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/classes/classes-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/classes/classes-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getClassesCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getClassesCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -135,9 +148,9 @@ const ClassesTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/courses/coursesSlice'; import {setRefetch, uploadCsv} from '../../stores/courses/coursesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const CoursesTablesPage = () => { const CoursesTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.courses', 'Courses');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const CoursesTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Courses')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Courses" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/courses/courses-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/courses/courses-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getCoursesCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getCoursesCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const CoursesTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -16,13 +16,19 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useTranslation } from 'react-i18next';
const Dashboard = () => { const Dashboard = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const iconsColor = useAppSelector((state) => state.style.iconsColor); const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners); const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const loadingMessage = 'Loading...'; const loadingMessage = translate('pages.dashboard.loading', 'Loading...');
const [users, setUsers] = React.useState(loadingMessage); const [users, setUsers] = React.useState(loadingMessage);
@ -88,6 +94,10 @@ const Dashboard = () => {
async function getWidgets(roleId) { async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId)); await dispatch(fetchWidgets(roleId));
} }
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
loadData().then(); loadData().then();
@ -103,13 +113,13 @@ const Dashboard = () => {
<> <>
<Head> <Head>
<title> <title>
{getPageTitle('Overview')} {getPageTitle(translate('pages.dashboard.overview', 'Overview'))}
</title> </title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant} icon={icon.mdiChartTimelineVariant}
title='Overview' title={translate('pages.dashboard.overview', 'Overview')}
main> main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
@ -123,7 +133,7 @@ const Dashboard = () => {
{!!rolesWidgets.length && {!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && ( hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'> <p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`} {translate('pages.dashboard.roleWidgets', '{{role}}\'s widgets').replace('{{role}}', widgetsRole?.role?.label || translate('entities.users', 'Users'))}
</p> </p>
)} )}
@ -137,7 +147,7 @@ const Dashboard = () => {
size={48} size={48}
path={icon.mdiLoading} path={icon.mdiLoading}
/>{' '} />{' '}
Loading widgets... {translate('pages.dashboard.loadingWidgets', 'Loading widgets...')}
</div> </div>
)} )}
@ -165,7 +175,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Users {translate('entities.users', 'Users')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{users} {users}
@ -193,7 +203,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Roles {translate('entities.roles', 'Roles')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{roles} {roles}
@ -221,7 +231,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Permissions {translate('entities.permissions', 'Permissions')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{permissions} {permissions}
@ -249,7 +259,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Schools {translate('entities.schools', 'Schools')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{schools} {schools}
@ -277,7 +287,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Students {translate('entities.students', 'Students')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{students} {students}
@ -305,7 +315,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Guardians {translate('entities.guardians', 'Guardians')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{guardians} {guardians}
@ -333,7 +343,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Student guardians {translate('entities.studentGuardians', 'Student guardians')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{student_guardians} {student_guardians}
@ -361,7 +371,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Teachers {translate('entities.teachers', 'Teachers')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{teachers} {teachers}
@ -389,7 +399,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Courses {translate('entities.courses', 'Courses')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{courses} {courses}
@ -417,7 +427,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Grades {translate('entities.grades', 'Grades')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{grades} {grades}
@ -445,7 +455,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Classes {translate('entities.classes', 'Classes')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{classes} {classes}
@ -473,7 +483,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Subjects {translate('entities.subjects', 'Subjects')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{subjects} {subjects}
@ -501,7 +511,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Enrollments {translate('entities.enrollments', 'Enrollments')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{enrollments} {enrollments}
@ -529,7 +539,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Assessments {translate('entities.assessments', 'Assessments')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{assessments} {assessments}
@ -557,7 +567,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Attendance {translate('entities.attendance', 'Attendance')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{attendance} {attendance}
@ -585,7 +595,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Invoices {translate('entities.invoices', 'Invoices')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{invoices} {invoices}
@ -613,7 +623,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Payments {translate('entities.payments', 'Payments')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{payments} {payments}
@ -641,7 +651,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Employees {translate('entities.employees', 'Employees')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{employees} {employees}
@ -669,7 +679,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Products {translate('entities.products', 'Products')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{products} {products}
@ -697,7 +707,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Books {translate('entities.books', 'Books')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{books} {books}
@ -725,7 +735,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center"> <div className="flex justify-between align-center">
<div> <div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Book loans {translate('entities.bookLoans', 'Book loans')}
</div> </div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-3xl leading-tight font-semibold">
{book_loans} {book_loans}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/employees/employeesSlice'; import {setRefetch, uploadCsv} from '../../stores/employees/employeesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const EmployeesTablesPage = () => { const EmployeesTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.employees', 'Employees');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const EmployeesTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Employees')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Employees" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/employees/employees-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/employees/employees-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getEmployeesCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getEmployeesCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const EmployeesTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/enrollments/enrollmentsSlice'; import {setRefetch, uploadCsv} from '../../stores/enrollments/enrollmentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const EnrollmentsTablesPage = () => { const EnrollmentsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.enrollments', 'Enrollments');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -96,28 +109,28 @@ const EnrollmentsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Enrollments')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Enrollments" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/enrollments/enrollments-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/enrollments/enrollments-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getEnrollmentsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getEnrollmentsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -139,9 +152,9 @@ const EnrollmentsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -13,32 +13,42 @@ import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import axios from "axios"; import axios from "axios";
import { useTranslation } from 'react-i18next';
export default function Forgot() { export default function Forgot() {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const [loading, setLoading] = React.useState(false) const [loading, setLoading] = React.useState(false)
const router = useRouter(); const router = useRouter();
const notify = (type, msg) => toast( msg, {type}); const notify = (type, msg) => toast( msg, {type});
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const handleSubmit = async (value) => { const handleSubmit = async (value) => {
setLoading(true) setLoading(true)
try { try {
const { data: response } = await axios.post('/auth/send-password-reset-email', value); const { data: response } = await axios.post('/auth/send-password-reset-email', value);
setLoading(false) setLoading(false)
notify('success', 'Please check your email for verification link'); notify('success', translate('pages.auth.checkEmailVerification', 'Please check your email for verification link'));
setTimeout(async () => { setTimeout(async () => {
await router.push('/login') await router.push('/login')
}, 3000) }, 3000)
} catch (error) { } catch (error) {
setLoading(false) setLoading(false)
console.log('error: ', error) console.log('error: ', error)
notify('error', 'Something was wrong. Try again') notify('error', translate('pages.auth.genericError', 'Something was wrong. Try again'))
} }
}; };
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Login')}</title> <title>{getPageTitle(translate('pages.forgot.pageTitle', 'Forgot password'))}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
@ -50,7 +60,7 @@ export default function Forgot() {
onSubmit={(values) => handleSubmit(values)} onSubmit={(values) => handleSubmit(values)}
> >
<Form> <Form>
<FormField label='Email' help='Please enter your email'> <FormField label={translate('pages.auth.emailLabel', 'Email')} help={translate('pages.auth.emailHelp', 'Please enter your email')}>
<Field name='email' /> <Field name='email' />
</FormField> </FormField>
@ -59,12 +69,12 @@ export default function Forgot() {
<BaseButtons> <BaseButtons>
<BaseButton <BaseButton
type='submit' type='submit'
label={loading ? 'Loading...' : 'Submit' } label={loading ? translate('pages.auth.loading', 'Loading...') : translate('pages.forgot.submit', 'Submit')}
color='info' color='info'
/> />
<BaseButton <BaseButton
href={'/login'} href={'/login'}
label={'Login'} label={translate('pages.auth.login', 'Login')}
color='info' color='info'
/> />
</BaseButtons> </BaseButtons>

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/grades/gradesSlice'; import {setRefetch, uploadCsv} from '../../stores/grades/gradesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const GradesTablesPage = () => { const GradesTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.grades', 'Grades');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const GradesTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Grades')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Grades" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/grades/grades-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/grades/grades-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getGradesCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getGradesCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -135,9 +148,9 @@ const GradesTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/guardians/guardiansSlice'; import {setRefetch, uploadCsv} from '../../stores/guardians/guardiansSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const GuardiansTablesPage = () => { const GuardiansTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.guardians', 'Guardians');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const GuardiansTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Guardians')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Guardians" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/guardians/guardians-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/guardians/guardians-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getGuardiansCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getGuardiansCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const GuardiansTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -1,166 +1,175 @@
import { mdiArrowRight, mdiChartTimelineVariant, mdiLockCheckOutline, mdiSchoolOutline } from '@mdi/js'
import Head from 'next/head'
import Link from 'next/link'
import React, { ReactElement } from 'react'
import React, { useEffect, useState } from 'react'; import BaseButton from '../components/BaseButton'
import type { ReactElement } from 'react'; import BaseIcon from '../components/BaseIcon'
import Head from 'next/head'; import LayoutGuest from '../layouts/Guest'
import Link from 'next/link'; import { getPageTitle } from '../config'
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
const modules = [
'Gestão Escolar',
'Financeiro',
'RH',
'Biblioteca',
'Stock',
'Portais',
'BI',
'IA',
]
const metrics = [
['Multi-escola', 'Dados isolados por escola'],
['PALOP ready', 'Pensado para Angola e expansão'],
['ERP modular', 'Cresce por fases sem refazer a base'],
]
export default function Starter() { export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'GB Gestao Escolar SA'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return ( return (
<div <>
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<Head> <Head>
<title>{getPageTitle('Starter Page')}</title> <title>{getPageTitle('GB-GESTÃO ESCOLAR S.A')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <main className="min-h-screen overflow-hidden bg-[#F8FAFC] text-slate-950">
<div <nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
className={`flex ${ <Link href="/" className="flex items-center gap-3">
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row' <span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#073B2A] text-sm font-black text-white shadow-lg shadow-emerald-900/20">
} min-h-screen w-full`} GB
> </span>
{contentType === 'image' && contentPosition !== 'background' <span>
? imageBlock(illustrationImage) <span className="block text-sm font-black tracking-tight">GB-GESTÃO ESCOLAR</span>
: null} <span className="block text-xs font-semibold text-slate-500">SaaS multi-escola</span>
{contentType === 'video' && contentPosition !== 'background' </span>
? videoBlock(illustrationVideo) </Link>
: null} <div className="flex items-center gap-3">
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'> <Link href="/login" className="hidden rounded-full px-4 py-2 text-sm font-bold text-slate-700 transition hover:bg-white hover:text-slate-950 sm:inline-flex">
<CardBox className='w-full md:w-3/5 lg:w-2/3'> Login
<CardBoxComponentTitle title="Welcome to your GB Gestao Escolar SA app!"/> </Link>
<BaseButton href="/login" color="info" label="Admin interface" roundedFull />
</div>
</nav>
<div className="space-y-3"> <section className="relative mx-auto grid max-w-7xl gap-10 px-6 pb-20 pt-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-center lg:pt-14">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p> <div className="absolute left-1/2 top-24 -z-0 h-72 w-72 rounded-full bg-emerald-300/30 blur-3xl" />
<p className='text-center text-gray-500'>For guides and documentation please check <div className="relative z-10">
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p> <span className="inline-flex rounded-full border border-emerald-200 bg-white px-4 py-2 text-xs font-black uppercase tracking-[0.24em] text-[#087A52] shadow-sm">
Angola · PALOP · ERP Escolar
</span>
<h1 className="mt-7 max-w-4xl text-5xl font-black leading-[0.95] tracking-[-0.05em] text-slate-950 md:text-7xl">
Uma plataforma única para gerir várias escolas com dados isolados.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600">
O GB-GESTÃO ESCOLAR S.A organiza escolas, alunos, professores, matrículas, finanças e operações num SaaS moderno preparado para crescer de uma instituição para uma rede completa.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<BaseButton href="/login" color="info" label="Entrar no admin" roundedFull className="shadow-xl shadow-emerald-900/20" />
<BaseButton href="/login" color="whiteDark" outline label="Criar primeira escola" roundedFull />
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-3">
{metrics.map(([title, subtitle]) => (
<div key={title} className="rounded-3xl border border-white bg-white/80 p-5 shadow-xl shadow-slate-200/60 backdrop-blur">
<p className="font-black text-slate-950">{title}</p>
<p className="mt-2 text-sm text-slate-500">{subtitle}</p>
</div>
))}
</div>
</div>
<div className="relative z-10">
<div className="rounded-[2rem] bg-[#071B2D] p-3 shadow-2xl shadow-blue-950/30">
<div className="rounded-[1.5rem] bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.35),_transparent_32%),linear-gradient(135deg,#0f172a_0%,#082f49_55%,#064e3b_100%)] p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-emerald-100">Executive dashboard</p>
<p className="mt-1 text-2xl font-black">Rede Escolar</p>
</div>
<span className="rounded-full bg-emerald-400/20 px-3 py-1 text-xs font-bold text-emerald-100 ring-1 ring-emerald-200/20">
Online + Offline ready
</span>
</div>
<div className="mt-8 grid gap-4 sm:grid-cols-2">
{[
['12', 'Escolas'],
['8.420', 'Alunos'],
['AOA 31M', 'Receita mensal'],
['94%', 'Frequência'],
].map(([value, label]) => (
<div key={label} className="rounded-3xl border border-white/10 bg-white/10 p-5 backdrop-blur">
<p className="text-3xl font-black">{value}</p>
<p className="mt-2 text-sm text-slate-300">{label}</p>
</div>
))}
</div>
<div className="mt-6 rounded-3xl border border-white/10 bg-white p-5 text-slate-950">
<div className="flex items-center gap-3">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-emerald-50 text-emerald-700">
<BaseIcon path={mdiSchoolOutline} size={22} />
</span>
<div>
<p className="font-black">Colégio Esperança</p>
<p className="text-sm text-slate-500">Tenant ativo · school_id isolado</p>
</div>
</div>
<div className="mt-5 h-3 rounded-full bg-slate-100">
<div className="h-3 w-4/5 rounded-full bg-gradient-to-r from-emerald-500 to-blue-600" />
</div>
</div>
</div>
</div>
</div>
</section>
<section className="bg-white px-6 py-20">
<div className="mx-auto max-w-7xl">
<div className="grid gap-8 lg:grid-cols-[0.8fr_1.2fr] lg:items-end">
<div>
<p className="text-sm font-black uppercase tracking-[0.24em] text-emerald-600">MVP profissional</p>
<h2 className="mt-3 text-4xl font-black tracking-tight text-slate-950">Do cadastro da escola ao crescimento modular.</h2>
</div>
<p className="text-lg leading-8 text-slate-600">
A primeira entrega foca no fluxo central de SaaS: criar e visualizar escolas/tenants. Depois disso, alunos, professores, matrículas e financeiro passam a operar ligados à escola correta.
</p>
</div> </div>
<BaseButtons> <div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<BaseButton {modules.map((module, index) => (
href='/login' <div key={module} className="group rounded-3xl border border-slate-100 bg-slate-50 p-6 transition hover:-translate-y-1 hover:bg-slate-950 hover:text-white hover:shadow-2xl hover:shadow-slate-300/70">
label='Login' <div className="mb-8 flex items-center justify-between">
color='info' <span className="text-sm font-black text-emerald-600 group-hover:text-emerald-300">0{index + 1}</span>
className='w-full' <BaseIcon path={index % 2 ? mdiLockCheckOutline : mdiChartTimelineVariant} size={22} />
/> </div>
<p className="text-lg font-black">{module}</p>
<p className="mt-2 text-sm leading-6 text-slate-500 group-hover:text-slate-300">Preparado para ser ativado por fases com permissões e dados por escola.</p>
</div>
))}
</div>
</div>
</section>
</BaseButtons> <section className="px-6 py-16">
</CardBox> <div className="mx-auto flex max-w-7xl flex-col items-start justify-between gap-6 rounded-[2rem] bg-[#073B2A] p-8 text-white shadow-2xl shadow-emerald-900/20 md:flex-row md:items-center">
</div> <div>
</div> <p className="text-sm font-bold uppercase tracking-[0.24em] text-emerald-200">Admin interface</p>
</SectionFullScreen> <h2 className="mt-2 text-3xl font-black">Entre para criar a primeira escola tenant.</h2>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'> </div>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p> <Link href="/login" className="inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 text-sm font-black text-[#073B2A] transition hover:bg-emerald-50">
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'> Abrir painel <BaseIcon path={mdiArrowRight} size={18} />
Privacy Policy </Link>
</Link> </div>
</div> </section>
</div> <footer className="border-t border-slate-200 px-6 py-8 text-center text-sm text-slate-500">
); © 2026 GB-GESTÃO ESCOLAR S.A · <Link href="/privacy-policy" className="font-bold text-slate-700">Privacy Policy</Link>
</footer>
</main>
</>
)
} }
Starter.getLayout = function getLayout(page: ReactElement) { Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>
}; }

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/invoices/invoicesSlice'; import {setRefetch, uploadCsv} from '../../stores/invoices/invoicesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const InvoicesTablesPage = () => { const InvoicesTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.invoices', 'Invoices');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const InvoicesTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Invoices')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Invoices" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/invoices/invoices-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/invoices/invoices-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getInvoicesCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getInvoicesCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -137,9 +150,9 @@ const InvoicesTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -21,9 +21,11 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link'; import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
import { useTranslation } from 'react-i18next';
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('common');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const textColor = useAppSelector((state) => state.style.linkColor); const textColor = useAppSelector((state) => state.style.linkColor);
const iconsColor = useAppSelector((state) => state.style.iconsColor); const iconsColor = useAppSelector((state) => state.style.iconsColor);
@ -37,6 +39,7 @@ export default function Login() {
const [contentType, setContentType] = useState('image'); const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right'); const [contentPosition, setContentPosition] = useState('right');
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth, (state) => state.auth,
); );
@ -46,6 +49,10 @@ export default function Login() {
const title = 'GB Gestao Escolar SA' const title = 'GB Gestao Escolar SA'
useEffect(() => {
setIsMounted(true);
}, []);
// Fetch Pexels image/video // Fetch Pexels image/video
useEffect( () => { useEffect( () => {
async function fetchData() { async function fetchData() {
@ -100,20 +107,30 @@ export default function Login() {
})); }));
}; };
const imageBlock = (image) => ( const imageBlock = (image) => {
<div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3" const imageUrl = image?.src?.original;
style={{ const photoCredit = t('pages.login.pexels.photoCredit', { photographer: image?.photographer || 'Pexels' });
backgroundImage: `${image ? `url(${image.src?.original})` : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'}`,
backgroundSize: 'cover', return (
backgroundPosition: 'left center', <div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3"
backgroundRepeat: 'no-repeat', style={{
}}> backgroundImage: imageUrl ? `url(${imageUrl})` : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))',
<div className="flex justify-center w-full bg-blue-300/20"> backgroundSize: 'cover',
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo backgroundPosition: 'left center',
by {image?.photographer} on Pexels</a> backgroundRepeat: 'no-repeat',
}}>
<div className="flex justify-center w-full bg-blue-300/20">
{image?.photographer_url ? (
<a className="text-[8px]" href={image.photographer_url} target="_blank" rel="noreferrer">
{photoCredit}
</a>
) : (
<span className="text-[8px]">{photoCredit}</span>
)}
</div>
</div> </div>
</div> );
) };
const videoBlock = (video) => { const videoBlock = (video) => {
if (video?.video_files?.length > 0) { if (video?.video_files?.length > 0) {
@ -126,7 +143,7 @@ export default function Login() {
muted muted
> >
<source src={video.video_files[0]?.link} type='video/mp4'/> <source src={video.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag. {t('pages.login.pexels.videoUnsupported')}
</video> </video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'> <div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a <a
@ -135,18 +152,22 @@ export default function Login() {
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
> >
Video by {video.user.name} on Pexels {t('pages.login.pexels.videoCredit', { name: video.user.name })}
</a> </a>
</div> </div>
</div>) </div>)
} }
}; };
if (!isMounted) {
return <div />;
}
return ( return (
<div style={contentPosition === 'background' ? { <div style={contentPosition === 'background' ? {
backgroundImage: `${ backgroundImage: `${
illustrationImage illustrationImage?.src?.original
? `url(${illustrationImage.src?.original})` ? `url(${illustrationImage.src.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))' : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`, }`,
backgroundSize: 'cover', backgroundSize: 'cover',
@ -154,7 +175,7 @@ export default function Login() {
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
} : {}}> } : {}}>
<Head> <Head>
<title>{getPageTitle('Login')}</title> <title>{getPageTitle(t('pages.login.pageTitle'))}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
@ -165,30 +186,36 @@ export default function Login() {
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'> <CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
<h2 className="text-4xl font-semibold my-4">{title}</h2> <div className='my-4'>
<h2 className="text-4xl font-semibold">{title}</h2>
</div>
<div className='flex flex-row text-gray-500 justify-between'> <div className='flex flex-row text-gray-500 justify-between'>
<div> <div>
<p className='mb-2'>Use{' '} <p className='mb-2'>
{t('pages.login.sampleCredentialsSuperAdmin', { email: '', password: '' })}{' '}
<code className={`cursor-pointer ${textColor} `} <code className={`cursor-pointer ${textColor} `}
data-password="1561e4f2" data-password="1561e4f2"
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '} onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>1561e4f2</code>{' / '} <code className={`${textColor}`}>1561e4f2</code>
to login as Super Admin</p> </p>
<p className='mb-2'>Use{' '} <p className='mb-2'>
{t('pages.login.sampleCredentialsAdmin', { email: '', password: '' })}{' '}
<code className={`cursor-pointer ${textColor} `} <code className={`cursor-pointer ${textColor} `}
data-password="1561e4f2" data-password="1561e4f2"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '} onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>1561e4f2</code>{' / '} <code className={`${textColor}`}>1561e4f2</code>
to login as Admin</p> </p>
<p>Use <code <p>
className={`cursor-pointer ${textColor} `} {t('pages.login.sampleCredentialsUser', { email: '', password: '' })}{' '}
data-password="ba8575c6f095" <code
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '} className={`cursor-pointer ${textColor} `}
<code className={`${textColor}`}>ba8575c6f095</code>{' / '} data-password="ba8575c6f095"
to login as User</p> onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>ba8575c6f095</code>
</p>
</div> </div>
<div> <div>
<BaseIcon <BaseIcon
@ -210,16 +237,18 @@ export default function Login() {
> >
<Form> <Form>
<FormField <FormField
label='Login' label={t('pages.login.form.loginLabel')}
help='Please enter your login'> labelFor='email'
<Field name='email' /> help={t('pages.login.form.loginHelp')}>
<Field id='email' name='email' autoComplete='username' />
</FormField> </FormField>
<div className='relative'> <div className='relative'>
<FormField <FormField
label='Password' label={t('pages.login.form.passwordLabel')}
help='Please enter your password'> labelFor='password'
<Field name='password' type={showPassword ? 'text' : 'password'} /> help={t('pages.login.form.passwordHelp')}>
<Field id='password' name='password' type={showPassword ? 'text' : 'password'} autoComplete='current-password' />
</FormField> </FormField>
<div <div
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer' className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
@ -234,12 +263,12 @@ export default function Login() {
</div> </div>
<div className={'flex justify-between'}> <div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'> <FormCheckRadio type='checkbox' label={t('pages.login.form.remember')}>
<Field type='checkbox' name='remember' /> <Field id='remember' type='checkbox' name='remember' />
</FormCheckRadio> </FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}> <Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password? {t('pages.login.form.forgotPassword')}
</Link> </Link>
</div> </div>
@ -249,16 +278,16 @@ export default function Login() {
<BaseButton <BaseButton
className={'w-full'} className={'w-full'}
type='submit' type='submit'
label={isFetching ? 'Loading...' : 'Login'} label={isFetching ? t('pages.login.form.loading') : t('pages.login.form.loginButton')}
color='info' color='info'
disabled={isFetching} disabled={isFetching}
/> />
</BaseButtons> </BaseButtons>
<br /> <br />
<p className={'text-center'}> <p className={'text-center'}>
Dont have an account yet?{' '} {t('pages.login.form.noAccountYet')}{' '}
<Link className={`${textColor}`} href={'/register'}> <Link className={`${textColor}`} href={'/register'}>
New Account {t('pages.login.form.newAccount')}
</Link> </Link>
</p> </p>
</Form> </Form>
@ -268,9 +297,9 @@ export default function Login() {
</div> </div>
</SectionFullScreen> </SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'> <div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p> <p className='py-6 text-sm'>{t('pages.login.footer.copyright', { year: 2026, title })}</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'> <Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy {t('pages.login.footer.privacy')}
</Link> </Link>
</div> </div>
<ToastContainer /> <ToastContainer />

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/payments/paymentsSlice'; import {setRefetch, uploadCsv} from '../../stores/payments/paymentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const PaymentsTablesPage = () => { const PaymentsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.payments', 'Payments');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const PaymentsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Payments')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Payments" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/payments/payments-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/payments/payments-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPaymentsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getPaymentsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -135,9 +148,9 @@ const PaymentsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/permissions/permissionsSlice'; import {setRefetch, uploadCsv} from '../../stores/permissions/permissionsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const PermissionsTablesPage = () => { const PermissionsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.permissions', 'Permissions');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -86,28 +99,28 @@ const PermissionsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Permissions')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Permissions" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPermissionsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getPermissionsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -129,9 +142,9 @@ const PermissionsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/products/productsSlice'; import {setRefetch, uploadCsv} from '../../stores/products/productsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const ProductsTablesPage = () => { const ProductsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.products', 'Products');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const ProductsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Products')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getProductsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getProductsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const ProductsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -18,8 +18,14 @@ import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from "axios"; import axios from "axios";
import { useTranslation } from 'react-i18next';
export default function Register() { export default function Register() {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const router = useRouter(); const router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
@ -48,6 +54,9 @@ export default function Register() {
label: org.name label: org.name
})); }));
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const handleSubmit = async (value) => { const handleSubmit = async (value) => {
setLoading(true) setLoading(true)
@ -58,18 +67,18 @@ export default function Register() {
const { data: response } = await axios.post('/auth/signup',formData); const { data: response } = await axios.post('/auth/signup',formData);
await router.push('/login') await router.push('/login')
setLoading(false) setLoading(false)
notify('success', 'Please check your email for verification link') notify('success', translate('pages.auth.checkEmailVerification', 'Please check your email for verification link'))
} catch (error) { } catch (error) {
setLoading(false) setLoading(false)
console.log('error: ', error) console.log('error: ', error)
notify('error', 'Something was wrong. Try again') notify('error', translate('pages.auth.genericError', 'Something was wrong. Try again'))
} }
}; };
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Login')}</title> <title>{getPageTitle(translate('pages.register.pageTitle', 'Register'))}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
@ -84,7 +93,7 @@ export default function Register() {
> >
<Form> <Form>
<label className="block font-bold mb-2" >Organization</label> <label className="block font-bold mb-2" >{translate('pages.register.organization', 'Organization')}</label>
<Select <Select
classNames={{ classNames={{
@ -93,16 +102,16 @@ export default function Register() {
value={selectedOrganization} value={selectedOrganization}
onChange={setSelectedOrganization} onChange={setSelectedOrganization}
options={options} options={options}
placeholder="Select organization..." placeholder={translate('pages.register.selectOrganization', 'Select organization...')}
/> />
<FormField label='Email' help='Please enter your email'> <FormField label={translate('pages.auth.emailLabel', 'Email')} help={translate('pages.auth.emailHelp', 'Please enter your email')}>
<Field type='email' name='email' /> <Field type='email' name='email' />
</FormField> </FormField>
<FormField label='Password' help='Please enter your password'> <FormField label={translate('pages.auth.passwordLabel', 'Password')} help={translate('pages.auth.passwordHelp', 'Please enter your password')}>
<Field type='password' name='password' /> <Field type='password' name='password' />
</FormField> </FormField>
<FormField label='Confirm Password' help='Please confirm your password'> <FormField label={translate('pages.auth.confirmPasswordLabel', 'Confirm Password')} help={translate('pages.auth.confirmPasswordHelp', 'Please confirm your password')}>
<Field type='password' name='confirm' /> <Field type='password' name='confirm' />
</FormField> </FormField>
@ -111,12 +120,12 @@ export default function Register() {
<BaseButtons> <BaseButtons>
<BaseButton <BaseButton
type='submit' type='submit'
label={loading ? 'Loading...' : 'Register' } label={loading ? translate('pages.auth.loading', 'Loading...') : translate('pages.register.submit', 'Register')}
color='info' color='info'
/> />
<BaseButton <BaseButton
href={'/login'} href={'/login'}
label={'Login'} label={translate('pages.auth.login', 'Login')}
color='info' color='info'
/> />
</BaseButtons> </BaseButtons>

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/roles/rolesSlice'; import {setRefetch, uploadCsv} from '../../stores/roles/rolesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const RolesTablesPage = () => { const RolesTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.roles', 'Roles');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -86,28 +99,28 @@ const RolesTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Roles')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Roles" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/roles/roles-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/roles/roles-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getRolesCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getRolesCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -129,9 +142,9 @@ const RolesTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -0,0 +1,386 @@
import { mdiDomain, mdiSchoolOutline } from '@mdi/js'
import axios from 'axios'
import Head from 'next/head'
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
import BaseButton from '../../components/BaseButton'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import { hasPermission } from '../../helpers/userPermissions'
import { useAppSelector } from '../../stores/hooks'
type SchoolStatus = 'active' | 'setup' | 'suspended'
type School = {
id: string
name: string
nif?: string | null
phone?: string | null
email?: string | null
province?: string | null
municipality?: string | null
address?: string | null
logoUrl?: string | null
status?: SchoolStatus | null
createdAt?: string
}
type SchoolForm = {
name: string
nif: string
phone: string
email: string
province: string
municipality: string
address: string
logoUrl: string
status: SchoolStatus
}
const emptyForm: SchoolForm = {
name: '',
nif: '',
phone: '',
email: '',
province: '',
municipality: '',
address: '',
logoUrl: '',
status: 'setup',
}
const statusCopy: Record<SchoolStatus, string> = {
active: 'Ativa',
setup: 'Em implantação',
suspended: 'Suspensa',
}
const statusClasses: Record<SchoolStatus, string> = {
active: 'bg-emerald-50 text-emerald-700 ring-emerald-200',
setup: 'bg-amber-50 text-amber-700 ring-amber-200',
suspended: 'bg-rose-50 text-rose-700 ring-rose-200',
}
function readableError(error: any) {
return error?.response?.data?.message || error?.message || 'Não foi possível concluir a operação.'
}
function cleanPayload(form: SchoolForm) {
return Object.fromEntries(
Object.entries(form).map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value]),
)
}
const inputClass =
'mt-2 w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-100 dark:border-slate-700 dark:bg-slate-900 dark:text-white'
const labelClass = 'text-sm font-semibold text-slate-700 dark:text-slate-200'
const SchoolsOnboardingPage = () => {
const { currentUser } = useAppSelector((state) => state.auth)
const canCreate = currentUser && hasPermission(currentUser, 'CREATE_SCHOOLS')
const [schools, setSchools] = useState<School[]>([])
const [selectedId, setSelectedId] = useState<string>('')
const [form, setForm] = useState<SchoolForm>(emptyForm)
const [isLoading, setIsLoading] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const selectedSchool = useMemo(
() => schools.find((school) => school.id === selectedId) || schools[0],
[schools, selectedId],
)
const totalActive = schools.filter((school) => (school.status || 'active') === 'active').length
const totalSetup = schools.filter((school) => (school.status || 'active') === 'setup').length
const loadSchools = async () => {
setIsLoading(true)
setError('')
try {
const response = await axios.get('/schools?limit=50&page=0')
const rows = Array.isArray(response.data?.rows) ? response.data.rows : []
setSchools(rows)
if (!selectedId && rows[0]?.id) setSelectedId(rows[0].id)
} catch (loadError) {
console.error('Failed to load schools:', loadError)
setError(readableError(loadError))
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadSchools()
}, [])
const handleFieldChange = (field: keyof SchoolForm) => (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
) => {
setForm((current) => ({ ...current, [field]: event.target.value }))
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setMessage('')
setError('')
if (!form.name.trim()) {
setError('Informe o nome oficial da escola para criar o tenant.')
return
}
if (form.email.trim() && !form.email.includes('@')) {
setError('Informe um email institucional válido.')
return
}
setIsSaving(true)
try {
await axios.post('/schools', { data: cleanPayload(form) })
setMessage('Escola criada com sucesso. O tenant já está pronto para receber alunos, professores e módulos.')
setForm(emptyForm)
await loadSchools()
} catch (saveError) {
console.error('Failed to create school:', saveError)
setError(readableError(saveError))
} finally {
setIsSaving(false)
}
}
return (
<>
<Head>
<title>{getPageTitle('Onboarding Multi-Escola')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiSchoolOutline} title="Onboarding Multi-Escola" main>
<BaseButton href="/schools/schools-list" color="whiteDark" label="CRUD completo" />
</SectionTitleLineWithButton>
<div className="mb-6 overflow-hidden rounded-3xl bg-slate-950 text-white shadow-2xl shadow-slate-300/40 dark:shadow-none">
<div className="grid gap-8 bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.36),_transparent_34%),linear-gradient(135deg,#06281f_0%,#0f172a_54%,#172554_100%)] p-8 lg:grid-cols-[1.35fr_0.65fr]">
<div>
<span className="inline-flex rounded-full border border-emerald-300/30 bg-white/10 px-4 py-2 text-xs font-bold uppercase tracking-[0.24em] text-emerald-100">
GB-GESTÃO ESCOLAR S.A
</span>
<h2 className="mt-5 max-w-3xl text-3xl font-black tracking-tight md:text-5xl">
Crie escolas isoladas por tenant e prepare cada instituição para operar no ERP escolar.
</h2>
<p className="mt-4 max-w-2xl text-base leading-7 text-slate-200">
Este primeiro fluxo cadastra a escola-mãe do tenant com NIF, contacto, localização e estado de implantação.
A partir daqui, os restantes módulos usam o vínculo por escola para manter os dados separados.
</p>
</div>
<div className="grid grid-cols-3 gap-3 lg:grid-cols-1">
<div className="rounded-3xl border border-white/10 bg-white/10 p-5 backdrop-blur">
<p className="text-sm text-slate-300">Escolas</p>
<p className="mt-2 text-3xl font-black">{schools.length}</p>
</div>
<div className="rounded-3xl border border-white/10 bg-white/10 p-5 backdrop-blur">
<p className="text-sm text-slate-300">Ativas</p>
<p className="mt-2 text-3xl font-black text-emerald-200">{totalActive}</p>
</div>
<div className="rounded-3xl border border-white/10 bg-white/10 p-5 backdrop-blur">
<p className="text-sm text-slate-300">Implantação</p>
<p className="mt-2 text-3xl font-black text-amber-200">{totalSetup}</p>
</div>
</div>
</div>
</div>
{(message || error) && (
<div
className={`mb-6 rounded-2xl px-5 py-4 text-sm font-semibold ${
error
? 'border border-rose-200 bg-rose-50 text-rose-700'
: 'border border-emerald-200 bg-emerald-50 text-emerald-700'
}`}
>
{error || message}
</div>
)}
<div className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-6 flex items-center justify-between gap-4">
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-emerald-600">Novo tenant</p>
<h3 className="text-2xl font-black text-slate-900 dark:text-white">Dados da escola</h3>
</div>
<span className="rounded-2xl bg-emerald-50 px-4 py-2 text-xs font-bold text-emerald-700 ring-1 ring-emerald-100">
school_id ready
</span>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div className="grid gap-4 md:grid-cols-2">
<label className={labelClass}>
Nome oficial *
<input className={inputClass} value={form.name} onChange={handleFieldChange('name')} placeholder="Colégio Esperança" />
</label>
<label className={labelClass}>
NIF
<input className={inputClass} value={form.nif} onChange={handleFieldChange('nif')} placeholder="5000000000" />
</label>
<label className={labelClass}>
Telefone
<input className={inputClass} value={form.phone} onChange={handleFieldChange('phone')} placeholder="+244 923 000 000" />
</label>
<label className={labelClass}>
Email institucional
<input className={inputClass} value={form.email} onChange={handleFieldChange('email')} placeholder="secretaria@escola.co.ao" />
</label>
<label className={labelClass}>
Província
<input className={inputClass} value={form.province} onChange={handleFieldChange('province')} placeholder="Luanda" />
</label>
<label className={labelClass}>
Município
<input className={inputClass} value={form.municipality} onChange={handleFieldChange('municipality')} placeholder="Talatona" />
</label>
<label className={labelClass}>
Estado
<select className={inputClass} value={form.status} onChange={handleFieldChange('status')}>
<option value="setup">Em implantação</option>
<option value="active">Ativa</option>
<option value="suspended">Suspensa</option>
</select>
</label>
<label className={labelClass}>
URL do logotipo
<input className={inputClass} value={form.logoUrl} onChange={handleFieldChange('logoUrl')} placeholder="https://..." />
</label>
</div>
<label className={labelClass}>
Endereço
<textarea className={`${inputClass} min-h-24`} value={form.address} onChange={handleFieldChange('address')} placeholder="Rua, bairro, referência" />
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<BaseButton type="submit" color="info" label={isSaving ? 'A criar...' : 'Criar escola tenant'} disabled={!canCreate || isSaving} className="w-full sm:w-auto" />
<BaseButton type="button" color="whiteDark" outline label="Limpar" onClick={() => setForm(emptyForm)} className="w-full sm:w-auto" />
</div>
{!canCreate && (
<p className="rounded-2xl bg-amber-50 px-4 py-3 text-sm font-semibold text-amber-700">
A sua conta pode consultar escolas, mas não tem permissão CREATE_SCHOOLS para criar novos tenants.
</p>
)}
</form>
</CardBox>
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr] xl:grid-cols-1 2xl:grid-cols-[0.9fr_1.1fr]">
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-5 flex items-center justify-between">
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-blue-600">Lista</p>
<h3 className="text-2xl font-black text-slate-900 dark:text-white">Escolas</h3>
</div>
<BaseButton color="whiteDark" small label="Atualizar" onClick={loadSchools} />
</div>
{isLoading && <p className="rounded-2xl bg-slate-50 p-5 text-sm text-slate-500">A carregar escolas...</p>}
{!isLoading && schools.length === 0 && (
<div className="rounded-3xl border border-dashed border-slate-300 bg-slate-50 p-8 text-center dark:border-slate-700 dark:bg-slate-900">
<p className="text-lg font-black text-slate-800 dark:text-white">Ainda não escolas.</p>
<p className="mt-2 text-sm text-slate-500">Crie o primeiro tenant para iniciar a operação multi-escola.</p>
</div>
)}
<div className="space-y-3">
{schools.map((school) => {
const status = (school.status || 'active') as SchoolStatus
return (
<button
key={school.id}
type="button"
onClick={() => setSelectedId(school.id)}
className={`w-full rounded-3xl border p-4 text-left transition hover:-translate-y-0.5 hover:shadow-lg ${
selectedSchool?.id === school.id
? 'border-emerald-300 bg-emerald-50/80 shadow-lg shadow-emerald-100/60 dark:bg-emerald-950/30'
: 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900'
}`}
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="font-black text-slate-900 dark:text-white">{school.name}</p>
<p className="mt-1 text-sm text-slate-500">{school.province || 'Província por definir'} · {school.municipality || 'Município por definir'}</p>
</div>
<span className={`shrink-0 rounded-full px-3 py-1 text-xs font-bold ring-1 ${statusClasses[status]}`}>
{statusCopy[status]}
</span>
</div>
</button>
)
})}
</div>
</CardBox>
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-6 flex items-center gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-3xl bg-gradient-to-br from-emerald-500 to-blue-700 text-lg font-black text-white">
{selectedSchool?.name?.slice(0, 2).toUpperCase() || 'GB'}
</div>
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-emerald-600">Detalhe</p>
<h3 className="text-2xl font-black text-slate-900 dark:text-white">{selectedSchool?.name || 'Nenhuma escola selecionada'}</h3>
</div>
</div>
{selectedSchool ? (
<div className="space-y-5">
<div className="rounded-3xl bg-slate-50 p-5 dark:bg-slate-900">
<p className="text-sm font-bold text-slate-500">Identificador do tenant</p>
<p className="mt-2 break-all font-mono text-sm text-slate-800 dark:text-slate-200">{selectedSchool.id}</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{[
['NIF', selectedSchool.nif],
['Telefone', selectedSchool.phone],
['Email', selectedSchool.email],
['Província', selectedSchool.province],
['Município', selectedSchool.municipality],
['Endereço', selectedSchool.address],
].map(([label, value]) => (
<div key={label} className="rounded-2xl border border-slate-100 p-4 dark:border-slate-700">
<p className="text-xs font-bold uppercase tracking-[0.18em] text-slate-400">{label}</p>
<p className="mt-2 text-sm font-semibold text-slate-800 dark:text-slate-100">{value || '—'}</p>
</div>
))}
</div>
<div className="rounded-3xl border border-emerald-100 bg-emerald-50 p-5 text-emerald-800">
<p className="font-black">Próximo passo operacional</p>
<p className="mt-2 text-sm leading-6">
Use este tenant para criar utilizadores da escola, associar alunos/professores e iniciar matrículas com dados isolados.
</p>
</div>
</div>
) : (
<div className="rounded-3xl border border-dashed border-slate-300 p-8 text-center text-slate-500">
Selecione uma escola para ver os dados do tenant.
</div>
)}
</CardBox>
</div>
</div>
</SectionMain>
</>
)
}
SchoolsOnboardingPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_SCHOOLS">{page}</LayoutAuthenticated>
}
export default SchoolsOnboardingPage

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/schools/schoolsSlice'; import {setRefetch, uploadCsv} from '../../stores/schools/schoolsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const SchoolsTablesPage = () => { const SchoolsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.schools', 'Schools');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -86,28 +99,28 @@ const SchoolsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Schools')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Schools" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/schools/schools-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/schools/schools-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getSchoolsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getSchoolsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -129,9 +142,9 @@ const SchoolsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react'; import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import { useAppDispatch } from '../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated'; import LayoutAuthenticated from '../layouts/Authenticated';

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/student_guardians/student_guardiansSlice'; import {setRefetch, uploadCsv} from '../../stores/student_guardians/student_guardiansSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const Student_guardiansTablesPage = () => { const Student_guardiansTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.studentGuardians', 'Student_guardians');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -94,28 +107,28 @@ const Student_guardiansTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Student_guardians')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Student_guardians" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/student_guardians/student_guardians-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/student_guardians/student_guardians-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getStudent_guardiansCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getStudent_guardiansCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -137,9 +150,9 @@ const Student_guardiansTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/students/studentsSlice'; import {setRefetch, uploadCsv} from '../../stores/students/studentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const StudentsTablesPage = () => { const StudentsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.students', 'Students');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const StudentsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Students')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Students" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/students/students-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/students/students-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getStudentsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getStudentsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const StudentsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/subjects/subjectsSlice'; import {setRefetch, uploadCsv} from '../../stores/subjects/subjectsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const SubjectsTablesPage = () => { const SubjectsTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.subjects', 'Subjects');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const SubjectsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Subjects')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Subjects" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/subjects/subjects-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/subjects/subjects-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getSubjectsCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getSubjectsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -135,9 +148,9 @@ const SubjectsTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/teachers/teachersSlice'; import {setRefetch, uploadCsv} from '../../stores/teachers/teachersSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const TeachersTablesPage = () => { const TeachersTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.teachers', 'Teachers');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const TeachersTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Teachers')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Teachers" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/teachers/teachers-new'} color='info' label='New Item'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/teachers/teachers-new'} color='info' label={translate('common.actions.newItem', 'New Item')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getTeachersCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getTeachersCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -131,9 +144,9 @@ const TeachersTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice'; import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
const UsersTablesPage = () => { const UsersTablesPage = () => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const entityTitle = translate('entities.users', 'Users');
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const [filterItems, setFilterItems] = useState([]); const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const UsersTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Users')}</title> <title>{getPageTitle(entityTitle)}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox id="usersList" className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox id="usersList" className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>} {hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label={translate('common.actions.addInviteUser', 'Add/Invite User')}/>}
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label={translate('common.actions.filter', 'Filter')}
onClick={addFilter} onClick={addFilter}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsersCSV} /> <BaseButton className={'mr-3'} color='info' label={translate('common.actions.downloadCsv', 'Download CSV')} onClick={getUsersCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
color='info' color='info'
label='Upload CSV' label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
/> />
)} )}
@ -135,9 +148,9 @@ const UsersTablesPage = () => {
</SectionMain> </SectionMain>
<CardBoxModal <CardBoxModal
title='Upload CSV' title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info' buttonColor='info'
buttonLabel={'Confirm'} buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'} // buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive} isActive={isModalActive}
onConfirm={onModalConfirm} onConfirm={onModalConfirm}

View File

@ -8,13 +8,23 @@ import LayoutGuest from '../layouts/Guest';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import axios from 'axios'; import axios from 'axios';
import { useTranslation } from 'react-i18next';
export default function Verify() { export default function Verify() {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = React.useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const router = useRouter(); const router = useRouter();
const { token } = router.query; const { token } = router.query;
const notify = (type, msg) => toast(msg, { type }); const notify = (type, msg) => toast(msg, { type });
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (!token) { if (!token) {
router.push('/login'); router.push('/login');
@ -28,7 +38,7 @@ export default function Verify() {
}).then(verified => { }).then(verified => {
if (verified) { if (verified) {
setLoading(false); setLoading(false);
notify('success', 'Your email was verified'); notify('success', translate('pages.verifyEmail.success', 'Your email was verified'));
} }
}).catch(error => { }).catch(error => {
setLoading(false); setLoading(false);
@ -44,11 +54,11 @@ export default function Verify() {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Verify Email')}</title> <title>{getPageTitle(translate('pages.verifyEmail.pageTitle', 'Verify Email'))}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'> <CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
<p>{loading ? 'Loading...' : ''}</p> <p>{loading ? translate('pages.auth.loading', 'Loading...') : ''}</p>
</CardBox> </CardBox>
</SectionFullScreen> </SectionFullScreen>