From 6ef6bce21c4d148336165ecd1fae18b5c90c4927 Mon Sep 17 00:00:00 2001 From: jonLucas33 Date: Fri, 23 May 2025 21:11:41 -0300 Subject: [PATCH 1/4] Started Register Institution --- src/modules/school-management/routes.ts | 42 ++- .../services/InstitutionService.ts | 230 ++++++++++++- .../institution/DashboardInstitution.vue | 312 ++++++++++++++++++ .../institution/InstitutionDetailView.vue | 160 +++++++++ .../institution/InstitutionDetailsDesktop.vue | 266 +++++++++++++++ .../institution/ListWithActionInstitution.vue | 160 +++++++++ .../views/institution/RegisterInstitution.vue | 283 ++++++++++++++++ .../institution/RegisterInstitutionMobile.vue | 264 +++++++++++++++ 8 files changed, 1712 insertions(+), 5 deletions(-) create mode 100644 src/modules/school-management/views/institution/DashboardInstitution.vue create mode 100644 src/modules/school-management/views/institution/InstitutionDetailView.vue create mode 100644 src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue create mode 100644 src/modules/school-management/views/institution/ListWithActionInstitution.vue create mode 100644 src/modules/school-management/views/institution/RegisterInstitution.vue create mode 100644 src/modules/school-management/views/institution/RegisterInstitutionMobile.vue 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..a4671fe --- /dev/null +++ b/src/modules/school-management/views/institution/DashboardInstitution.vue @@ -0,0 +1,312 @@ + + + + + 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..2b715f9 --- /dev/null +++ b/src/modules/school-management/views/institution/InstitutionDetailView.vue @@ -0,0 +1,160 @@ + + + + + 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..dd64d69 --- /dev/null +++ b/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue @@ -0,0 +1,266 @@ + + + + + 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..930c52e --- /dev/null +++ b/src/modules/school-management/views/institution/RegisterInstitution.vue @@ -0,0 +1,283 @@ + + + + + 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..19dfa58 --- /dev/null +++ b/src/modules/school-management/views/institution/RegisterInstitutionMobile.vue @@ -0,0 +1,264 @@ + + + + + From eae3a94138fadc90fd93fe895cd6f486aa4c4b40 Mon Sep 17 00:00:00 2001 From: jonLucas33 Date: Mon, 26 May 2025 01:03:28 -0300 Subject: [PATCH 2/4] Added masks and api cep --- .../institution/InstitutionDetailsDesktop.vue | 39 +- .../views/institution/RegisterInstitution.vue | 618 ++++++++++++------ src/utils/imasks.ts | 10 + 3 files changed, 432 insertions(+), 235 deletions(-) diff --git a/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue b/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue index dd64d69..9acf50a 100644 --- a/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue +++ b/src/modules/school-management/views/institution/InstitutionDetailsDesktop.vue @@ -110,12 +110,6 @@ watch( {{ institutionDetails.phone || '-' }} - -
- CEP - {{ institutionDetails.postalCode || '-' }} -
-
@@ -126,9 +120,15 @@ watch( - +
- Endereço completo + CEP + {{ institutionDetails.postalCode || '-' }} +
+
+ +
+ Logradouro {{ institutionDetails.address || '-' }}
@@ -137,36 +137,29 @@ watch(
- Cidade - {{ institutionDetails.city || '-' }} + Número + {{ '-' }}
- Estado - {{ institutionDetails.state || '-' }} + Bairro + {{ '-' }}
-
- -
-

- Informações do sistema -

-
- Data de criação - {{ formatDate(institutionDetails.createdAt) }} + Cidade + {{ institutionDetails.city || '-' }}
- Última atualização - {{ formatDate(institutionDetails.updatedAt) }} + Estado + {{ institutionDetails.state || '-' }}
diff --git a/src/modules/school-management/views/institution/RegisterInstitution.vue b/src/modules/school-management/views/institution/RegisterInstitution.vue index 930c52e..a9f669e 100644 --- a/src/modules/school-management/views/institution/RegisterInstitution.vue +++ b/src/modules/school-management/views/institution/RegisterInstitution.vue @@ -1,158 +1,18 @@ - - + + 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) From 7ad6ee679ecc9cd293ca9ac16832e01214a67595 Mon Sep 17 00:00:00 2001 From: jonLucas33 Date: Mon, 26 May 2025 12:38:09 -0300 Subject: [PATCH 3/4] Adjusuts to navigation from edit page to create page --- .../views/institution/RegisterInstitution.vue | 92 +++++++++++++++---- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/src/modules/school-management/views/institution/RegisterInstitution.vue b/src/modules/school-management/views/institution/RegisterInstitution.vue index a9f669e..f10f137 100644 --- a/src/modules/school-management/views/institution/RegisterInstitution.vue +++ b/src/modules/school-management/views/institution/RegisterInstitution.vue @@ -12,7 +12,7 @@ import { IonToolbar, } from '@ionic/vue' import { ErrorMessage, Field, Form } from 'vee-validate' -import { computed, onMounted, ref, watch } from 'vue' +import { computed, onActivated, onMounted, ref, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' import showToast from '@/utils/toast-alert' import InstitutionService from '../../services/InstitutionService' @@ -36,12 +36,36 @@ const initialFormValues = { } const formValues = ref({ ...initialFormValues }) +const originalFormValues = ref({ ...initialFormValues }) +const hasChanges = ref(false) const form = ref(null) +watch( + () => ({ ...formValues.value }), + () => { + if (isEditing.value) { + checkForChanges() + } + }, + { deep: true, immediate: true } +) + onMounted(async () => { if (isEditing.value) { await loadInstitution() + } else { + formValues.value = { ...initialFormValues } + originalFormValues.value = { ...initialFormValues } + hasChanges.value = false + } +}) + +onActivated(() => { + if (!isEditing.value) { + formValues.value = { ...initialFormValues } + originalFormValues.value = { ...initialFormValues } + hasChanges.value = false } }) @@ -59,20 +83,29 @@ async function buscarEndereco(cep: string) { } } -// Watcher para o campo de CEP para autocompletar os campos de endereço -watch(() => formValues.value.postalCode, async (novoCep) => { +watch(() => formValues.value.postalCode, async (novoCep, antigoCep) => { + if (!novoCep || novoCep !== antigoCep) { + formValues.value.address = ''; + formValues.value.city = ''; + formValues.value.state = ''; + + if (form.value) { + form.value.setFieldValue('address', ''); + form.value.setFieldValue('city', ''); + form.value.setFieldValue('state', ''); + } + } + if (!novoCep) return; const cepLimpo = String(novoCep).replace(/\D/g, ''); if (cepLimpo && cepLimpo.length === 8) { const endereco = await buscarEndereco(cepLimpo); if (endereco) { - // Atualiza os valores do formulário formValues.value.address = endereco.logradouro || ''; formValues.value.city = endereco.localidade || ''; formValues.value.state = endereco.uf || ''; - // Atualiza os valores nos campos do formulário vee-validate if (form.value) { form.value.setFieldValue('address', endereco.logradouro || ''); form.value.setFieldValue('city', endereco.localidade || ''); @@ -87,7 +120,7 @@ async function loadInstitution() { const id = route.params.id as string const institution = await institutionService.getInstitutionById(id) if (institution) { - formValues.value = { + const loadedValues = { id: institution.id || '', name: institution.name || '', address: institution.address || '', @@ -97,6 +130,10 @@ async function loadInstitution() { phone: institution.phone || '', email: institution.email || '' } + + formValues.value = { ...loadedValues } + originalFormValues.value = { ...loadedValues } + hasChanges.value = false } } catch (error) { console.error('Erro ao carregar instituição:', error) @@ -104,9 +141,32 @@ async function loadInstitution() { } } +function checkForChanges() { + if (!isEditing.value) return + + const hasNameChanged = formValues.value.name !== originalFormValues.value.name + const hasAddressChanged = formValues.value.address !== originalFormValues.value.address + const hasCityChanged = formValues.value.city !== originalFormValues.value.city + const hasStateChanged = formValues.value.state !== originalFormValues.value.state + const hasPostalCodeChanged = formValues.value.postalCode !== originalFormValues.value.postalCode + const hasPhoneChanged = formValues.value.phone !== originalFormValues.value.phone + const hasEmailChanged = formValues.value.email !== originalFormValues.value.email + + hasChanges.value = hasNameChanged || hasAddressChanged || hasCityChanged || + hasStateChanged || hasPostalCodeChanged || hasPhoneChanged || + hasEmailChanged +} + const saveButtonEnabled = computed(() => { - // Habilita o botão se pelo menos o nome estiver preenchido - return !!formValues.value.name + if (!isEditing.value) { + const anyFieldFilled = !!formValues.value.name || !!formValues.value.address || + !!formValues.value.city || !!formValues.value.state || + !!formValues.value.postalCode || !!formValues.value.phone || + !!formValues.value.email + return anyFieldFilled + } + + return hasChanges.value }) async function handleSubmit(values: any) { @@ -119,7 +179,7 @@ async function handleSubmit(values: any) { address: values.address?.trim() || '', city: values.city?.trim() || '', state: values.state?.trim() || '', - postalCode: values.postalCode?.trim() || '', // Usando o nome padronizado postalCode + postalCode: values.postalCode?.trim() || '', phone: values.phone?.trim() || '', email: values.email?.trim() || '', } @@ -132,7 +192,7 @@ async function handleSubmit(values: any) { 'success' ) - navigateBack(true) + resetForm() } catch (error) { console.error('Erro ao salvar instituição:', error) showToast('Erro ao salvar instituição', 'top', 'danger') @@ -142,16 +202,10 @@ async function handleSubmit(values: any) { } function resetForm() { - formValues.value = { ...initialFormValues } + router.back() } -function navigateBack(refresh = false) { - if (refresh) { - router.push({ name: 'DashboardInstitution', query: { refresh: 'true' } }) - } else { - router.push({ name: 'DashboardInstitution' }) - } -} +