diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3baec6003..68364fe49 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,8 @@ "streetsidesoftware.code-spell-checker", "bradlc.vscode-tailwindcss", "foxundermoon.shell-format", - "vitest.explorer" + "vitest.explorer", + "streetsidesoftware.code-spell-checker-italian" ] } } diff --git a/backend/.render-build-trigger b/backend/.render-build-trigger index 1b4b5aaff..864de0cde 100644 --- a/backend/.render-build-trigger +++ b/backend/.render-build-trigger @@ -2,9 +2,9 @@ # This file is used to force Docker service rebuilds in PR previews # Modify LAST_UPDATE to trigger rebuilds -LAST_UPDATE=2025-07-30T00:00:00Z +LAST_UPDATE=2025-08-01T09:54:23Z # Instructions: # 1. To force rebuild of Docker services in a PR, update LAST_UPDATE -# 2. Run: perl -i -pe "s/LAST_UPDATE=.*/LAST_UPDATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)/" .render-build-trigger +# 2. Run: perl -i -pe "s/LAST_UPDATE=2025-08-01T09:54:23Z # 2. Commit and push changes to trigger Docker rebuilds \ No newline at end of file diff --git a/backend/services/email/templates/welcome.html b/backend/services/email/templates/welcome.html index eb9dc041a..a8a0f098c 100644 --- a/backend/services/email/templates/welcome.html +++ b/backend/services/email/templates/welcome.html @@ -253,7 +253,7 @@
- ➡️ Login and change password + ➡️ Login and Change password + + + + diff --git a/frontend/src/components/SideMenu.vue b/frontend/src/components/SideMenu.vue index 601b7733e..458b8da49 100644 --- a/frontend/src/components/SideMenu.vue +++ b/frontend/src/components/SideMenu.vue @@ -28,6 +28,12 @@ import { faBuilding as falBuilding, faUserGroup as falUserGroup, } from '@nethesis/nethesis-light-svg-icons' +import { + canReadCustomers, + canReadDistributors, + canReadResellers, + canReadUsers, +} from '@/lib/permissions' type MenuItem = { name: string @@ -52,37 +58,34 @@ const navigation = computed(() => { { name: 'dashboard.title', to: 'dashboard', solidIcon: fasHouse, lightIcon: falHouse }, ] - if (loginStore.userInfo?.org_role) { - if (loginStore.userInfo.org_role === 'Owner') { - menuItems.push({ - name: 'distributors.title', - to: 'distributors', - solidIcon: fasGlobe, - lightIcon: falGlobe, - }) - } + if (canReadDistributors()) { + menuItems.push({ + name: 'distributors.title', + to: 'distributors', + solidIcon: fasGlobe, + lightIcon: falGlobe, + }) + } - if (['Owner', 'Distributor'].includes(loginStore.userInfo.org_role)) { - menuItems.push({ - name: 'resellers.title', - to: 'resellers', - solidIcon: fasCity, - lightIcon: falCity, - }) - } + if (canReadResellers()) { + menuItems.push({ + name: 'resellers.title', + to: 'resellers', + solidIcon: fasCity, + lightIcon: falCity, + }) + } - if (['Owner', 'Distributor', 'Reseller'].includes(loginStore.userInfo.org_role)) { - menuItems.push({ - name: 'customers.title', - to: 'customers', - solidIcon: fasBuilding, - lightIcon: falBuilding, - }) - } + if (canReadCustomers()) { + menuItems.push({ + name: 'customers.title', + to: 'customers', + solidIcon: fasBuilding, + lightIcon: falBuilding, + }) } - // users menu item - if (loginStore.userInfo?.user_roles && loginStore.userInfo.user_roles.includes('Admin')) { + if (canReadUsers()) { menuItems.push({ name: 'users.title', to: 'users', diff --git a/frontend/src/components/TopBar.vue b/frontend/src/components/TopBar.vue index 256d5e570..76da815d5 100644 --- a/frontend/src/components/TopBar.vue +++ b/frontend/src/components/TopBar.vue @@ -22,7 +22,6 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { useI18n } from 'vue-i18n' import { useNotificationsStore } from '@/stores/notifications' import router from '@/router' -import OrganizationRoleBadge from './OrganizationRoleBadge.vue' const emit = defineEmits(['openSidebar']) @@ -217,7 +216,6 @@ function openNotificationsDrawer() {
{{ loginStore.userInfo?.email }}
-
diff --git a/frontend/src/components/account/ProfilePanel.vue b/frontend/src/components/account/ProfilePanel.vue index 96030f781..21e9ae363 100644 --- a/frontend/src/components/account/ProfilePanel.vue +++ b/frontend/src/components/account/ProfilePanel.vue @@ -20,6 +20,7 @@ import type { AxiosError } from 'axios' import { ref, useTemplateRef, watch, type ShallowRef } from 'vue' import { useI18n } from 'vue-i18n' import * as v from 'valibot' +import { USERS_KEY } from '@/lib/users' const { t } = useI18n() const loginStore = useLoginStore() @@ -47,8 +48,7 @@ const { validationIssues.value = getValidationIssues(error as AxiosError, 'users') }, onSettled: () => { - queryCache.invalidateQueries({ key: ['authMe'] }) - queryCache.invalidateQueries({ key: ['users'] }) + queryCache.invalidateQueries({ key: [USERS_KEY] }) }, }) diff --git a/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue b/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue index fe8cab84d..cc1fbf5d5 100644 --- a/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue +++ b/frontend/src/components/customers/CreateOrEditCustomerDrawer.vue @@ -14,6 +14,8 @@ import { import { computed, ref, useTemplateRef, watch, type ShallowRef } from 'vue' import { CreateCustomerSchema, + CUSTOMERS_KEY, + CUSTOMERS_TOTAL_KEY, CustomerSchema, postCustomer, putCustomer, @@ -63,10 +65,12 @@ const { }, onError: (error) => { console.error('Error creating customer:', error) - validationIssues.value = getValidationIssues(error as AxiosError, 'customers') + validationIssues.value = getValidationIssues(error as AxiosError, 'organizations') + }, + onSettled: () => { + queryCache.invalidateQueries({ key: [CUSTOMERS_KEY] }) + queryCache.invalidateQueries({ key: [CUSTOMERS_TOTAL_KEY] }) }, - - onSettled: () => queryCache.invalidateQueries({ key: ['customers'] }), }) const { @@ -95,20 +99,21 @@ const { onError: (error) => { console.error('Error editing customer:', error) }, - - onSettled: () => queryCache.invalidateQueries({ key: ['customers'] }), + onSettled: () => queryCache.invalidateQueries({ key: [CUSTOMERS_KEY] }), }) const name = ref('') const nameRef = useTemplateRef('nameRef') const description = ref('') const descriptionRef = useTemplateRef('descriptionRef') +const vatNumber = ref('') +const vatNumberRef = useTemplateRef('vatNumberRef') const validationIssues = ref>({}) const fieldRefs: Record>> = { name: nameRef, description: descriptionRef, - //// other fields + custom_data_vat: vatNumberRef, } const saving = computed(() => { @@ -126,12 +131,12 @@ watch( // editing customer name.value = currentCustomer.name description.value = currentCustomer.description || '' - //// + vatNumber.value = currentCustomer.custom_data?.vat || '' } else { // creating customer, reset form to defaults name.value = '' description.value = '' - //// + vatNumber.value = '' } } }, @@ -149,16 +154,24 @@ function clearErrors() { function validateCreate(customer: CreateCustomer): boolean { validationIssues.value = {} - const validation = v.safeParse(CreateCustomerSchema, customer) + const validation = v.safeParse(CreateCustomerSchema, customer) //// + // const validation = { success: true } //// remove if (validation.success) { // no validation issues return true } else { - const issues = v.flatten(validation.issues) + const flattenedIssues = v.flatten(validation.issues) - if (issues.nested) { - validationIssues.value = issues.nested as Record + if (flattenedIssues.nested) { + const issues: Record = {} + + for (const key in flattenedIssues.nested) { + // replace dots with underscores for i18n key + const newKey = key.replace(/\./g, '_') + issues[newKey] = flattenedIssues.nested[key] ?? [] + } + validationIssues.value = issues // focus the first field with error @@ -174,16 +187,24 @@ function validateCreate(customer: CreateCustomer): boolean { function validateEdit(customer: Customer): boolean { validationIssues.value = {} - const validation = v.safeParse(CustomerSchema, customer) + const validation = v.safeParse(CustomerSchema, customer) //// + // const validation = { success: true } //// remove if (validation.success) { // no validation issues return true } else { - const issues = v.flatten(validation.issues) + const flattenedIssues = v.flatten(validation.issues) + + if (flattenedIssues.nested) { + const issues: Record = {} - if (issues.nested) { - validationIssues.value = issues.nested as Record + for (const key in flattenedIssues.nested) { + // replace dots with underscores for i18n key + const newKey = key.replace(/\./g, '_') + issues[newKey] = flattenedIssues.nested[key] ?? [] + } + validationIssues.value = issues // focus the first field with error @@ -203,6 +224,9 @@ async function saveCustomer() { const customer = { name: name.value, description: description.value, + custom_data: { + vat: vatNumber.value, + }, } if (currentCustomer?.id) { @@ -244,7 +268,7 @@ async function saveCustomer() { @@ -252,12 +276,23 @@ async function saveCustomer() { + + + + diff --git a/frontend/src/components/dashboard/DistributorsCounterCard.vue b/frontend/src/components/dashboard/DistributorsCounterCard.vue new file mode 100644 index 000000000..7e651acf1 --- /dev/null +++ b/frontend/src/components/dashboard/DistributorsCounterCard.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/components/dashboard/ResellersCounterCard.vue b/frontend/src/components/dashboard/ResellersCounterCard.vue new file mode 100644 index 000000000..1e213285b --- /dev/null +++ b/frontend/src/components/dashboard/ResellersCounterCard.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/components/dashboard/UsersCounterCard.vue b/frontend/src/components/dashboard/UsersCounterCard.vue new file mode 100644 index 000000000..00ee42cf6 --- /dev/null +++ b/frontend/src/components/dashboard/UsersCounterCard.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue b/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue index 62c7f17b2..39b22683b 100644 --- a/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue +++ b/frontend/src/components/distributors/CreateOrEditDistributorDrawer.vue @@ -14,6 +14,8 @@ import { import { computed, ref, useTemplateRef, watch, type ShallowRef } from 'vue' import { CreateDistributorSchema, + DISTRIBUTORS_KEY, + DISTRIBUTORS_TOTAL_KEY, DistributorSchema, postDistributor, putDistributor, @@ -63,10 +65,12 @@ const { }, onError: (error) => { console.error('Error creating distributor:', error) - validationIssues.value = getValidationIssues(error as AxiosError, 'distributors') + validationIssues.value = getValidationIssues(error as AxiosError, 'organizations') + }, + onSettled: () => { + queryCache.invalidateQueries({ key: [DISTRIBUTORS_KEY] }) + queryCache.invalidateQueries({ key: [DISTRIBUTORS_TOTAL_KEY] }) }, - - onSettled: () => queryCache.invalidateQueries({ key: ['distributors'] }), }) const { @@ -95,21 +99,23 @@ const { onError: (error) => { console.error('Error editing distributor:', error) }, - - onSettled: () => queryCache.invalidateQueries({ key: ['distributors'] }), + onSettled: () => { + queryCache.invalidateQueries({ key: [DISTRIBUTORS_KEY] }) + }, }) const name = ref('') const nameRef = useTemplateRef('nameRef') const description = ref('') const descriptionRef = useTemplateRef('descriptionRef') -//// other fields +const vatNumber = ref('') +const vatNumberRef = useTemplateRef('vatNumberRef') const validationIssues = ref>({}) const fieldRefs: Record>> = { name: nameRef, description: descriptionRef, - //// other fields + custom_data_vat: vatNumberRef, } const saving = computed(() => { @@ -127,12 +133,12 @@ watch( // editing distributor name.value = currentDistributor.name description.value = currentDistributor.description || '' - //// + vatNumber.value = currentDistributor.custom_data?.vat || '' } else { // creating distributor, reset form to defaults name.value = '' description.value = '' - //// + vatNumber.value = '' } } }, @@ -150,20 +156,27 @@ function clearErrors() { function validateCreate(distributor: CreateDistributor): boolean { validationIssues.value = {} - const validation = v.safeParse(CreateDistributorSchema, distributor) + const validation = v.safeParse(CreateDistributorSchema, distributor) //// + // const validation = { success: true } //// remove if (validation.success) { // no validation issues return true } else { - const issues = v.flatten(validation.issues) + const flattenedIssues = v.flatten(validation.issues) - if (issues.nested) { - validationIssues.value = issues.nested as Record + if (flattenedIssues.nested) { + const issues: Record = {} - console.log('validationIssues', validationIssues.value) //// + for (const key in flattenedIssues.nested) { + // replace dots with underscores for i18n key + const newKey = key.replace(/\./g, '_') + issues[newKey] = flattenedIssues.nested[key] ?? [] + } + validationIssues.value = issues // focus the first field with error + const firstErrorFieldName = Object.keys(validationIssues.value)[0] console.log('firstFieldName', firstErrorFieldName) //// @@ -182,10 +195,17 @@ function validateEdit(distributor: Distributor): boolean { // no validation issues return true } else { - const issues = v.flatten(validation.issues) + const flattenedIssues = v.flatten(validation.issues) + + if (flattenedIssues.nested) { + const issues: Record = {} - if (issues.nested) { - validationIssues.value = issues.nested as Record + for (const key in flattenedIssues.nested) { + // replace dots with underscores for i18n key + const newKey = key.replace(/\./g, '_') + issues[newKey] = flattenedIssues.nested[key] ?? [] + } + validationIssues.value = issues // focus the first field with error @@ -193,7 +213,7 @@ function validateEdit(distributor: Distributor): boolean { console.log('firstFieldName', firstErrorFieldName) //// - fieldRefs[firstErrorFieldName].value?.focus() + fieldRefs[firstErrorFieldName]?.value?.focus() } return false } @@ -205,6 +225,9 @@ async function saveDistributor() { const distributor = { name: name.value, description: description.value, + custom_data: { + vat: vatNumber.value, + }, } if (currentDistributor?.id) { @@ -250,7 +273,7 @@ async function saveDistributor() { @@ -258,12 +281,23 @@ async function saveDistributor() { + + { console.error('Error deleting distributor:', error) }, - - onSettled: () => queryCache.invalidateQueries({ key: ['distributors'] }), + onSettled: () => queryCache.invalidateQueries({ key: [DISTRIBUTORS_KEY] }), }) function onShow() { diff --git a/frontend/src/components/distributors/DistributorsTable.vue b/frontend/src/components/distributors/DistributorsTable.vue index bf77c1659..1305a8942 100644 --- a/frontend/src/components/distributors/DistributorsTable.vue +++ b/frontend/src/components/distributors/DistributorsTable.vue @@ -4,7 +4,7 @@ --> @@ -244,7 +269,7 @@ async function saveReseller() { @@ -252,12 +277,23 @@ async function saveReseller() { + + { console.error('Error deleting reseller:', error) }, - - onSettled: () => queryCache.invalidateQueries({ key: ['resellers'] }), + onSettled: () => { + queryCache.invalidateQueries({ key: [RESELLERS_KEY] }) + queryCache.invalidateQueries({ key: [RESELLERS_TOTAL_KEY] }) + }, }) function onShow() { diff --git a/frontend/src/components/resellers/ResellersTable.vue b/frontend/src/components/resellers/ResellersTable.vue index 5d2f99dce..c6eef51ec 100644 --- a/frontend/src/components/resellers/ResellersTable.vue +++ b/frontend/src/components/resellers/ResellersTable.vue @@ -4,7 +4,7 @@ --> @@ -18,7 +19,7 @@ const isShownCreateUserDrawer = ref(false) {{ $t('users.title') }}
- {{ $t('users.page_description') }} + {{ $t('users.page_description', { productName: PRODUCT_NAME }) }}
diff --git a/proxy/.render-build-trigger b/proxy/.render-build-trigger index e7b7d8643..864de0cde 100644 --- a/proxy/.render-build-trigger +++ b/proxy/.render-build-trigger @@ -2,9 +2,9 @@ # This file is used to force Docker service rebuilds in PR previews # Modify LAST_UPDATE to trigger rebuilds -LAST_UPDATE=2025-07-29T06:56:10Z +LAST_UPDATE=2025-08-01T09:54:23Z # Instructions: # 1. To force rebuild of Docker services in a PR, update LAST_UPDATE -# 2. Run: perl -i -pe "s/LAST_UPDATE=.*/LAST_UPDATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)/" .render-build-trigger +# 2. Run: perl -i -pe "s/LAST_UPDATE=2025-08-01T09:54:23Z # 2. Commit and push changes to trigger Docker rebuilds \ No newline at end of file