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
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+ {{ counter }}
+
+
+
+
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