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 FileDBApi = require('./file');
const crypto = require('crypto');
const Utils = require('../utils');
@ -25,6 +23,14 @@ module.exports = class SchoolsDBApi {
||
null
,
nif: data.nif || null,
phone: data.phone || null,
email: data.email || null,
province: data.province || null,
municipality: data.municipality || null,
address: data.address || null,
logoUrl: data.logoUrl || null,
status: data.status || 'active',
importHash: data.importHash || null,
createdById: currentUser.id,
@ -55,6 +61,14 @@ module.exports = class SchoolsDBApi {
||
null
,
nif: item.nif || null,
phone: item.phone || null,
email: item.email || null,
province: item.province || null,
municipality: item.municipality || null,
address: item.address || null,
logoUrl: item.logoUrl || null,
status: item.status || 'active',
importHash: item.importHash || null,
createdById: currentUser.id,
@ -74,8 +88,6 @@ module.exports = class SchoolsDBApi {
static async update(id, data, options) {
const currentUser = (options && options.currentUser) || {id: null};
const transaction = (options && options.transaction) || undefined;
const globalAccess = currentUser.app_role?.globalAccess;
const schools = await db.schools.findByPk(id, {}, {transaction});
@ -84,6 +96,14 @@ module.exports = class SchoolsDBApi {
const updatePayload = {};
if (data.name !== undefined) updatePayload.name = data.name;
if (data.nif !== undefined) updatePayload.nif = data.nif;
if (data.phone !== undefined) updatePayload.phone = data.phone;
if (data.email !== undefined) updatePayload.email = data.email;
if (data.province !== undefined) updatePayload.province = data.province;
if (data.municipality !== undefined) updatePayload.municipality = data.municipality;
if (data.address !== undefined) updatePayload.address = data.address;
if (data.logoUrl !== undefined) updatePayload.logoUrl = data.logoUrl;
if (data.status !== undefined) updatePayload.status = data.status;
updatePayload.updatedById = currentUser.id;
@ -286,10 +306,6 @@ module.exports = class SchoolsDBApi {
offset = currentPage * limit;
const orderBy = null;
const transaction = (options && options.transaction) || undefined;
let include = [
@ -316,6 +332,24 @@ module.exports = class SchoolsDBApi {
};
}
if (filter.status) {
where = {
...where,
status: filter.status,
};
}
if (filter.province) {
where = {
...where,
[Op.and]: Utils.ilike(
'schools',
'province',
filter.province,
),
};
}

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) {
const schools = sequelize.define(
'schools',
@ -19,6 +13,63 @@ name: {
},
nif: {
type: DataTypes.TEXT,
},
phone: {
type: DataTypes.TEXT,
},
email: {
type: DataTypes.TEXT,
},
province: {
type: DataTypes.TEXT,
},
municipality: {
type: DataTypes.TEXT,
},
address: {
type: DataTypes.TEXT,
},
logoUrl: {
type: DataTypes.TEXT,
},
status: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: 'active',
},
importHash: {

View File

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

View File

@ -3,20 +3,50 @@ const SchoolsDBApi = require('../db/api/schools');
const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream');
const SCHOOL_STATUSES = new Set(['active', 'setup', 'suspended']);
function cleanString(value) {
if (value === undefined || value === null) return null;
const text = String(value).trim();
return text || null;
}
function normalizeSchoolData(data = {}) {
const normalized = {
name: cleanString(data.name),
nif: cleanString(data.nif),
phone: cleanString(data.phone),
email: cleanString(data.email),
province: cleanString(data.province),
municipality: cleanString(data.municipality),
address: cleanString(data.address),
logoUrl: cleanString(data.logoUrl),
status: cleanString(data.status) || 'active',
};
if (!normalized.name) {
throw new ValidationError('schoolsNameRequired');
}
if (normalized.email && !normalized.email.includes('@')) {
throw new ValidationError('schoolsEmailInvalid');
}
if (!SCHOOL_STATUSES.has(normalized.status)) {
throw new ValidationError('schoolsStatusInvalid');
}
return normalized;
}
module.exports = class SchoolsService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await SchoolsDBApi.create(
data,
normalizeSchoolData(data),
{
currentUser,
transaction,
@ -28,9 +58,9 @@ module.exports = class SchoolsService {
await transaction.rollback();
throw error;
}
};
}
static async bulkImport(req, res, sendInvitationEmails = true, host) {
static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction();
try {
@ -81,7 +111,7 @@ module.exports = class SchoolsService {
const updatedSchools = await SchoolsDBApi.update(
id,
data,
normalizeSchoolData(data),
{
currentUser,
transaction,
@ -95,7 +125,7 @@ module.exports = class SchoolsService {
await transaction.rollback();
throw error;
}
};
}
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();

View File

@ -4,34 +4,66 @@
"pageTitle": "Dashboard",
"overview": "Overview",
"loadingWidgets": "Loading widgets...",
"loading": "Loading..."
"loading": "Loading...",
"roleWidgets": "{{role}}'s widgets"
},
"login": {
"pageTitle": "Login",
"form": {
"loginLabel": "Login",
"loginHelp": "Please enter your login",
"passwordLabel": "Password",
"passwordHelp": "Please enter your password",
"remember": "Remember",
"forgotPassword": "Forgot password?",
"loginButton": "Login",
"loading": "Loading...",
"noAccountYet": "Dont have an account yet?",
"newAccount": "New Account"
"loginLabel": "Login",
"loginHelp": "Please enter your login",
"passwordLabel": "Password",
"passwordHelp": "Please enter your password",
"remember": "Remember",
"forgotPassword": "Forgot password?",
"loginButton": "Login",
"loading": "Loading...",
"noAccountYet": "Dont have an account yet?",
"newAccount": "New Account"
},
"pexels": {
"photoCredit": "Photo by {{photographer}} on Pexels",
"videoCredit": "Video by {{name}} on Pexels",
"videoUnsupported": "Your browser does not support the video tag."
},
"footer": {
"copyright": "© {{year}} {{title}}. All rights reserved",
"privacy": "Privacy Policy"
}
"privacy": "Privacy Policy"
},
"sampleCredentialsSuperAdmin": "Use to login as Super Admin:",
"sampleCredentialsAdmin": "Use to login as Admin:",
"sampleCredentialsUser": "Use to login as User:"
},
"auth": {
"emailLabel": "Email",
"emailHelp": "Please enter your email",
"passwordLabel": "Password",
"passwordHelp": "Please enter your password",
"confirmPasswordLabel": "Confirm Password",
"confirmPasswordHelp": "Please confirm your password",
"loading": "Loading...",
"login": "Login",
"checkEmailVerification": "Please check your email for verification link",
"genericError": "Something was wrong. Try again"
},
"register": {
"pageTitle": "Register",
"organization": "Organization",
"selectOrganization": "Select organization...",
"submit": "Register"
},
"forgot": {
"pageTitle": "Forgot password",
"submit": "Submit"
},
"verifyEmail": {
"pageTitle": "Verify Email",
"success": "Your email was verified"
},
"password": {
"setTitle": "Set Password",
"resetTitle": "Reset Password",
"enterNewPassword": "Enter your new password"
}
},
"components": {
@ -41,12 +73,114 @@
"settingsTitle": "Widget Creator Settings",
"settingsDescription": "What role are we showing and creating widgets for?",
"doneButton": "Done",
"loading": "Loading..."
"loading": "Loading...",
"creationError": "Error with widget creation"
},
"search": {
"placeholder": "Search",
"required": "Required",
"minLength": "Minimum length: {{count}} characters"
}
},
"navigation": {
"aside": {
"dashboard": "Dashboard",
"users": "Users",
"roles": "Roles",
"permissions": "Permissions",
"schoolOnboarding": "School Onboarding",
"academicMvp": "Academic MVP",
"schoolsCrud": "Schools CRUD",
"students": "Students",
"guardians": "Guardians",
"studentGuardians": "Student guardians",
"teachers": "Teachers",
"courses": "Courses",
"grades": "Grades",
"classes": "Classes",
"subjects": "Subjects",
"enrollments": "Enrollments",
"assessments": "Assessments",
"attendance": "Attendance",
"invoices": "Invoices",
"payments": "Payments",
"employees": "Employees",
"products": "Products",
"books": "Books",
"bookLoans": "Book loans",
"profile": "Profile",
"swaggerApi": "Swagger API"
},
"nav": {
"myProfile": "My Profile",
"logOut": "Log Out",
"lightDark": "Light/Dark"
}
},
"layout": {
"footer": {
"madeWith": "Hand-crafted & Made with ❤️"
}
},
"entities": {
"users": "Users",
"roles": "Roles",
"permissions": "Permissions",
"schools": "Schools",
"students": "Students",
"guardians": "Guardians",
"studentGuardians": "Student guardians",
"teachers": "Teachers",
"courses": "Courses",
"grades": "Grades",
"classes": "Classes",
"subjects": "Subjects",
"enrollments": "Enrollments",
"assessments": "Assessments",
"attendance": "Attendance",
"invoices": "Invoices",
"payments": "Payments",
"employees": "Employees",
"products": "Products",
"books": "Books",
"bookLoans": "Book loans"
},
"common": {
"actions": {
"newItem": "New Item",
"addInviteUser": "Add/Invite User",
"filter": "Filter",
"downloadCsv": "Download CSV",
"uploadCsv": "Upload CSV",
"confirm": "Confirm",
"cancel": "Cancel",
"delete": "Delete",
"apply": "Apply",
"deleting": "Deleting...",
"view": "View",
"edit": "Edit"
},
"upload": {
"allowedFormats": "Allowed formats: {{formats}}",
"clickToUpload": "Click to upload",
"dragAndDrop": "or drag and drop"
},
"filters": {
"value": "Value",
"selectValue": "Select Value",
"from": "From",
"to": "To",
"contains": "Contains",
"contained": "Contained",
"action": "Action"
},
"confirm": {
"title": "Please confirm",
"deleteItem": "Are you sure you want to delete this item?"
},
"table": {
"row": "Row",
"rows": "Rows"
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import BaseIcon from './BaseIcon';
import { mdiFileUploadOutline } from '@mdi/js';
@ -9,10 +10,19 @@ type Props = {
};
const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string, options = {}): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback, ...options })) : fallback
);
const [highlight, setHighlight] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const fileInput = React.createRef<HTMLInputElement>();
useEffect(() => {
setIsTranslationMounted(true);
}, []);
useEffect(() => {
if (!file && fileInput) fileInput.current.value = '';
}, [file, fileInput]);
@ -26,7 +36,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
setFile(newFile);
setErrorMessage('');
} else {
setErrorMessage(`Allowed formats: ${formats}`);
setErrorMessage(translate('common.upload.allowedFormats', `Allowed formats: ${formats}`, { formats }));
}
}
}
@ -97,8 +107,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
) : (
<>
<p className='mb-2 text-sm text-gray-500 dark:text-gray-400'>
<span className='font-semibold'>Click to upload</span> or drag
and drop
<span className='font-semibold'>{translate('common.upload.clickToUpload', 'Click to upload')}</span> {translate('common.upload.dragAndDrop', 'or drag and drop')}
</p>
{formats && (
<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 { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10
const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }) => {
const { t } = useTranslation('common');
const [isTranslationMounted, setIsTranslationMounted] = useState(false);
const translate = (key: string, fallback: string): string => (
isTranslationMounted ? String(t(key, { defaultValue: fallback })) : fallback
);
useEffect(() => {
setIsTranslationMounted(true);
}, []);
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
return (
<div key={filterItem.id} className="flex mb-4">
<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
className={controlClasses}
name='selectedField'
@ -300,9 +311,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
</div>
<div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
<Field
className={controlClasses}
name="filterValue"
@ -311,7 +320,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
)?.number ? (
<div className="flex flex-row 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
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<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
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder={translate('common.filters.to', 'to')}
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
)?.date ? (
<div className='flex flex-row 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
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
/>
</div>
<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
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder={translate('common.filters.to', 'to')}
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
</div>
) : (
<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
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
</div>
)}
<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
className="my-2"
type='reset'
color='danger'
label='Delete'
label={translate('common.actions.delete', 'Delete')}
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +420,13 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset}
/>
</div>
@ -429,14 +436,14 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
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>
@ -450,7 +457,7 @@ const TableSampleEmployees = ({ filterItems, setFilterItems, filters, showGrid }
<BaseButton
className='me-4'
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)}
/>,
document.getElementById('delete-rows-button'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import BaseIcon from './BaseIcon';
import {
@ -31,6 +31,16 @@ const ListActionsPopover = ({
pathEdit,
pathView,
}: 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 handleClick = (event) => {
setAnchorEl(event.currentTarget);
@ -81,7 +91,7 @@ const ListActionsPopover = ({
href={linkView}
sx={{ justifyContent: "start" }}
>
View
{translate('common.actions.view', 'View')}
</Button>
{hasUpdatePermission && (
<Button
@ -90,7 +100,7 @@ const ListActionsPopover = ({
href={linkEdit}
sx={{ justifyContent: "start" }}
>
Edit
{translate('common.actions.edit', 'Edit')}
</Button>
)}
{hasUpdatePermission && (
@ -103,7 +113,7 @@ const ListActionsPopover = ({
}}
sx={{ justifyContent: "start" }}
>
Delete
{translate('common.actions.delete', 'Delete')}
</Button>
)}
</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 { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@ -12,6 +11,7 @@ import { setDarkMode } from '../stores/styleSlice'
import { logoutUser } from '../stores/authSlice'
import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside";
import { useTranslation } from 'react-i18next';
type Props = {
item: MenuNavBarItem
@ -33,6 +33,12 @@ export default function NavBarItem({ item }: Props) {
const userName = `${currentUser?.firstName ? currentUser?.firstName : ""} ${currentUser?.lastName ? currentUser?.lastName : ""}`;
const [isDropdownActive, setIsDropdownActive] = useState(false)
const [isMounted, setIsMounted] = useState(false)
const { t } = useTranslation('common')
useEffect(() => {
setIsMounted(true)
}, [])
useEffect(() => {
return () => setIsDropdownActive(false);
@ -47,7 +53,11 @@ export default function NavBarItem({ item }: Props) {
item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '',
].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 = () => {
if (item.menu) {
@ -64,21 +74,16 @@ export default function NavBarItem({ item }: Props) {
}
}
const getItemId = (label) => {
switch (label) {
case 'Light/Dark':
return 'themeToggle';
case 'Log out':
return 'logout';
default:
return undefined;
}
const getItemId = () => {
if (item.isToggleLightDark) return 'themeToggle';
if (item.isLogout) return 'logout';
return undefined;
};
const NavBarItemComponentContents = (
<>
<div
id={getItemId(itemLabel)}
id={getItemId()}
className={`flex items-center ${
item.menu
? '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 { passwordReset } from '../stores/authSlice';
import {useAppDispatch} from '../stores/hooks';
import { useTranslation } from 'react-i18next';
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 [isInvitation, setIsInvitation] = React.useState(false);
const router = useRouter();
@ -24,6 +30,10 @@ export default function PasswordSetOrReset() {
const dispatch = useAppDispatch();
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => {
if (invitation) {
setIsInvitation(true);
@ -49,16 +59,16 @@ export default function PasswordSetOrReset() {
return (
<>
<Head>
{isInvitation && <title>{getPageTitle('Set Password')}</title>}
{!isInvitation && <title>{getPageTitle('Reset Password')}</title>}
{isInvitation && <title>{getPageTitle(translate('pages.password.setTitle', 'Set Password'))}</title>}
{!isInvitation && <title>{getPageTitle(translate('pages.password.resetTitle', 'Reset Password'))}</title>}
</Head>
<SectionFullScreen bg='violet'>
<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'>
{isInvitation && <p className='text-xl mb-2'>Set Password</p>}
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>}
<p className='text-base mb-4'>Enter your new password</p>
{isInvitation && <p className='text-xl mb-2'>{translate('pages.password.setTitle', 'Set Password')}</p>}
{!isInvitation && <p className='text-xl mb-2'>{translate('pages.password.resetTitle', 'Reset Password')}</p>}
<p className='text-base mb-4'>{translate('pages.password.enterNewPassword', 'Enter your new password')}</p>
<Formik
initialValues={{
@ -74,7 +84,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='password'
placeholder='Password'
placeholder={translate('pages.auth.passwordLabel', 'Password')}
/>
</FormField>
<FormField
@ -82,7 +92,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='confirm'
placeholder='Confirm Password'
placeholder={translate('pages.auth.confirmPasswordLabel', 'Confirm Password')}
/>
</FormField>
@ -93,10 +103,10 @@ export default function PasswordSetOrReset() {
disabled={loading}
label={
loading
? 'Loading...'
? translate('pages.auth.loading', 'Loading...')
: isInvitation
? 'Set Password'
: 'Reset Password'
? translate('pages.password.setTitle', 'Set Password')
: translate('pages.password.resetTitle', 'Reset Password')
}
color='info'
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,9 +43,9 @@ export const loadColumns = async (
{
field: 'name',
headerName: 'Name',
headerName: 'Escola',
flex: 1,
minWidth: 120,
minWidth: 180,
filterable: false,
headerClassName: 'datagrid--header',
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',

View File

@ -1,19 +1,26 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { useAppSelector } from '../stores/hooks';
import { useTranslation } from 'react-i18next';
const Search = () => {
const router = useRouter();
const { t } = useTranslation('common');
const [isMounted, setIsMounted] = useState(false);
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
useEffect(() => {
setIsMounted(true);
}, []);
const validateSearch = (value) => {
let error;
if (!value) {
error = 'Required';
error = isMounted ? t('components.search.required') : 'Required';
} else if (value.length < 2) {
error = 'Minimum length: 2 characters';
error = isMounted ? t('components.search.minLength', { count: 2 }) : 'Minimum length: 2 characters';
}
return error;
};
@ -36,7 +43,7 @@ const Search = () => {
id='search'
name='search'
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`}
/>
{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 { createPortal } from 'react-dom';
import { ToastContainer, toast } from 'react-toastify';
@ -22,6 +23,16 @@ import {dataGridStyles} from "../../styles";
const perPage = 10
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 dispatch = useAppDispatch();
@ -277,7 +288,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
return (
<div key={filterItem.id} className="flex mb-4">
<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
className={controlClasses}
name='selectedField'
@ -300,9 +311,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
</div>
<div className="text-gray-500 font-bold">{translate('common.filters.value', 'Value')}</div>
<Field
className={controlClasses}
name="filterValue"
@ -311,7 +320,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">{translate('common.filters.selectValue', 'Select Value')}</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +335,22 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
)?.number ? (
<div className="flex flex-row 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
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<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
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder={translate('common.filters.to', 'to')}
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -355,13 +364,11 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
)?.date ? (
<div className='flex flex-row 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
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder={translate('common.filters.from', 'From')}
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +376,11 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
/>
</div>
<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
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder={translate('common.filters.to', 'to')}
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +390,11 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
</div>
) : (
<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
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder={translate('common.filters.contained', 'Contained')}
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +402,12 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
</div>
)}
<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
className="my-2"
type='reset'
color='danger'
label='Delete'
label={translate('common.actions.delete', 'Delete')}
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +420,13 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label={translate('common.actions.apply', 'Apply')}
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label={translate('common.actions.cancel', 'Cancel')}
onClick={handleReset}
/>
</div>
@ -429,14 +436,14 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title={translate('common.confirm.title', 'Please confirm')}
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? translate('common.actions.deleting', 'Deleting...') : translate('common.actions.confirm', 'Confirm')}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
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>
@ -450,7 +457,7 @@ const TableSampleStudent_guardians = ({ filterItems, setFilterItems, filters, sh
<BaseButton
className='me-4'
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)}
/>,
document.getElementById('delete-rows-button'),

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import { Field, Form, Formik } from 'formik';
import { ToastContainer, toast } from 'react-toastify';
import FormField from '../FormField';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
aiPrompt,
setErrorNotification,
@ -24,10 +25,19 @@ export const WidgetCreator = ({
widgetsRole,
}) => {
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 { notify: openAiNotify } = useAppSelector((state) => state.openAi);
const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' });
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => {
if (openAiNotify.showNotification) {
notify(openAiNotify.typeNotification, openAiNotify.textNotification);
@ -67,7 +77,7 @@ export const WidgetCreator = ({
const errorMessage =
responcePayload.data?.error?.message || error?.message;
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>
<FormField
label='Create Chart or Widget'
label={translate('components.widgetCreator.title', 'Create Chart or Widget')}
help={
isFetchingQuery ?
'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.loading', 'Loading...') :
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} />
@ -110,14 +120,14 @@ export const WidgetCreator = ({
>
{({ submitForm }) => (
<CardBoxModal
title='Widget Creator Settings'
title={translate('components.widgetCreator.settingsTitle', 'Widget Creator Settings')}
buttonColor='info'
buttonLabel='Done'
buttonLabel={translate('components.widgetCreator.doneButton', 'Done')}
isActive={isModalOpen}
onConfirm={submitForm}
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>
<FormField>

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import React, { ReactNode } from 'react'
import { useAppSelector } from '../stores/hooks'
import LanguageSwitcher from '../components/LanguageSwitcher'
type Props = {
children: ReactNode
@ -11,7 +12,12 @@ export default function LayoutGuest({ children }: Props) {
return (
<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>
)
}

View File

@ -6,11 +6,13 @@ const menuAside: MenuAsideItem[] = [
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
labelKey: 'navigation.aside.dashboard',
},
{
href: '/users/users-list',
label: 'Users',
labelKey: 'navigation.aside.users',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
@ -19,6 +21,7 @@ const menuAside: MenuAsideItem[] = [
{
href: '/roles/roles-list',
label: 'Roles',
labelKey: 'navigation.aside.roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
@ -27,14 +30,32 @@ const menuAside: MenuAsideItem[] = [
{
href: '/permissions/permissions-list',
label: 'Permissions',
labelKey: 'navigation.aside.permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
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',
label: 'Schools',
label: 'Schools CRUD',
labelKey: 'navigation.aside.schoolsCrud',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ?? icon.mdiTable,
@ -43,6 +64,7 @@ const menuAside: MenuAsideItem[] = [
{
href: '/students/students-list',
label: 'Students',
labelKey: 'navigation.aside.students',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Guardians',
labelKey: 'navigation.aside.guardians',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Student guardians',
labelKey: 'navigation.aside.studentGuardians',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Teachers',
labelKey: 'navigation.aside.teachers',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Courses',
labelKey: 'navigation.aside.courses',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Grades',
labelKey: 'navigation.aside.grades',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Classes',
labelKey: 'navigation.aside.classes',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Subjects',
labelKey: 'navigation.aside.subjects',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Enrollments',
labelKey: 'navigation.aside.enrollments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Assessments',
labelKey: 'navigation.aside.assessments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Attendance',
labelKey: 'navigation.aside.attendance',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Invoices',
labelKey: 'navigation.aside.invoices',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Payments',
labelKey: 'navigation.aside.payments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Employees',
labelKey: 'navigation.aside.employees',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Products',
labelKey: 'navigation.aside.products',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Books',
labelKey: 'navigation.aside.books',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
label: 'Book loans',
labelKey: 'navigation.aside.bookLoans',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBookClock' in icon ? icon['mdiBookClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -179,6 +217,7 @@ const menuAside: MenuAsideItem[] = [
{
href: '/profile',
label: 'Profile',
labelKey: 'navigation.aside.profile',
icon: icon.mdiAccountCircle,
},
@ -187,6 +226,7 @@ const menuAside: MenuAsideItem[] = [
href: '/api-docs',
target: '_blank',
label: 'Swagger API',
labelKey: 'navigation.aside.swaggerApi',
icon: icon.mdiFileCode,
permissions: 'READ_API_DOCS'
},

View File

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

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 { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -100,28 +113,28 @@ const AssessmentsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Assessments')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Assessments" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -143,9 +156,9 @@ const AssessmentsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/attendance/attendanceSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const AttendanceTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Attendance')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Attendance" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,9 +148,9 @@ const AttendanceTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/book_loans/book_loansSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -96,28 +109,28 @@ const Book_loansTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Book_loans')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Book_loans" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -143,9 +156,9 @@ const Book_loansTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/books/booksSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const BooksTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Books')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Books" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const BooksTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/classes/classesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const ClassesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Classes')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Classes" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,9 +148,9 @@ const ClassesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/courses/coursesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const CoursesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Courses')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Courses" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const CoursesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

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

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/employees/employeesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const EmployeesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Employees')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Employees" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const EmployeesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/enrollments/enrollmentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -96,28 +109,28 @@ const EnrollmentsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Enrollments')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Enrollments" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -139,9 +152,9 @@ const EnrollmentsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -13,32 +13,42 @@ import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import axios from "axios";
import { useTranslation } from 'react-i18next';
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 router = useRouter();
const notify = (type, msg) => toast( msg, {type});
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
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 () => {
await router.push('/login')
}, 3000)
} catch (error) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
notify('error', translate('pages.auth.genericError', 'Something was wrong. Try again'))
}
};
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle(translate('pages.forgot.pageTitle', 'Forgot password'))}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -50,7 +60,7 @@ export default function Forgot() {
onSubmit={(values) => handleSubmit(values)}
>
<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' />
</FormField>
@ -59,12 +69,12 @@ export default function Forgot() {
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Submit' }
label={loading ? translate('pages.auth.loading', 'Loading...') : translate('pages.forgot.submit', 'Submit')}
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
label={translate('pages.auth.login', 'Login')}
color='info'
/>
</BaseButtons>

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/grades/gradesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const GradesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Grades')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Grades" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,9 +148,9 @@ const GradesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/guardians/guardiansSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const GuardiansTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Guardians')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Guardians" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const GuardiansTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
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 type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
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';
import BaseButton from '../components/BaseButton'
import BaseIcon from '../components/BaseIcon'
import LayoutGuest from '../layouts/Guest'
import { getPageTitle } from '../config'
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() {
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 (
<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>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('GB-GESTÃO ESCOLAR S.A')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your GB Gestao Escolar SA app!"/>
<div className="space-y-3">
<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>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<main className="min-h-screen overflow-hidden bg-[#F8FAFC] text-slate-950">
<nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
<Link href="/" className="flex items-center gap-3">
<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">
GB
</span>
<span>
<span className="block text-sm font-black tracking-tight">GB-GESTÃO ESCOLAR</span>
<span className="block text-xs font-semibold text-slate-500">SaaS multi-escola</span>
</span>
</Link>
<div className="flex items-center gap-3">
<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">
Login
</Link>
<BaseButton href="/login" color="info" label="Admin interface" roundedFull />
</div>
</nav>
<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">
<div className="absolute left-1/2 top-24 -z-0 h-72 w-72 rounded-full bg-emerald-300/30 blur-3xl" />
<div className="relative z-10">
<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>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<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>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<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>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</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>
);
<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 className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{modules.map((module, index) => (
<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">
<div className="mb-8 flex items-center justify-between">
<span className="text-sm font-black text-emerald-600 group-hover:text-emerald-300">0{index + 1}</span>
<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>
<section className="px-6 py-16">
<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>
<p className="text-sm font-bold uppercase tracking-[0.24em] text-emerald-200">Admin interface</p>
<h2 className="mt-2 text-3xl font-black">Entre para criar a primeira escola tenant.</h2>
</div>
<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">
Abrir painel <BaseIcon path={mdiArrowRight} size={18} />
</Link>
</div>
</section>
<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) {
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 { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const InvoicesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Invoices')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Invoices" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -137,9 +150,9 @@ const InvoicesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

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

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/payments/paymentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const PaymentsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Payments')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Payments" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,9 +148,9 @@ const PaymentsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/permissions/permissionsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -86,28 +99,28 @@ const PermissionsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Permissions')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Permissions" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -129,9 +142,9 @@ const PermissionsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/products/productsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const ProductsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Products')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const ProductsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -18,8 +18,14 @@ import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from "axios";
import { useTranslation } from 'react-i18next';
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 router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
@ -48,6 +54,9 @@ export default function Register() {
label: org.name
}));
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
const handleSubmit = async (value) => {
setLoading(true)
@ -58,18 +67,18 @@ export default function Register() {
const { data: response } = await axios.post('/auth/signup',formData);
await router.push('/login')
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) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
notify('error', translate('pages.auth.genericError', 'Something was wrong. Try again'))
}
};
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle(translate('pages.register.pageTitle', 'Register'))}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -84,7 +93,7 @@ export default function Register() {
>
<Form>
<label className="block font-bold mb-2" >Organization</label>
<label className="block font-bold mb-2" >{translate('pages.register.organization', 'Organization')}</label>
<Select
classNames={{
@ -93,16 +102,16 @@ export default function Register() {
value={selectedOrganization}
onChange={setSelectedOrganization}
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' />
</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' />
</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' />
</FormField>
@ -111,12 +120,12 @@ export default function Register() {
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
label={loading ? translate('pages.auth.loading', 'Loading...') : translate('pages.register.submit', 'Register')}
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
label={translate('pages.auth.login', 'Login')}
color='info'
/>
</BaseButtons>

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/roles/rolesSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -86,28 +99,28 @@ const RolesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Roles')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Roles" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -129,9 +142,9 @@ const RolesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
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 { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -86,28 +99,28 @@ const SchoolsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Schools')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Schools" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -129,9 +142,9 @@ const SchoolsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
import { useAppDispatch } from '../stores/hooks';
import { useAppSelector } from '../stores/hooks';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
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 { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -94,28 +107,28 @@ const Student_guardiansTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Student_guardians')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Student_guardians" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -137,9 +150,9 @@ const Student_guardiansTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/students/studentsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const StudentsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Students')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Students" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const StudentsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/subjects/subjectsSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const SubjectsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Subjects')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Subjects" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,9 +148,9 @@ const SubjectsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/teachers/teachersSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -88,28 +101,28 @@ const TeachersTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Teachers')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Teachers" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -131,9 +144,9 @@ const TeachersTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -17,11 +17,24 @@ import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
import { useTranslation } from 'react-i18next';
import {hasPermission} from "../../helpers/userPermissions";
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 [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
@ -92,28 +105,28 @@ const UsersTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Users')}</title>
<title>{getPageTitle(entityTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={entityTitle} main>
{''}
</SectionTitleLineWithButton>
<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
className={'mr-3'}
color='info'
label='Filter'
label={translate('common.actions.filter', 'Filter')}
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 && (
<BaseButton
color='info'
label='Upload CSV'
label={translate('common.actions.uploadCsv', 'Upload CSV')}
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,9 +148,9 @@ const UsersTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title={translate('common.actions.uploadCsv', 'Upload CSV')}
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={translate('common.actions.confirm', 'Confirm')}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -8,13 +8,23 @@ import LayoutGuest from '../layouts/Guest';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import axios from 'axios';
import { useTranslation } from 'react-i18next';
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 router = useRouter();
const { token } = router.query;
const notify = (type, msg) => toast(msg, { type });
React.useEffect(() => {
setIsTranslationMounted(true);
}, []);
React.useEffect(() => {
if (!token) {
router.push('/login');
@ -28,7 +38,7 @@ export default function Verify() {
}).then(verified => {
if (verified) {
setLoading(false);
notify('success', 'Your email was verified');
notify('success', translate('pages.verifyEmail.success', 'Your email was verified'));
}
}).catch(error => {
setLoading(false);
@ -44,11 +54,11 @@ export default function Verify() {
return (
<>
<Head>
<title>{getPageTitle('Verify Email')}</title>
<title>{getPageTitle(translate('pages.verifyEmail.pageTitle', 'Verify Email'))}</title>
</Head>
<SectionFullScreen bg='violet'>
<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>
</SectionFullScreen>