diff --git a/src/modules/school-management/routes.ts b/src/modules/school-management/routes.ts index a78bfb7..d747771 100644 --- a/src/modules/school-management/routes.ts +++ b/src/modules/school-management/routes.ts @@ -1,4 +1,4 @@ -import { create, person, school, albums } from 'ionicons/icons' +import { create, person, school, albums, business } from 'ionicons/icons' import DashboardFunction from './views/DashboardFunction.vue' import DashboardClassroom from './views/classroom/DashboardClassroom.vue' import RegisterClassroom from './views/classroom/RegisterClassroom.vue' @@ -7,6 +7,9 @@ import ClassroomDetailsDesktop from './views/classroom/ClassroomDetailsDesktop.v import FunctionDetailsDesktop from './views/FunctionDetailsDesktop.vue' import RegisterFunction from './views/RegisterFunction.vue' // import RegisterSchool from './views/RegisterSchool.vue' +import DashboardInstitution from './views/institution/DashboardInstitution.vue' +import RegisterInstitution from './views/institution/RegisterInstitution.vue' +import InstitutionDetailsDesktop from './views/institution/InstitutionDetailsDesktop.vue' const routes = [ // { @@ -233,6 +236,43 @@ const routes = [ requiredRole: ['ADMIN', 'GESTORMUNICIPAL', 'GESTORESCOLAR'], }, }, + { + path: '/Institution/list', + name: 'DashboardInstitution', + component: DashboardInstitution, + meta: { + moduleName: 'Schools', + moduleIcon: school, + icon: business, + name: 'Instituições', + order: 19, + requiredRole: ['ADMIN', 'GESTORMUNICIPAL', 'GESTORESCOLAR'], + }, + }, + { + path: '/Institution/register/:id?', + name: 'RegisterInstitution', + component: RegisterInstitution, + meta: { + moduleName: 'Schools', + icon: create, + name: 'Registrar instituição', + order: 20, + requiredRole: ['ADMIN', 'GESTORMUNICIPAL', 'GESTORESCOLAR'], + }, + }, + { + path: '/Institution/details/:id', + name: 'InstitutionDetails', + component: InstitutionDetailsDesktop, + meta: { + moduleName: 'Schools', + icon: business, + name: 'Detalhes da instituição', + order: 21, + requiredRole: ['ADMIN', 'GESTORMUNICIPAL', 'GESTORESCOLAR'], + }, + }, ] export default routes diff --git a/src/modules/school-management/services/InstitutionService.ts b/src/modules/school-management/services/InstitutionService.ts index 8f102bc..978b584 100644 --- a/src/modules/school-management/services/InstitutionService.ts +++ b/src/modules/school-management/services/InstitutionService.ts @@ -1,11 +1,233 @@ import BaseService from '@/services/BaseService' +import type { Institution } from '@prisma/client' +import errorHandler from '@/utils/error-handler' -const table = 'institution' as const +interface InstitutionToSave { + id?: string + name: string + address?: string | null + city?: string | null + state?: string | null + postalCode?: string | null + phone?: string | null + email?: string | null + metadata?: any + tenantId?: string | null + userCreated?: string | null +} -type InstitutionTable = typeof table +const table = 'institution' as const -export default class InstitutionService extends BaseService { +export default class InstitutionService extends BaseService { constructor() { - super(table) // Passando o nome da tabela para a classe base + super(table) + } + + async getInstitutions() { + const { data, error } = await this.client + .from(table) + .select('*') + .is('deletedAt', null) + .order('name') + + if (error) { + errorHandler(error, 'Erro ao buscar instituições') + return [] + } + + return data as Institution[] + } + + async getInstitutionById(id: string) { + if (!id) { + return null + } + + const { data, error } = await this.client + .from(table) + .select('*') + .eq('id', id) + .is('deletedAt', null) + .maybeSingle() + + if (error) { + errorHandler(error, 'Erro ao buscar instituição por ID') + return null + } + + return data as Institution | null + } + + async getInstitutionByName(name: string) { + if (!name) { + return null + } + + const { data, error } = await this.client + .from(table) + .select('*') + .eq('name', name) + .is('deletedAt', null) + .maybeSingle() + + if (error) { + errorHandler(error, 'Erro ao buscar instituição por nome') + return null + } + + return data as Institution | null + } + + async getInstitutionByNameInsensitive(name: string, excludeId?: string) { + if (!name) { + return null + } + + const { data, error } = await this.client + .from(table) + .select('*') + .ilike('name', name) + .is('deletedAt', null) + + if (error) { + errorHandler(error, 'Erro ao buscar instituição por nome') + return null + } + + const filtered = excludeId ? data.filter(item => item.id !== excludeId) : data + return filtered.length > 0 ? filtered[0] : null + } + + async upsertInstitution(institution: InstitutionToSave) { + const now = new Date().toISOString() + + // Verifica se já existe uma instituição com o mesmo nome (ignorando case) + const existingWithSameName = await this.getInstitutionByNameInsensitive( + institution.name, + institution.id, + ) + + if (existingWithSameName) { + throw new Error('Nome de instituição já existente') + } + + let existing = null + if (institution.id) { + const { data, error: fetchError } = await this.client + .from(table) + .select('*') + .eq('id', institution.id) + .is('deletedAt', null) + .maybeSingle() + + if (fetchError) { + errorHandler(fetchError, 'Erro ao buscar instituição existente por ID') + } + existing = data + } + + const payload: Partial & { id?: string } = { + name: institution.name, + address: institution.address || null, + city: institution.city || null, + state: institution.state || null, + postalCode: institution.postalCode || null, + phone: institution.phone || null, + email: institution.email || null, + metadata: institution.metadata || null, + tenantId: institution.tenantId || null, + userCreated: institution.userCreated || null, + deletedAt: null, + updatedAt: now, + ...(existing?.id && { id: existing.id }), + } + + const { data, error } = await this.client + .from(table) + .upsert(payload) + .select() + + if (error) { + errorHandler(error, 'Erro ao inserir/atualizar instituição') + } + return data as Institution[] + } + + async softDeleteInstitution(institutionId: string, userId?: string) { + if (!institutionId) { + errorHandler({ message: 'ID da instituição não fornecido' }, 'Erro ao apagar instituição') + return [] + } + + // Verificar escolas associadas + const { data: schools, error: schoolsError } = await this.client + .from('school') + .select('id') + .eq('institutionId', institutionId) + .is('deletedAt', null) + + if (schoolsError) { + errorHandler(schoolsError, 'Erro ao verificar escolas associadas') + return [] + } + + if (schools && schools.length > 0) { + throw new Error('Não é possível excluir: existem escolas vinculadas a essa instituição.') + } + + // Verificar cursos associados + const { data: courses, error: coursesError } = await this.client + .from('course') + .select('id') + .eq('institutionId', institutionId) + .is('deletedAt', null) + + if (coursesError) { + errorHandler(coursesError, 'Erro ao verificar cursos associados') + return [] + } + + if (courses && courses.length > 0) { + throw new Error('Não é possível excluir: existem cursos vinculados a essa instituição.') + } + + const now = new Date().toISOString() + const updateFields: Record = { + deletedAt: now, + updatedAt: now, + } + + if (userId) { + updateFields.updatedBy = userId + } + + const { data, error } = await this.client + .from(table) + .update(updateFields) + .eq('id', institutionId) + .select() + + if (error) { + errorHandler(error, 'Erro ao excluir instituição') + return [] + } + + return data as Institution[] + } + + async getInstitutionsByFilter(filter: string) { + const { data, error } = await this.client + .from(table) + .select('*') + .or(`name.ilike.%${filter}%, city.ilike.%${filter}%, state.ilike.%${filter}%`) + .is('deletedAt', null) + .order('name') + + if (error) { + errorHandler(error, 'Erro ao buscar instituições com filtro') + return [] + } + + return data as Institution[] } } diff --git a/src/modules/school-management/views/institution/DashboardInstitution.vue b/src/modules/school-management/views/institution/DashboardInstitution.vue new file mode 100644 index 0000000..657fe05 --- /dev/null +++ b/src/modules/school-management/views/institution/DashboardInstitution.vue @@ -0,0 +1,320 @@ + + + + + diff --git a/src/modules/school-management/views/institution/InstitutionDetailView.vue b/src/modules/school-management/views/institution/InstitutionDetailView.vue new file mode 100644 index 0000000..c8e5842 --- /dev/null +++ b/src/modules/school-management/views/institution/InstitutionDetailView.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue b/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue new file mode 100644 index 0000000..9acf50a --- /dev/null +++ b/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/src/modules/school-management/views/institution/ListWithActionInstitution.vue b/src/modules/school-management/views/institution/ListWithActionInstitution.vue new file mode 100644 index 0000000..6b3f8e1 --- /dev/null +++ b/src/modules/school-management/views/institution/ListWithActionInstitution.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/src/modules/school-management/views/institution/RegisterInstitution.vue b/src/modules/school-management/views/institution/RegisterInstitution.vue new file mode 100644 index 0000000..92b2acc --- /dev/null +++ b/src/modules/school-management/views/institution/RegisterInstitution.vue @@ -0,0 +1,543 @@ + + + + + diff --git a/src/modules/school-management/views/institution/RegisterInstitutionMobile.vue b/src/modules/school-management/views/institution/RegisterInstitutionMobile.vue new file mode 100644 index 0000000..5843ddb --- /dev/null +++ b/src/modules/school-management/views/institution/RegisterInstitutionMobile.vue @@ -0,0 +1,527 @@ + + + + + diff --git a/src/utils/imasks.ts b/src/utils/imasks.ts index c54fae5..6b6b2bb 100644 --- a/src/utils/imasks.ts +++ b/src/utils/imasks.ts @@ -3,12 +3,22 @@ import IMask from 'imask' export default { mounted(el: HTMLElement, binding: DirectiveBinding) { + if (el.tagName === 'INPUT') { + IMask(el, binding.value) + return + } + const htmlInput = el.querySelector('input') if (htmlInput) { IMask(htmlInput, binding.value) } }, updated(el: HTMLElement, binding: DirectiveBinding) { + if (el.tagName === 'INPUT') { + IMask(el, binding.value) + return + } + const htmlInput = el.querySelector('input') if (htmlInput) { IMask(htmlInput, binding.value)