3 Commits

Author SHA1 Message Date
Surya Paolo
7b746e3b6f - Caricamento Video 2025-12-19 22:59:13 +01:00
Surya Paolo
99d623da79 Versione 1.2.87 2025-12-18 19:45:36 +01:00
Surya Paolo
a305bd8493 - invio RIS... 2025-12-18 19:03:24 +01:00
50 changed files with 2780 additions and 737 deletions

4
.env
View File

@@ -1,6 +1,6 @@
VITE_APP_VERSION="1.2.86"
VITE_APP_VERSION="1.2.87"
VITE_LANG_DEFAULT="it"
VITE_PAO_APP_ID="KKPPAA5KJK435J3KSS9F9D8S9F8SD98F9SDF"
VITE_SERVICE_WORKER_FILE="sw-1.2.86.js"
VITE_SERVICE_WORKER_FILE="sw-1.2.87.js"
VITE_PROJECT_ID_MAIN="5cc0a13fe5c9d156728f400a"
VITE_VUE_ROUTER_MODE="history"

View File

@@ -10,7 +10,7 @@
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="version" content="1.2.86">
<meta name="version" content="1.2.87">
<meta name="viewport"
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">

View File

@@ -1,6 +1,6 @@
{
"name": "riso",
"version": "1.2.86",
"version": "1.2.87",
"productName": "Riso 💚 - Rete Italiana Scambio orizzontale",
"description": "Progetto RISO (Rete Italiana Scambio orizzontale) promuove una rete di comunità locali che favoriscono scambi di beni, servizi e ospitalità. Con l'App RISO, sviluppata per facilitare il baratto, il dono e l'uso di monete alternative come i RIS, il progetto crea legami autentici basati sulla fiducia e sostenibilità. Partecipa agli scambi e costruisci una comunità più consapevole e autosufficiente.",
"author": "Surya",
@@ -9,11 +9,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "APP_VERSION='1.2.86' PORT=8084 quasar dev",
"dev": "APP_VERSION='1.2.87' PORT=8084 quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "quasar build -m spa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -74,6 +74,8 @@ export default defineConfig((ctx) => {
'@paths': path.resolve(__dirname, 'src/store/Api/ApiRoutes.ts'),
'@images': path.resolve(__dirname, 'src/assets/images'),
'@icons': path.resolve(__dirname, 'src/public/myicons'),
'@types': path.resolve(__dirname, 'src/types'),
'@services': path.resolve(__dirname, 'src/services'),
},
};
@@ -82,7 +84,7 @@ export default defineConfig((ctx) => {
...(viteConf.css || {}),
preprocessorOptions: {
scss: {
additionalData: `@use "sass:color"; @use "@/css/variables.scss" as *;`,
additionalData: `@use "sass:color"; @use "src/css/variables.scss" as *;`,
},
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "cnm",
"version": "1.2.86",
"version": "1.2.87",
"description": "Comunita Nuovo Mondo",
"productName": "ComunitaNuovoMondo",
"author": "Surya",
@@ -9,7 +9,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "PORT=8083 APP_VERSION='1.2.86' quasar dev",
"dev": "PORT=8083 APP_VERSION='1.2.87' quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production quasar build -m pwa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -25,6 +25,7 @@ const msg_website_it = {
Ammetti: 'Ammetti',
AbilitaCircuito: 'Abilita Circuito',
installaApp: 'Installa App',
VideoPage: 'Video',
fundraising: 'Sostieni il Progetto',
notifs: 'Configura le Notifiche',
unsubscribe: 'Disiscriviti',
@@ -88,6 +89,7 @@ const msg_website_it = {
eventodef: 'Evento:',
prova: 'prova',
dbop: 'Operazioni',
VideoPage: 'Video',
dbopmacro: 'Operazioni Macro',
projall: 'Comunitari',
groups: 'Lista Gruppi',

View File

@@ -1,6 +1,6 @@
{
"name": "freeplanet",
"version": "1.2.86",
"version": "1.2.87",
"description": "freeplanet",
"productName": "freeplanet",
"author": "Surya",
@@ -9,11 +9,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "PORT=8087 APP_VERSION='1.2.86' quasar dev",
"dev": "PORT=8087 APP_VERSION='1.2.87' quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "quasar build -m spa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8097 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8087 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8097 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8087 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -1,6 +1,6 @@
{
"name": "riso",
"version": "1.2.86",
"version": "1.2.87",
"productName": "Riso 💚 - Rete Italiana Scambio orizzontale",
"description": "Progetto RISO (Rete Italiana Scambio orizzontale) promuove una rete di comunità locali che favoriscono scambi di beni, servizi e ospitalità. Con l'App RISO, sviluppata per facilitare il baratto, il dono e l'uso di monete alternative come i RIS, il progetto crea legami autentici basati sulla fiducia e sostenibilità. Partecipa agli scambi e costruisci una comunità più consapevole e autosufficiente.",
"author": "Surya",
@@ -9,11 +9,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "APP_VERSION='1.2.86' PORT=8084 quasar dev",
"dev": "APP_VERSION='1.2.87' PORT=8084 quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "quasar build -m spa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -1,6 +1,6 @@
{
"name": "gruppomacro",
"version": "1.2.86",
"version": "1.2.87",
"productName": "Gruppo Macro",
"description": "Il Gruppo Editoriale Macro, attivo dal 1987, è leader europeo nella pubblicazione di libri per il benessere e la consapevolezza. Con oltre 1.500 titoli, promuove una visione armonica del mondo, offrendo opere di autori internazionali e italiani come Gregg Braden, Bruce Lipton, Joe Dispenza, Louise Hay, Eckhart Tolle e molti altri. Scopri un'editoria che abbraccia il corpo, la mente, lo spirito e l'ecologia.",
"author": "Surya",
@@ -9,20 +9,20 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "PORT=8089 APP_VERSION='1.2.86' quasar dev",
"dev": "PORT=8089 APP_VERSION='1.2.87' quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "APP_VERSION='1.2.86' quasar build -m spa",
"buildspa": "APP_VERSION='1.2.87' quasar build -m spa",
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\"",
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8089 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8089 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -1,6 +1,6 @@
{
"name": "nuovomondo",
"version": "1.2.86",
"version": "1.2.87",
"description": "Nuovo Mondo",
"productName": "Nuovo Mondo",
"author": "Surya",
@@ -9,11 +9,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "APP_VERSION='1.2.86' PORT=8083 quasar dev",
"dev": "APP_VERSION='1.2.87' PORT=8083 quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "quasar build -m spa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -1,6 +1,6 @@
{
"name": "nutriben",
"version": "1.2.86",
"version": "1.2.87",
"description": "Nutriben",
"productName": "Nutriben",
"author": "Surya",
@@ -9,20 +9,20 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "PORT=8093 APP_VERSION='1.2.86' quasar dev",
"dev": "PORT=8093 APP_VERSION='1.2.87' quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "APP_VERSION='1.2.86' quasar build -m spa",
"buildspa": "APP_VERSION='1.2.87' quasar build -m spa",
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\"",
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -1,6 +1,6 @@
{
"name": "piuchebuono",
"version": "1.2.86",
"version": "1.2.87",
"description": "PiuCheBuono",
"productName": "PiuCheBuono",
"author": "Surya",
@@ -9,11 +9,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "PORT=8085 APP_VERSION='1.2.86' quasar dev",
"dev": "PORT=8085 APP_VERSION='1.2.87' quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "quasar build -m spa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -1,6 +1,6 @@
{
"name": "riso",
"version": "1.2.86",
"version": "1.2.87",
"productName": "Riso 💚 - Rete Italiana Scambio orizzontale",
"description": "Progetto RISO (Rete Italiana Scambio orizzontale) promuove una rete di comunità locali che favoriscono scambi di beni, servizi e ospitalità. Con l'App RISO, sviluppata per facilitare il baratto, il dono e l'uso di monete alternative come i RIS, il progetto crea legami autentici basati sulla fiducia e sostenibilità. Partecipa agli scambi e costruisci una comunità più consapevole e autosufficiente.",
"author": "Surya",
@@ -9,11 +9,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "APP_VERSION='1.2.86' PORT=8084 quasar dev",
"dev": "APP_VERSION='1.2.87' PORT=8084 quasar dev",
"dev_noCheck": "SKIP_TSC=true quasar dev",
"build": "quasar build",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"buildspa": "quasar build -m spa",
@@ -21,8 +21,8 @@
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.86' quasar dev",
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.87' quasar dev",
"debug": "quasar dev --mode debug",
"test": "echo \"No test specified\" && exit 0",
"generate-sw": "workbox generateSW workbox-config.js",

View File

@@ -3,7 +3,7 @@
/* global workbox */
/* global cfgenv */
const VITE_APP_VERSION = '1.2.86';
const VITE_APP_VERSION = '1.2.87';
// Costanti di configurazione
const DYNAMIC_CACHE = 'dynamic-cache-v2';

View File

@@ -35,6 +35,7 @@ import { colmyUserPeople, colmyUserGroup } from '@store/Modules/fieldsTable';
export default defineComponent({
name: 'CFindUsers',
emits: ['clickContact'],
props: {
actionType: {
type: Number,
@@ -46,6 +47,11 @@ export default defineComponent({
required: false,
default: '',
},
enableContactClick: {
type: Boolean,
required: false,
default: false,
},
},
components: {
CMyUser,
@@ -57,7 +63,7 @@ export default defineComponent({
CGridTableRec,
CQRCode,
},
setup(props) {
setup(props, { emit }) {
const userStore = useUserStore();
const globalStore = useGlobalStore();
const circuitStore = useCircuitStore();
@@ -253,6 +259,10 @@ export default defineComponent({
usersList.value.listgroup = receiveRislistgroup;
}
function clickContact(data: any) {
emit('clickContact', data)
}
return {
userStore,
tools,
@@ -277,6 +287,7 @@ export default defineComponent({
extraparams_groups,
filtercustom,
arrfilterand,
clickContact,
};
},
});

View File

@@ -65,6 +65,9 @@
:showCol="false"
:extraparams="extraparams()"
:actionType="actionType"
@clickContact="clickContact"
:enableContactClick="enableContactClick"
>
</CGridTableRec>
</div>
@@ -99,6 +102,8 @@
:extraparams="extraparams_groups()"
:actionType="actionType"
:visufind="costanti.FIND_GROUP"
@clickContact="clickContact"
:enableContactClick="enableContactClick"
>
</CGridTableRec>
</div>

View File

@@ -71,7 +71,7 @@ import { isMap } from 'util/types';
export default defineComponent({
name: 'CGridTableRec',
emits: ['clickButtBar', 'savefilter'],
emits: ['clickButtBar', 'savefilter', 'clickContact'],
props: {
prop_mytitle: {
type: String,
@@ -377,6 +377,11 @@ export default defineComponent({
required: false,
default: false,
},
enableContactClick: {
type: Boolean,
required: false,
default: false,
},
},
components: {
CMyPopupEdit,
@@ -3230,6 +3235,10 @@ export default defineComponent({
return false;
}
function clickContact(data: any) {
emit('clickContact', data);
}
created();
return {
@@ -3368,6 +3377,7 @@ export default defineComponent({
getisDettagliByCatalog,
getisDettagliByRaccolta,
hidewindowEdit,
clickContact,
};
},
});

View File

@@ -679,6 +679,8 @@
col_footer ? tools.getLabelFooterByRow(row, col_footer, tablesel) : ''
"
@showInnerDialog="showInnerDialog"
@clickContact="clickContact"
:enableContactClick="enableContactClick"
>
</CMyUser>
@@ -701,6 +703,8 @@
:finder="false"
:mygrp="row"
:visu="visufind ? visufind : costanti.FIND_GROUP"
@clickContact="clickContact"
:enableContactClick="enableContactClick"
/>
</div>
</div>

View File

@@ -1,20 +1,72 @@
// ==========================================
// CMENUITEM.SCSS - ORIGINALE + MIGLIORAMENTI RISO
// Mantiene tutto il codice originale + stile moderno
// CMENUITEM.SCSS
// ==========================================
// ==========================================
// VARIABILI COLORI MENU
// ==========================================
$menu-border-color: #dedede;
$menu-border-color-dark: #404040;
$menu-active-color: #027be3;
$menu-active-color-dark: #4da3ff;
$menu-active-bg: #dadada;
$menu-active-bg-dark: #2a3a4a;
$menu-hover-bg: rgba(0, 0, 0, 0.04);
$menu-hover-bg-dark: rgba(255, 255, 255, 0.08);
$menu-text-color: rgba(0, 0, 0, 0.87);
$menu-text-color-dark: rgba(255, 255, 255, 0.87);
$menu-icon-color: rgba(0, 0, 0, 0.5);
$menu-icon-color-dark: rgba(255, 255, 255, 0.5);
$subtitle-color: #666;
$subtitle-color-dark: #aaa;
// Colori ruoli
$color-admin: #d32f2f;
$color-admin-dark: #ef5350;
$color-manager: #388e3c;
$color-manager-dark: #66bb6a;
$color-socio: #1b5e20;
$color-socio-dark: #4caf50;
$color-facilitatore: #4a148c;
$color-facilitatore-dark: #9c27b0;
$color-collaboratore: #f57c00;
$color-collaboratore-dark: #ffb74d;
$color-editor: #6a1b9a;
$color-editor-dark: #ab47bc;
$color-commerciale: #e65100;
$color-commerciale-dark: #ff9800;
$color-grafico: #00796b;
$color-grafico-dark: #26a69a;
$color-doc: #42a5f5;
$color-doc-dark: #64b5f6;
// ==========================================
// DEBUG CLASS
// ==========================================
.prova {
color: red;
}
// ==========================================
// BASE MENU STYLES
// ==========================================
.q-list-header {
min-height: 12px;
padding: 5px 8px;
}
.menu-hr {
border-color: #dedede;
border-color: $menu-border-color;
height: 0.5px;
.body--dark & {
border-color: $menu-border-color-dark;
}
}
.list-label:first-child {
@@ -23,90 +75,266 @@
margin: 1px;
}
// ==========================================
// ROUTER LINK ACTIVE
// ==========================================
.router-link-active {
color: #027be3;
background-color: #dadada !important;
border-right: 2px solid #027be3;
}
.router-link-active .item-primary {
color: #027be3;
color: $menu-active-color;
background-color: $menu-active-bg !important;
border-right: 2px solid $menu-active-color;
.body--dark & {
color: $menu-active-color-dark;
background-color: $menu-active-bg-dark !important;
border-right-color: $menu-active-color-dark;
}
.item-primary {
color: $menu-active-color;
.body--dark & {
color: $menu-active-color-dark;
}
}
}
// ==========================================
// MENU ARROW
// ==========================================
.menu_freccina {
position: absolute;
right: 10px;
display: inline-block;
padding: 0 0 0 0;
-webkit-transform: rotate(-180deg);
padding: 0;
transform: rotate(-180deg);
}
// ==========================================
// MENU SIZES
// ==========================================
.my-menu,
.my-menu>i {
.my-menu > i {
min-height: 40px;
min-width: 26px;
font-size: 1rem;
}
.my-menu-small,
.my-menu-small>i {
.my-menu-small > i {
min-height: 40px;
min-width: 26px;
font-size: 0.75rem;
}
.isAdmin {
color: red !important;
}
.isSocioResidente {
color: darkgreen;
}
.isCalendar {}
.isManager {
color: green !important;
}
.isFacilitatore {
color: #201a80;
}
.onlyCollaboratore {
color: #bd7b10;
}
.my-menu-icon {
min-width: 2px;
font-size: 1rem;
> i {
min-width: 26px;
font-size: 1.25rem;
}
}
.my-menu-icon>i {
min-width: 26px;
font-size: 1.25rem;
}
.clexpansion {
min-width: 0 !important;
}
.my-menu-active {
background-color: rgba(174, 189, 241, 0.71);
}
.my-menu-separat>i {
.my-menu-separat > i {
min-width: 26px;
font-size: 1rem;
}
.my-menu-icon-none>i {
.my-menu-icon-none > i {
display: none;
}
.clicon img,
.clicon {
// ==========================================
// ROLE COLORS - Con Dark Mode
// ==========================================
.isAdmin {
color: $color-admin !important;
border-left: 3px solid $color-admin;
background: linear-gradient(90deg, rgba($color-admin, 0.08) 0%, rgba($color-admin, 0.02) 100%);
.body--dark & {
color: $color-admin-dark !important;
border-left-color: $color-admin-dark;
background: linear-gradient(90deg, rgba($color-admin-dark, 0.15) 0%, rgba($color-admin-dark, 0.05) 100%);
}
.q-avatar {
background: rgba($color-admin, 0.12) !important;
.q-icon {
color: $color-admin;
}
.body--dark & {
background: rgba($color-admin-dark, 0.2) !important;
.q-icon {
color: $color-admin-dark;
}
}
}
&:hover {
background: linear-gradient(90deg, rgba($color-admin, 0.12) 0%, rgba($color-admin, 0.04) 100%);
.body--dark & {
background: linear-gradient(90deg, rgba($color-admin-dark, 0.2) 0%, rgba($color-admin-dark, 0.08) 100%);
}
}
}
.isManager {
color: $color-manager !important;
border-left: 3px solid $color-manager;
background: linear-gradient(90deg, rgba($color-manager, 0.08) 0%, rgba($color-manager, 0.02) 100%);
.body--dark & {
color: $color-manager-dark !important;
border-left-color: $color-manager-dark;
background: linear-gradient(90deg, rgba($color-manager-dark, 0.15) 0%, rgba($color-manager-dark, 0.05) 100%);
}
.q-avatar {
background: rgba($color-manager, 0.12) !important;
.q-icon {
color: $color-manager;
}
.body--dark & {
background: rgba($color-manager-dark, 0.2) !important;
.q-icon {
color: $color-manager-dark;
}
}
}
&:hover {
background: linear-gradient(90deg, rgba($color-manager, 0.12) 0%, rgba($color-manager, 0.04) 100%);
.body--dark & {
background: linear-gradient(90deg, rgba($color-manager-dark, 0.2) 0%, rgba($color-manager-dark, 0.08) 100%);
}
}
}
.isSocioResidente {
color: $color-socio !important;
.body--dark & {
color: $color-socio-dark !important;
}
.q-avatar .q-icon {
color: $color-socio;
.body--dark & {
color: $color-socio-dark;
}
}
}
.isFacilitatore {
color: $color-facilitatore !important;
.body--dark & {
color: $color-facilitatore-dark !important;
}
.q-avatar .q-icon {
color: $color-facilitatore;
.body--dark & {
color: $color-facilitatore-dark;
}
}
}
.onlyCollaboratore {
color: $color-collaboratore !important;
.body--dark & {
color: $color-collaboratore-dark !important;
}
.q-avatar .q-icon {
color: $color-collaboratore;
.body--dark & {
color: $color-collaboratore-dark;
}
}
}
.isEditor {
color: $color-editor !important;
.body--dark & {
color: $color-editor-dark !important;
}
.q-avatar .q-icon {
color: $color-editor;
.body--dark & {
color: $color-editor-dark;
}
}
}
.isCommerciale {
color: $color-commerciale !important;
.body--dark & {
color: $color-commerciale-dark !important;
}
.q-avatar .q-icon {
color: $color-commerciale;
.body--dark & {
color: $color-commerciale-dark;
}
}
}
.isGrafico {
color: $color-grafico !important;
.body--dark & {
color: $color-grafico-dark !important;
}
.q-avatar .q-icon {
color: $color-grafico;
.body--dark & {
color: $color-grafico-dark;
}
}
}
.isDoc {
border-left: 3px solid $color-doc;
background: linear-gradient(90deg, rgba($color-doc, 0.08) 0%, rgba($color-doc, 0.02) 100%);
.body--dark & {
border-left-color: $color-doc-dark;
background: linear-gradient(90deg, rgba($color-doc-dark, 0.15) 0%, rgba($color-doc-dark, 0.05) 100%);
}
}
.isCalendar {
// Placeholder per eventuali stili calendario
}
// ==========================================
// ICONS & AVATARS
// ==========================================
.clicon,
.clicon img {
font-size: 16px;
}
@@ -114,100 +342,151 @@
min-width: 30px;
}
.OLD_q-item__section--side {
padding-right: 8px;
}
.imgicon img {
font-size: 2.5rem !important;
border-radius: 8px;
}
/*
.menu-enter-active, .scale-enter {
-webkit-animation: moveFromTopFade .5s ease both;
animation: moveFromTopFade .5s ease both;
.clexpansion {
min-width: 0 !important;
}
.menu-leave-to, .scale-leave-active {
-webkit-animation: moveToBottom .5s ease both;
animation: moveToBottom .5s ease both;
}
*/
// ==========================================
// MENU ACTIVE STATE
// ==========================================
.my-menu-active {
background: linear-gradient(90deg, rgba($menu-active-color, 0.12) 0%, rgba($menu-active-color, 0.05) 100%) !important;
border-radius: 8px;
border-right: 3px solid $menu-active-color;
.body--dark & {
background: linear-gradient(90deg, rgba($menu-active-color-dark, 0.2) 0%, rgba($menu-active-color-dark, 0.08) 100%) !important;
border-right-color: $menu-active-color-dark;
}
span {
color: $menu-active-color;
font-weight: 600;
.body--dark & {
color: $menu-active-color-dark;
}
}
.q-avatar {
background: rgba($menu-active-color, 0.1) !important;
.body--dark & {
background: rgba($menu-active-color-dark, 0.2) !important;
}
.q-icon {
color: $menu-active-color;
.body--dark & {
color: $menu-active-color-dark;
}
}
}
&:hover {
background: linear-gradient(90deg, rgba($menu-active-color, 0.16) 0%, rgba($menu-active-color, 0.08) 100%) !important;
.body--dark & {
background: linear-gradient(90deg, rgba($menu-active-color-dark, 0.25) 0%, rgba($menu-active-color-dark, 0.12) 100%) !important;
}
}
}
// ==========================================
// BIG MENU - Titoli grandi
// ==========================================
.bigmenu {
font-size: 1.25rem;
font-weight: bold;
text-shadow: 0.0512rem 0.052rem .01rem #555;
font-size: 1.2rem;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
letter-spacing: -0.01em;
.body--dark & {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
}
// ==========================================
// SUBTITLE
// ==========================================
.subtitle {
font-size: 0.85em;
color: #666;
font-size: 0.8rem;
color: $subtitle-color;
font-style: italic;
margin-top: 2px;
opacity: 0.85;
line-height: 1.3;
.body--dark & {
color: $subtitle-color-dark;
}
}
// ==========================================
// MIGLIORAMENTI STILE RISO - AGGIUNTI
// QUASAR COMPONENT OVERRIDES
// ==========================================
// ==========================================
// INDENTAZIONE LIVELLI - Più compatta
// ==========================================
[style*="paddingLeft"] {
transition: all 0.3s ease;
}
// ==========================================
// Q-SEPARATOR - Divisore migliorato
// ==========================================
// Q-SEPARATOR
.q-separator {
background: linear-gradient(90deg,
transparent 0%,
rgba(0, 0, 0, 0.1) 50%,
transparent 100%);
background: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.1) 50%, transparent 100%);
margin: 6px 0;
height: 1px;
.body--dark & {
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.15) 50%, transparent 100%);
}
}
// ==========================================
// Q-EXPANSION-ITEM - Espandibile migliorato
// ==========================================
// Q-EXPANSION-ITEM
.q-expansion-item {
border-radius: 8px;
margin-bottom: 2px;
overflow: hidden;
// Header dell'expansion
> .q-item {
border-radius: 8px;
padding: 6px 10px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
background: rgba(0, 0, 0, 0.04);
background: $menu-hover-bg;
transform: translateX(2px);
.body--dark & {
background: $menu-hover-bg-dark;
}
}
}
// Label header
.q-item__label {
font-size: 0.95rem;
font-weight: 500;
}
// Icona expand/collapse
.q-icon {
transition: all 0.3s ease;
font-size: 0.9rem;
color: rgba(0, 0, 0, 0.5);
}
}
color: $menu-icon-color;
// Quando espanso
.q-expansion-item--expanded {
> .q-item {
.body--dark & {
color: $menu-icon-color-dark;
}
}
// Expanded state
&--expanded > .q-item {
background: rgba(0, 0, 0, 0.02);
.body--dark & {
background: rgba(255, 255, 255, 0.05);
}
.q-icon {
color: var(--q-primary);
transform: rotate(0deg);
@@ -215,52 +494,55 @@
}
}
// ==========================================
// Q-ITEM FOGLIA - Voce menu semplice
// ==========================================
// Q-ITEM
.q-item {
border-radius: 8px;
padding: 6px 10px;
margin-bottom: 2px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
min-height: 42px;
&:hover {
background: rgba(0, 0, 0, 0.05);
background: $menu-hover-bg;
transform: translateX(3px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
.body--dark & {
background: $menu-hover-bg-dark;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
&:active {
transform: translateX(1px);
}
// Sezione thumbnail con avatar
.q-item__section--thumbnail {
min-width: 36px;
padding-right: 10px;
}
// Label principale
> .q-item__section > span:not(.subtitle) {
font-size: 0.95rem;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
color: $menu-text-color;
.body--dark & {
color: $menu-text-color-dark;
}
}
}
// ==========================================
// Q-AVATAR - Icone migliorate
// ==========================================
// Q-AVATAR
.q-avatar {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: transparent !important;
&.rounded {
border-radius: 8px;
overflow: hidden;
}
.q-icon {
font-size: 1.3rem;
transition: all 0.3s ease;
@@ -269,212 +551,28 @@
.q-item:hover .q-avatar {
transform: scale(1.08);
.q-icon {
transform: rotate(-5deg);
}
}
// ==========================================
// MY-MENU-ACTIVE - Stato attivo
// INDENTATION TRANSITION
// ==========================================
.my-menu-active {
background: linear-gradient(90deg,
rgba(2, 123, 227, 0.12) 0%,
rgba(2, 123, 227, 0.05) 100%) !important;
border-radius: 8px;
border-right: 3px solid #027be3;
span {
color: #027be3;
font-weight: 600;
}
.q-avatar {
background: rgba(2, 123, 227, 0.1) !important;
.q-icon {
color: #027be3;
}
}
&:hover {
background: linear-gradient(90deg,
rgba(2, 123, 227, 0.16) 0%,
rgba(2, 123, 227, 0.08) 100%) !important;
}
[style*="paddingLeft"] {
transition: all 0.3s ease;
}
// ==========================================
// CLASSI PERMESSI - Badge colorati
// ==========================================
.isAdmin {
color: #d32f2f !important;
border-left: 3px solid #d32f2f;
background: linear-gradient(90deg,
rgba(211, 47, 47, 0.08) 0%,
rgba(211, 47, 47, 0.02) 100%);
.q-avatar {
background: rgba(211, 47, 47, 0.12) !important;
.q-icon {
color: #d32f2f;
}
}
&:hover {
background: linear-gradient(90deg,
rgba(211, 47, 47, 0.12) 0%,
rgba(211, 47, 47, 0.04) 100%);
}
}
.isManager {
color: #388e3c !important;
border-left: 3px solid #388e3c;
background: linear-gradient(90deg,
rgba(56, 142, 60, 0.08) 0%,
rgba(56, 142, 60, 0.02) 100%);
.q-avatar {
background: rgba(56, 142, 60, 0.12) !important;
.q-icon {
color: #388e3c;
}
}
&:hover {
background: linear-gradient(90deg,
rgba(56, 142, 60, 0.12) 0%,
rgba(56, 142, 60, 0.04) 100%);
}
}
.isSocioResidente {
color: #1b5e20 !important;
.q-avatar .q-icon {
color: #1b5e20;
}
}
.isFacilitatore {
color: #4a148c !important;
.q-avatar .q-icon {
color: #4a148c;
}
}
.onlyCollaboratore {
color: #f57c00 !important;
.q-avatar .q-icon {
color: #f57c00;
}
}
.isEditor {
color: #6a1b9a !important;
.q-avatar .q-icon {
color: #6a1b9a;
}
}
.isCommerciale {
color: #e65100 !important;
.q-avatar .q-icon {
color: #e65100;
}
}
.isGrafico {
color: #00796b !important;
.q-avatar .q-icon {
color: #00796b;
}
}
.isDoc {
border-left: 3px solid #42a5f5;
background: linear-gradient(90deg,
rgba(66, 165, 245, 0.08) 0%,
rgba(66, 165, 245, 0.02) 100%);
}
// ==========================================
// SUBTITLE - Testo secondario
// ==========================================
.subtitle {
font-size: 0.8rem;
color: #757575;
font-style: italic;
margin-top: 2px;
opacity: 0.85;
line-height: 1.3;
}
// ==========================================
// BIGMENU - Titoli grandi
// ==========================================
.bigmenu {
font-size: 1.2rem;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
letter-spacing: -0.01em;
}
// ==========================================
// CLASSI EXTRA - Supporto customizzazioni
// EXTRA CLASSES
// ==========================================
.extraclass {
// Placeholder per classi custom
}
// ==========================================
// RESPONSIVE
// ==========================================
@media screen and (max-width: 600px) {
.q-item {
padding: 5px 8px;
min-height: 40px;
}
.q-expansion-item > .q-item {
padding: 5px 8px;
}
.q-avatar {
width: 1.8rem !important;
height: 1.8rem !important;
font-size: 1.8rem !important;
.q-icon {
font-size: 1.1rem;
}
}
.q-item__section--thumbnail {
min-width: 32px;
padding-right: 8px;
}
.bigmenu {
font-size: 1.1rem;
}
.subtitle {
font-size: 0.75rem;
}
}
// ==========================================
// ANIMAZIONI ENTRATA
// ANIMATIONS
// ==========================================
@keyframes fadeSlideIn {
from {
@@ -493,11 +591,50 @@
}
// ==========================================
// FOCUS ACCESSIBILITA
// ACCESSIBILITY - Focus States
// ==========================================
.q-item:focus-visible,
.q-expansion-item:focus-visible {
outline: 2px solid var(--q-primary);
outline-offset: 2px;
border-radius: 8px;
.q-item,
.q-expansion-item {
&:focus-visible {
outline: 2px solid var(--q-primary);
outline-offset: 2px;
border-radius: 8px;
}
}
// ==========================================
// RESPONSIVE
// ==========================================
@media screen and (max-width: 600px) {
.q-item {
padding: 5px 8px;
min-height: 40px;
}
.q-expansion-item > .q-item {
padding: 5px 8px;
}
.q-avatar {
width: 1.8rem !important;
height: 1.8rem !important;
font-size: 1.8rem !important;
.q-icon {
font-size: 1.1rem;
}
}
.q-item__section--thumbnail {
min-width: 32px;
padding-right: 8px;
}
.bigmenu {
font-size: 1.1rem;
}
.subtitle {
font-size: 0.75rem;
}
}

View File

@@ -17,7 +17,7 @@ import { useCircuitStore } from '@store/CircuitStore'
export default defineComponent({
name: 'CMyUser',
components: { CSendCoins, CSaldo, CUserInfoAccount },
emits: ['setCmd', 'showInnerDialog'],
emits: ['setCmd', 'showInnerDialog', 'clickContact'],
props: {
mycontact: {
type: Object as PropType<IUserFields | null>,
@@ -72,6 +72,11 @@ export default defineComponent({
type: Boolean,
required: false,
default: false,
},
enableContactClick: {
type: Boolean,
required: false,
default: false,
}
},
@@ -128,13 +133,17 @@ export default defineComponent({
emit('showInnerDialog', showsendCoinTo.value)
}
function clickToUser(username: string) {
if (props.actionType === costanti.ACTIONTYPE.SEND_RIS)
naviga(`/my/` + username + '?sr=0')
else if (props.actionType === costanti.ACTIONTYPE.LINK_REG)
naviga(`/registrati/` + username)
else
naviga(`/my/` + username)
function clickToUser(username: string, recuser: any) {
if (props.enableContactClick) {
emit('clickContact', { username, recuser })
} else {
if (props.actionType === costanti.ACTIONTYPE.SEND_RIS)
naviga(`/my/` + username + '?sr=0')
else if (props.actionType === costanti.ACTIONTYPE.LINK_REG)
naviga(`/registrati/` + username)
else
naviga(`/my/` + username)
}
}
onMounted(mounted)

View File

@@ -6,7 +6,7 @@
>
<q-item-section
avatar
@click="clickToUser(contact.username)"
@click="clickToUser(contact.username, contact)"
>
<q-avatar size="60px">
<q-img
@@ -18,7 +18,7 @@
</q-avatar>
</q-item-section>
<q-item-section @click="clickToUser(contact.username)">
<q-item-section @click="clickToUser(contact.username, contact)">
<q-item-label v-if="labelextra && labelextra !== contact.username"
><strong>{{ labelextra }}</strong></q-item-label
>

View File

@@ -362,9 +362,7 @@ export default defineComponent({
}
} finally {
loading.value = false;
if (load) {
fine_caricamento();
}
fine_caricamento();
}
}
}

View File

@@ -1,6 +1,6 @@
import { ref, reactive } from 'vue';
import { useQuasar } from 'quasar';
import type { InvitoAmicoForm, InvitoAmicoResponse } from './invita-amico.types';
import type { InvitoAmicoForm, InvitoAmicoResponse } from '../../types/invita-amico.types';
// Composables
const $q = useQuasar();

View File

@@ -425,89 +425,131 @@ display: flex;
}
}
/* ---------------- Dialog annunci ---------------- */
.annunci-dialog {
width: min(560px, 92vw);
border-radius: $r-lg;
overflow: hidden;
}
max-width: 500px;
width: 90vw;
.dialog-header {
padding: $s-md;
.dialog-title-row {
.dialog-header {
background: $gradient-primary;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
padding: $s-lg;
.dialog-title {
margin: 0;
font-size: 1.3rem;
font-weight: 700;
}
}
.dialog-content {
padding: $s-md;
}
.annunci-options-mobile {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $s-sm;
}
.dialog-title {
margin: 0;
font-weight: 900;
font-size: 1.15rem;
.annuncio-option {
display: flex;
flex-direction: column;
align-items: center;
gap: $s-sm;
padding: $s-md;
border-radius: $r-lg;
cursor: pointer;
transition: all 0.3s ease;
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
min-height: 140px;
justify-content: center;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
&:hover::before {
left: 100%;
}
&:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
&:active {
transform: translateY(-2px) scale(1);
}
.q-icon {
opacity: 0.95;
}
.option-title {
font-size: 1.5rem;
font-weight: 700;
text-align: center;
line-height: 1.2;
}
.option-subtitle {
font-size: 1rem;
opacity: 0.9;
text-align: center;
line-height: 1.3;
}
&.gradient-indigo {
background: $gradient-indigo;
}
&.gradient-red {
background: $gradient-red;
}
&.gradient-lime {
background: $gradient-lime;
}
&.gradient-teal {
background: $gradient-teal;
}
}
.dialog-subtitle {
margin-top: 4px;
font-size: 0.9rem;
opacity: 0.72;
@media (max-width: 599px) {
.annuncio-option {
padding: $s-md;
min-height: 130px;
.q-icon {
font-size: 2rem !important;
}
.option-title {
font-size: 1.3rem;
}
.option-subtitle {
font-size: 0.9rem;
}
}
}
}
.annunci-list {
padding: 0;
}
.annunci-item {
min-height: 74px;
color: white;
border-radius: 0;
transition: filter 140ms ease;
position: relative;
overflow: hidden;
/* Quasar adds a light overlay on hover/focus; we force a darker overlay so white titles stay readable */
.q-focus-helper {
background: rgba(0, 0, 0, 0.22) !important;
opacity: 0;
transition: opacity 120ms ease;
}
&:hover .q-focus-helper,
&:focus .q-focus-helper,
&.q-item--active .q-focus-helper,
&.q-item--highlighted .q-focus-helper {
opacity: 1;
}
&:hover {
filter: saturate(1.06);
}
&.disabled {
opacity: 0.72;
}
.annunci-icon {
width: 44px;
height: 44px;
border-radius: 14px;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.20);
}
.option-title {
font-weight: 900;
letter-spacing: 0.01em;
}
.option-subtitle {
opacity: 0.92;
}
}
/* gradients used in list */
.gradient-green { background: linear-gradient(135deg, #22c55e, #16a34a); }

View File

@@ -136,14 +136,6 @@ export default defineComponent({
// TODO: aprire dialog/pagina ricezione RIS
};
const inviteFriend = () => {
// TODO: aprire dialog/pagina invito amico
};
const showMembers = () => {
// TODO: navigare alla lista iscritti
// $router.push('/members')
};
// Wallet
const refreshWallet = () => {
@@ -289,8 +281,6 @@ export default defineComponent({
goToProfile,
sendRIS,
receiveRIS,
inviteFriend,
showMembers,
refreshWallet,
goToTransactions,
goToAllEvents,

View File

@@ -173,130 +173,65 @@
>
<q-card class="annunci-dialog">
<q-card-section class="dialog-header">
<div class="dialog-title-row">
<h3 class="dialog-title">Scegli categoria</h3>
<q-btn
flat
round
dense
icon="close"
aria-label="Chiudi"
@click="showAnnunciDialog = false"
/>
</div>
<div class="dialog-subtitle">Apri la sezione giusta in un tap.</div>
<h3 class="dialog-title">Scegli Categoria</h3>
<q-btn
flat
round
dense
icon="close"
v-close-popup
/>
</q-card-section>
<q-separator />
<q-card-section class="dialog-content">
<div class="annunci-options-mobile">
<div
class="annuncio-option gradient-indigo"
@click="goToGoods"
>
<q-icon
name="fas fa-tshirt"
size="2.5rem"
/>
<span class="option-title">Beni</span>
<span class="option-subtitle">Autoproduzioni · Cibo · Oggetti</span>
</div>
<q-list class="annunci-list">
<q-item
clickable
v-ripple
class="annunci-item gradient-green"
@click="goToGoods"
>
<q-item-section avatar>
<div class="annunci-icon">
<q-icon
name="fas fa-tshirt"
size="1.4rem"
/>
</div>
</q-item-section>
<q-item-section>
<q-item-label class="option-title">Beni</q-item-label>
<q-item-label
caption
class="option-subtitle"
>
Autoproduzioni · Cibo · Oggetti
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="chevron_right" />
</q-item-section>
</q-item>
<div
class="annuncio-option gradient-red"
@click="goToServices"
>
<q-icon
name="fas fa-house-user"
size="2.5rem"
/>
<span class="option-title">Servizi</span>
<span class="option-subtitle">Competenze · Aiuti · Consulenze</span>
</div>
<q-item
clickable
v-ripple
class="annunci-item gradient-red"
@click="goToServices"
>
<q-item-section avatar>
<div class="annunci-icon">
<q-icon
name="fas fa-house-user"
size="1.4rem"
/>
</div>
</q-item-section>
<q-item-section>
<q-item-label class="option-title">Servizi</q-item-label>
<q-item-label
caption
class="option-subtitle"
>
Competenze · Aiuti · Consulenze
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="chevron_right" />
</q-item-section>
</q-item>
<div
class="annuncio-option gradient-lime"
@click="goToHospitality"
>
<q-icon
name="fas fa-bed"
size="2.5rem"
/>
<span class="option-title">Ospitalità</span>
<span class="option-subtitle">Ospitare · Viaggi · Accoglienza</span>
</div>
<q-item
clickable
v-ripple
class="annunci-item gradient-lime"
@click="goToHospitality"
>
<q-item-section avatar>
<div class="annunci-icon">
<q-icon
name="fas fa-bed"
size="1.4rem"
/>
</div>
</q-item-section>
<q-item-section>
<q-item-label class="option-title">Ospitalità</q-item-label>
<q-item-label
caption
class="option-subtitle"
>
Ospitare · Viaggi · Accoglienza
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="chevron_right" />
</q-item-section>
</q-item>
<q-item
class="annunci-item gradient-blue disabled"
disable
>
<q-item-section avatar>
<div class="annunci-icon">
<q-icon
name="commute"
size="1.4rem"
/>
</div>
</q-item-section>
<q-item-section>
<q-item-label class="option-title">Trasporti</q-item-label>
<q-item-label
caption
class="option-subtitle"
>
In arrivo
</q-item-label>
</q-item-section>
</q-item>
</q-list>
<div class="annuncio-option gradient-teal">
<q-icon
name="directions_car"
size="2.5rem"
/>
<span class="option-title">Trasporti</span>
<span class="option-subtitle">Condivisione viaggi</span>
<span class="option-subtitle"> (IN ARRIVO...)</span>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
</div>

View File

@@ -1,8 +1,15 @@
/* =========================================================
RISO Home (parte finale) — refresh estetico coerente
========================================================= */
$space-xs: 4px;
$space-sm: 8px;
$space-md: 12px;
$space-lg: 16px;
$space-xl: 20px;
$space-xxl: 24px;
.riso-modern-home {
padding: $s-md;
padding-bottom: calc(#{$s-md} + env(safe-area-inset-bottom));
@@ -28,6 +35,7 @@
}
:global(body.body--dark) {
.content-section,
.community-actions-section,
.footer-section-modern {
@@ -82,10 +90,12 @@
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.12);
border-radius: 999px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
@@ -344,6 +354,7 @@
/* ---- Footer ---- */
.footer-section-modern {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $s-sm;
@media (min-width: 700px) {
@@ -373,3 +384,159 @@
border: 1px solid rgba(255, 255, 255, 0.10);
}
}
// ========================================
// Community Actions Grid
// ========================================
.community-actions-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $space-md;
margin-bottom: $space-lg;
// Mobile: colonna singola se necessario
@media (max-width: 400px) {
grid-template-columns: 1fr;
gap: $space-sm;
}
}
.community-action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: $space-sm;
padding: $space-md $space-lg;
min-height: 48px;
font-size: 14px;
font-weight: 600;
text-transform: none;
box-shadow: var(--cm-shadow-sm);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: var(--cm-shadow-md);
}
&:active {
transform: translateY(0);
}
// Quasar icon spacing fix
:deep(.q-icon) {
margin-right: $space-xs;
}
// Dark mode adjustments
.body--dark & {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}
}
// ========================================
// Invite Friend Dialog
// ========================================
.invite-friend-dialog {
width: 100%;
max-width: 500px;
max-height: 90vh;
display: flex;
flex-direction: column;
border-radius: $radius-xl;
overflow: hidden;
// Mobile fullscreen
@media (max-width: 599px) {
max-width: 100%;
max-height: 100vh;
height: 100vh;
border-radius: 0;
}
// Header
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $space-lg $space-xl;
color: white;
flex-shrink: 0;
&.invite {
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
.body--dark & {
background: linear-gradient(135deg, #388e3c 0%, #1b5e20 100%);
}
}
.dialog-title {
display: flex;
align-items: center;
gap: $space-sm;
font-size: 18px;
font-weight: 700;
}
}
// Contenuto scrollabile
.dialog-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: $space-lg;
// Scrollbar styling
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
.body--dark & {
background: rgba(255, 255, 255, 0.05);
}
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.3);
}
.body--dark & {
background: rgba(255, 255, 255, 0.2);
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
}
}
// ========================================
// Fix Dialog Background - Light/Dark
// ========================================
.q-dialog__inner>.invite-friend-dialog {
background: #ffffff;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
}
.body--dark {
.q-dialog__inner>.invite-friend-dialog {
background: #1e293b;
.dialog-content {
background: #1e293b;
}
}
}

View File

@@ -1,6 +1,7 @@
import { defineComponent, ref, computed, watch, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { CRISBalanceBar } from '@/components/CRISBalanceBar';
import { InvitaAmico } from '@/components/InvitaAmico';
import { tools } from '@tools';
import { useGlobalStore, useUserStore } from 'app/src/store';
@@ -8,7 +9,7 @@ const isTest = true; // Cambia a false in produzione
export default defineComponent({
name: 'Riso_Home_ParteFinale',
components: { CRISBalanceBar },
components: { CRISBalanceBar, InvitaAmico },
setup() {
const $router = useRouter();
@@ -16,6 +17,7 @@ export default defineComponent({
const showAnnunciDialog = ref(false);
const showBannScambio = ref(true);
const walletSectionOpen = ref(false);
const showInviteFriend = ref(false);
const selectedCircuit = ref<'provinciale' | 'italia'>('provinciale');
const handshakesView = ref<'mine' | 'all'>('mine');
@@ -138,11 +140,12 @@ export default defineComponent({
const inviteFriend = () => {
// TODO: aprire dialog/pagina invito amico
showInviteFriend.value = true
};
const showMembers = () => {
// TODO: navigare alla lista iscritti
// $router.push('/members')
$router.push('/friends')
};
// Wallet
@@ -210,6 +213,10 @@ export default defineComponent({
// TODO: navigare a guida
$router.push('/guida')
};
const openPresentazione = () => {
// TODO: navigare a guida
$router.push('/presentazione')
};
const openInfo = () => {
// TODO: navigare a info
@@ -521,6 +528,7 @@ export default defineComponent({
// $router.push('/circuits')
};
loadTestData();
onMounted(() => {
@@ -609,6 +617,8 @@ export default defineComponent({
allTransactions,
userStore,
walletSectionOpen,
showInviteFriend,
openPresentazione,
};
},
});

View File

@@ -1,6 +1,5 @@
<template>
<q-page class="riso-modern-home riso-modern-home--tail">
<!-- Organizzazioni -->
<section
v-if="hasOrganizations"
@@ -125,6 +124,43 @@
</div>
</section>
<q-dialog
v-model="showInviteFriend"
:maximized="$q.screen.lt.sm"
transition-show="slide-up"
transition-hide="slide-down"
:persistent="false"
>
<q-card class="invite-friend-dialog">
<!-- Header con pulsante chiusura -->
<q-card-section class="dialog-header invite">
<div class="dialog-title">
<q-icon
name="person_add"
size="24px"
/>
<span>Invita Amici</span>
</div>
<q-btn
flat
round
dense
icon="close"
color="white"
@click="showInviteFriend = false"
/>
</q-card-section>
<!-- Contenuto scrollabile -->
<q-card-section class="dialog-content">
<InvitaAmico
@invito-inviato="showInviteFriend = false"
persistent
/>
</q-card-section>
</q-card>
</q-dialog>
<!-- Canali Telegram -->
<!--<section
v-if="hasTelegramLinks"
@@ -186,10 +222,10 @@
unelevated
rounded
class="footer-btn-modern"
icon="help_outline"
label="FAQ"
color="primary"
@click="openFAQ"
icon="present_to_all"
label="Presentazione"
color="accent"
@click="openPresentazione"
/>
<q-btn
unelevated
@@ -200,6 +236,15 @@
color="secondary"
@click="openGuide"
/>
<q-btn
unelevated
rounded
class="footer-btn-modern"
icon="help_outline"
label="Guida ai RIS"
color="primary"
@click="openFAQ"
/>
<q-btn
unelevated
rounded

View File

@@ -998,15 +998,23 @@ $space-xxl: 24px;
// ========================================
// Send/Receive/Confirm Dialogs
// ========================================
// ========================================
// Send/Receive/Confirm Dialogs - MODIFICATO
// ========================================
.send-dialog,
.confirm-dialog {
max-width: 500px;
width: 100%;
border-radius: $radius-xl;
overflow: hidden;
display: flex; // AGGIUNTO
flex-direction: column; // AGGIUNTO
max-height: 100vh; // AGGIUNTO
@media (max-width: 599px) {
max-width: 100%;
height: 100vh; // AGGIUNTO
max-height: 100vh; // AGGIUNTO
border-radius: 0;
}
}
@@ -1017,6 +1025,7 @@ $space-xxl: 24px;
align-items: center;
padding: $space-lg $space-xl;
color: white;
flex-shrink: 0; // AGGIUNTO - impedisce compressione
&.send {
background: $gradient-send;
@@ -1037,32 +1046,41 @@ $space-xxl: 24px;
.dialog-content {
padding: $space-xl;
flex: 1; // AGGIUNTO - occupa spazio disponibile
overflow-y: auto; // AGGIUNTO - abilita scroll
overflow-x: hidden; // AGGIUNTO
// AGGIUNTO - Scrollbar styling
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.3);
}
}
}
.dialog-actions {
padding: $space-md $space-xl $space-xl;
flex-shrink: 0; // AGGIUNTO - impedisce compressione
border-top: 1px solid var(--cm-border); // AGGIUNTO - separatore visivo
}
.info-banner {
display: flex;
gap: $space-md;
align-items: flex-start;
padding: $space-md;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: $radius-md;
margin-bottom: $space-lg;
p {
margin: 0;
font-size: 13px;
color: #2563eb;
}
}
// Contacts
// ========================================
// Contacts Section - MODIFICATO
// ========================================
.contacts-section {
margin-bottom: $space-lg;
// Rimosso max-height - ora gestito dal parent
.contacts-title {
display: flex;
@@ -1074,14 +1092,45 @@ $space-xxl: 24px;
letter-spacing: 0.5px;
color: var(--cm-text-hint);
margin-bottom: $space-md;
position: sticky; // AGGIUNTO - titolo sticky
top: 0; // AGGIUNTO
background: var(--cm-bg-card); // AGGIUNTO
padding: $space-sm 0; // AGGIUNTO
z-index: 1; // AGGIUNTO
}
}
.contacts-scrollable {
max-height: 300px;
overflow-y: auto;
// Rimosso max-height e overflow - gestito dal parent .dialog-content
display: flex;
flex-direction: column;
gap: $space-sm;
}
// ========================================
// Dark Mode per Scrollbar - AGGIUNTO
// ========================================
.body--dark {
.dialog-content {
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
.contacts-section .contacts-title {
background: #1e293b;
}
}
.contact-item {
display: flex;
align-items: center;
@@ -2237,4 +2286,5 @@ $space-xxl: 24px;
}
}
}
}
}

View File

@@ -999,6 +999,7 @@
transition-hide="slide-down"
>
<q-card class="send-dialog">
<!-- HEADER FISSO -->
<q-card-section class="dialog-header send">
<div class="dialog-title">
<q-icon
@@ -1017,123 +1018,151 @@
/>
</q-card-section>
<!-- CONTENUTO SCROLLABILE -->
<q-card-section class="dialog-content">
<!-- TOGGLE SELEZIONE VISTA CONTATTI -->
<q-btn-toggle
v-model="contactViewMode"
spread
no-caps
rounded
unelevated
toggle-color="primary"
color="grey-3"
text-color="grey-8"
class="contact-view-toggle q-mb-md"
:options="contactViewOptions"
/>
<!-- CAMPO DI RICERCA -->
<q-input
v-if="contactViewMode === 'recent'"
v-model="sendSearch"
label="Cerca destinatario..."
:label="searchPlaceholder"
outlined
dense
clearable
class="search-input"
@update:model-value="searchRecipients"
>
<template v-slot:prepend>
<template #prepend>
<q-icon name="search" />
</template>
</q-input>
<!-- Contatti recenti -->
<!-- VISTA: CONTATTI RECENTI -->
<div
v-if="recentContacts.length && !sendSearch"
v-if="contactViewMode === 'recent'"
class="contacts-section"
>
<div class="contacts-title">
<q-icon
name="history"
size="16px"
/>
<span>Usati di recente</span>
</div>
<div
v-for="contact in recentContacts"
:key="contact.username"
class="contact-item recent"
@click="selectRecipient(contact)"
>
<q-avatar
round
size="44px"
>
<img :src="userStore.getImgByProfile(contact)" />
<q-badge
v-if="tools.isUserOnline(contact)"
floating
color="green"
rounded
/>
</q-avatar>
<div class="contact-info">
<div
v-if="contact.name"
class="contact-name"
>
{{ contact.name }}
<span v-if="contact.surname">{{ contact.surname }}</span>
</div>
<div class="contact-username">@{{ contact.username }}</div>
</div>
<q-icon
name="chevron_right"
size="20px"
color="grey-6"
/>
</div>
</div>
<!-- Risultati ricerca -->
<div
v-if="sendSearch && filteredContacts.length"
class="contacts-section"
>
<div class="contacts-title">
<q-icon
name="people"
size="16px"
/>
<span>Risultati ricerca</span>
</div>
<div class="contacts-scrollable">
<div
v-for="contact in filteredContacts"
:key="contact.id"
class="contact-item"
@click="selectRecipient(contact)"
>
<q-avatar
size="44px"
:color="contact.avatarColor"
text-color="white"
>
{{ contact.initials }}
</q-avatar>
<div class="contact-info">
<div class="contact-name">{{ contact.name }}</div>
<div class="contact-username">@{{ contact.username }}</div>
</div>
<!-- Lista contatti recenti -->
<template v-if="filteredRecentContacts.length">
<div class="contacts-title">
<q-icon
name="chevron_right"
size="20px"
color="grey-6"
name="history"
size="16px"
/>
<span>Usati di recente</span>
<q-badge
color="primary"
:label="filteredRecentContacts.length"
class="q-ml-sm"
/>
</div>
<div class="contacts-scrollable">
<div
v-for="contact in filteredRecentContacts"
:key="contact.username"
class="contact-item recent"
@click="selectRecipient(contact)"
>
<q-avatar
round
size="44px"
>
<img :src="userStore.getImgByProfile(contact)" />
<q-badge
v-if="tools.isUserOnline(contact)"
floating
color="green"
rounded
/>
</q-avatar>
<div class="contact-info">
<div
v-if="contact.name"
class="contact-name"
>
{{ contact.name }}
<span v-if="contact.surname">{{ contact.surname }}</span>
</div>
<div class="contact-username">@{{ contact.username }}</div>
</div>
<q-icon
name="chevron_right"
size="20px"
color="grey-6"
/>
</div>
</div>
</template>
<!-- Nessun contatto recente -->
<div
v-else
class="no-results"
>
<q-icon
:name="sendSearch ? 'search_off' : 'history'"
size="56px"
color="grey-5"
/>
<p>
{{
sendSearch
? 'Nessun contatto recente trovato'
: 'Nessun contatto recente'
}}
</p>
<q-btn
v-if="!sendSearch"
flat
color="primary"
label="Cerca in tutti i contatti"
icon="people"
no-caps
@click="contactViewMode = 'all'"
/>
</div>
</div>
<!-- VISTA: TUTTI I CONTATTI -->
<div
v-if="sendSearch && !filteredContacts.length"
class="no-results"
v-else-if="contactViewMode === 'all'"
class="contacts-section"
>
<q-icon
name="search_off"
size="56px"
color="grey-5"
/>
<p>Nessun contatto trovato</p>
<CTitleBanner
class="q-pa-xs"
:title="$t('circuit.sendcoins')"
bgcolor="white"
bgcolor2="lightblue"
clcolor="text-indigo"
:canopen="true"
:small="true"
:open="true"
>
<CFindUsers
v-show="contactViewMode === 'all'"
:actionType="costanti.ACTIONTYPE.SEND_RIS"
:enableContactClick="true"
@clickContact="clickContact"
/>
</CTitleBanner>
</div>
</q-card-section>
</q-card>
</q-dialog>
<CSendCoins
v-if="showReceiveDialog"
mode="receive"
@@ -1238,6 +1267,7 @@ import { CRISBalanceBar } from '@/components/CRISBalanceBar';
import { CSingleMovement } from '@/components/CSingleMovement';
import { CSendCoins } from '@/components/CSendCoins';
import { CTitleBanner } from '@/components/CTitleBanner';
import { CFindUsers } from '@/components/CFindUsers';
import { CQRCode } from '@/components/CQRCode';
import { CCopyBtnSmall } from '@/components/CCopyBtnSmall';
import { CFinder } from '@/components/CFinder';
@@ -1272,6 +1302,61 @@ const exploreSearch = ref('');
// Transactions
const allTransactions = ref<IMovVisu[]>([]);
// ============================================
// STATE
// ============================================
const contactViewMode = ref('recent'); // 'recent' | 'all'
const sendSearch = ref('');
// ============================================
// COMPUTED
// ============================================
// Opzioni per il toggle
const contactViewOptions = computed(() => [
{
label: 'Recenti',
value: 'recent',
icon: 'history',
slot: 'recent',
},
{
label: 'Tutti',
value: 'all',
icon: 'people',
slot: 'all',
},
]);
// Placeholder dinamico per la ricerca
const searchPlaceholder = computed(() => {
return contactViewMode.value === 'recent'
? 'Cerca nei contatti recenti...'
: 'Cerca in tutti i contatti...';
});
// Contatti recenti filtrati
const filteredRecentContacts = computed(() => {
if (!sendSearch.value) {
return recentContacts.value;
}
const search = sendSearch.value.toLowerCase().trim();
return recentContacts.value.filter((contact) => {
const name = contact.name?.toLowerCase() || '';
const surname = contact.surname?.toLowerCase() || '';
const username = contact.username?.toLowerCase() || '';
return (
name.includes(search) ||
surname.includes(search) ||
username.includes(search) ||
`${name} ${surname}`.includes(search)
);
});
});
function configureShowcase() {
// Naviga alla configurazione vetrina
closeReceiveDialog();
@@ -1400,7 +1485,6 @@ const showConfirmSendDialog = ref(false);
const showConfirmReceiveDialog = ref(false);
// Send states
const sendSearch = ref('');
const selectedRecipient = ref<any>(null);
const filteredContacts = computed(() => {
@@ -1694,8 +1778,8 @@ const getOtherUserUsername = computed(() => {
if (!selectedMov.value) return '';
// Adatta in base alla tua struttura dati
return isSelectedMovSent.value
? selectedMov.value.to_username || getOtherUserName.value
: selectedMov.value.from_username || getOtherUserName.value;
? selectedMov.value.userto.username || getOtherUserName.value
: selectedMov.value.userfrom.username || getOtherUserName.value;
});
const getOtherUserAvatar = computed(() => {
@@ -1744,6 +1828,15 @@ const openPresentazione = () => {
const openGuidaRIS = () => {
$router.push('/faq_ris');
};
const clickContact = (data: any) => {
const recuser = data.recuser;
if (recuser) {
selectRecipient(recuser);
}
};
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,150 @@
.video-gallery {
max-width: 1400px;
margin: 0 auto;
.breadcrumb-section {
background: rgba(0, 0, 0, 0.02);
}
.section-title {
font-weight: 500;
}
.cursor-pointer {
cursor: pointer;
}
// Folder Cards
.folder-card {
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
// Video Cards
.video-card {
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.video-container {
position: relative;
padding-top: 56.25%; // 16:9
background: #000;
overflow: hidden;
cursor: pointer;
}
.video-preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
transition: background 0.3s ease;
&:hover {
background: rgba(0, 0, 0, 0.5);
}
}
// List View
.video-list {
.video-list-item {
transition: background 0.2s ease;
&:hover {
background: rgba(0, 0, 0, 0.02);
}
}
}
.video-thumbnail {
position: relative;
overflow: hidden;
border-radius: 4px;
video {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
}
}
// Empty State
.empty-state {
text-align: center;
padding: 60px 20px;
}
// Dialogs
.video-player-dialog {
background: #000;
.video-player-container {
height: calc(100vh - 50px);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.video-player {
max-width: 100%;
max-height: 100%;
}
}
.dialog-move {
min-width: 350px;
@media (max-width: 400px) {
min-width: 90vw;
}
}
}
// Dark Mode
.body--dark {
.video-gallery {
.breadcrumb-section {
background: rgba(255, 255, 255, 0.05);
}
.video-list-item:hover {
background: rgba(255, 255, 255, 0.05);
}
}
}

View File

@@ -0,0 +1,291 @@
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
import { useQuasar } from 'quasar';
import { videoService } from '@/services/videoService';
import type { IVideo, IFolder, IFolderOption } from '@/types/video.types';
interface IBreadcrumb {
name: string;
path: string;
}
export default defineComponent({
name: 'VideoGallery',
props: {
refreshTrigger: {
type: Number,
default: 0
}
},
setup(props) {
const $q = useQuasar();
// State
const loading = ref(false);
const currentPath = ref('');
const videos = ref<IVideo[]>([]);
const subfolders = ref<IFolder[]>([]);
const allFolders = ref<IFolderOption[]>([]);
const viewMode = ref<'grid' | 'list'>('grid');
// Video player
const showVideoDialog = ref(false);
const currentVideo = ref<IVideo | null>(null);
const videoPlayer = ref<HTMLVideoElement | null>(null);
// Move dialog
const showMoveDialog = ref(false);
const moveDestination = ref('');
const videoToMove = ref<IVideo | null>(null);
const moving = ref(false);
// Computed
const breadcrumbs = computed<IBreadcrumb[]>(() => {
if (!currentPath.value) return [];
const parts = currentPath.value.split('/');
return parts.map((part, index) => ({
name: part,
path: parts.slice(0, index + 1).join('/')
}));
});
// Methods
const loadContent = async (): Promise<void> => {
loading.value = true;
try {
const response = await videoService.getVideos(currentPath.value);
videos.value = response.data?.videos || [];
subfolders.value = response.data?.folders || [];
} catch (error) {
$q.notify({
type: 'negative',
message: 'Errore nel caricamento dei contenuti'
});
} finally {
loading.value = false;
}
};
const loadAllFolders = async (): Promise<void> => {
try {
const response = await videoService.getFolders();
const folders = response.data?.folders || [];
allFolders.value = [
{ label: 'Root', value: '' },
...folders.map(f => ({
label: f.path,
value: f.path
}))
];
} catch (error) {
console.error('Error loading folders:', error);
}
};
const navigateTo = (path: string): void => {
currentPath.value = path;
loadContent();
};
const toggleViewMode = (): void => {
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid';
};
const openVideo = (video: IVideo): void => {
currentVideo.value = video;
showVideoDialog.value = true;
};
const getVideoUrl = (path: string): string => {
return videoService.getVideoUrl(path);
};
const downloadVideo = (video: IVideo): void => {
const link = document.createElement('a');
link.href = getVideoUrl(video.path);
link.download = video.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const renameVideo = (video: IVideo): void => {
$q.dialog({
title: 'Rinomina Video',
message: 'Inserisci il nuovo nome del file:',
prompt: {
model: video.filename,
type: 'text'
},
cancel: true,
persistent: true
}).onOk(async (newName: string) => {
if (!newName.trim() || newName === video.filename) return;
try {
await videoService.renameVideo(video.folder || currentPath.value, video.filename, newName);
$q.notify({ type: 'positive', message: 'Video rinominato!' });
loadContent();
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.response?.data?.error || 'Errore durante la rinomina'
});
}
});
};
const moveVideo = async (video: IVideo): Promise<void> => {
await loadAllFolders();
videoToMove.value = video;
moveDestination.value = '';
showMoveDialog.value = true;
};
const confirmMoveVideo = async (): Promise<void> => {
if (!videoToMove.value) return;
moving.value = true;
try {
await videoService.moveVideo(
videoToMove.value.folder || currentPath.value,
videoToMove.value.filename,
moveDestination.value
);
$q.notify({ type: 'positive', message: 'Video spostato!' });
showMoveDialog.value = false;
loadContent();
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.response?.data?.error || 'Errore durante lo spostamento'
});
} finally {
moving.value = false;
}
};
const confirmDeleteVideo = (video: IVideo): void => {
$q.dialog({
title: 'Conferma Eliminazione',
message: `Sei sicuro di voler eliminare "${video.filename}"?`,
cancel: true,
persistent: true,
color: 'negative'
}).onOk(async () => {
try {
await videoService.deleteVideo(video.folder || currentPath.value, video.filename);
$q.notify({ type: 'positive', message: 'Video eliminato!' });
loadContent();
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.response?.data?.error || 'Errore durante l\'eliminazione'
});
}
});
};
const renameFolder = (folder: IFolder): void => {
$q.dialog({
title: 'Rinomina Cartella',
message: 'Inserisci il nuovo nome:',
prompt: {
model: folder.name,
type: 'text'
},
cancel: true,
persistent: true
}).onOk(async (newName: string) => {
if (!newName.trim() || newName === folder.name) return;
try {
await videoService.renameFolder(folder.path, newName);
$q.notify({ type: 'positive', message: 'Cartella rinominata!' });
loadContent();
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.response?.data?.error || 'Errore durante la rinomina'
});
}
});
};
const confirmDeleteFolder = (folder: IFolder): void => {
$q.dialog({
title: 'Conferma Eliminazione',
message: `Sei sicuro di voler eliminare la cartella "${folder.name}" e tutto il suo contenuto?`,
cancel: true,
persistent: true,
color: 'negative'
}).onOk(async () => {
try {
await videoService.deleteFolder(folder.path);
$q.notify({ type: 'positive', message: 'Cartella eliminata!' });
loadContent();
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.response?.data?.error || 'Errore durante l\'eliminazione'
});
}
});
};
const formatFileSize = (bytes: number): string => {
return videoService.formatFileSize(bytes);
};
const formatDate = (dateString: string): string => {
return videoService.formatDate(dateString);
};
// Watchers
watch(() => props.refreshTrigger, () => {
loadContent();
});
// Lifecycle
onMounted(() => {
loadContent();
});
return {
// State
loading,
currentPath,
videos,
subfolders,
allFolders,
viewMode,
showVideoDialog,
currentVideo,
videoPlayer,
showMoveDialog,
moveDestination,
moving,
// Computed
breadcrumbs,
// Methods
loadContent,
navigateTo,
toggleViewMode,
openVideo,
getVideoUrl,
downloadVideo,
renameVideo,
moveVideo,
confirmMoveVideo,
confirmDeleteVideo,
renameFolder,
confirmDeleteFolder,
formatFileSize,
formatDate
};
}
});

View File

@@ -0,0 +1,271 @@
<template>
<q-card class="video-gallery">
<q-card-section>
<div class="row items-center justify-between">
<div class="text-h6">
<q-icon name="video_library" class="q-mr-sm" />
Galleria Video
</div>
<div class="row q-gutter-sm">
<q-btn
flat
round
:icon="viewMode === 'grid' ? 'view_list' : 'grid_view'"
@click="toggleViewMode"
>
<q-tooltip>Cambia vista</q-tooltip>
</q-btn>
<q-btn
flat
round
icon="refresh"
:loading="loading"
@click="loadContent"
/>
</div>
</div>
</q-card-section>
<q-separator />
<!-- Breadcrumb -->
<q-card-section class="q-py-sm breadcrumb-section">
<q-breadcrumbs>
<q-breadcrumbs-el
icon="home"
label="Root"
class="cursor-pointer"
@click="navigateTo('')"
/>
<q-breadcrumbs-el
v-for="(crumb, index) in breadcrumbs"
:key="index"
:label="crumb.name"
class="cursor-pointer"
@click="navigateTo(crumb.path)"
/>
</q-breadcrumbs>
</q-card-section>
<q-separator />
<q-card-section>
<!-- Loading -->
<div v-if="loading" class="text-center q-pa-lg">
<q-spinner-orbit size="50px" color="primary" />
<div class="q-mt-md">Caricamento...</div>
</div>
<!-- Content -->
<div v-else>
<!-- Folders -->
<div v-if="subfolders.length > 0" class="q-mb-lg">
<div class="text-subtitle1 text-grey-7 q-mb-sm section-title">
<q-icon name="folder" class="q-mr-xs" />
Cartelle
</div>
<div class="row q-gutter-md">
<div
v-for="folder in subfolders"
:key="folder.path"
class="col-6 col-sm-4 col-md-3 col-lg-2"
>
<q-card
class="folder-card cursor-pointer"
flat
bordered
@click="navigateTo(folder.path)"
>
<q-card-section class="text-center">
<q-icon name="folder" size="48px" color="amber" />
<div class="text-subtitle2 q-mt-sm ellipsis">
{{ folder.name }}
</div>
</q-card-section>
<q-menu touch-position context-menu>
<q-list dense>
<q-item v-close-popup clickable @click="renameFolder(folder)">
<q-item-section avatar>
<q-icon name="edit" color="primary" />
</q-item-section>
<q-item-section>Rinomina</q-item-section>
</q-item>
<q-item v-close-popup clickable @click="confirmDeleteFolder(folder)">
<q-item-section avatar>
<q-icon name="delete" color="negative" />
</q-item-section>
<q-item-section>Elimina</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-card>
</div>
</div>
</div>
<!-- Videos -->
<div v-if="videos.length > 0">
<div class="text-subtitle1 text-grey-7 q-mb-sm section-title">
<q-icon name="movie" class="q-mr-xs" />
Video ({{ videos.length }})
</div>
<!-- Grid View -->
<div v-if="viewMode === 'grid'" class="row q-gutter-md">
<div
v-for="video in videos"
:key="video.id"
class="col-12 col-sm-6 col-md-4 col-lg-3"
>
<q-card class="video-card">
<div class="video-container" @click="openVideo(video)">
<video
:src="getVideoUrl(video.path)"
class="video-preview"
preload="metadata"
/>
<div class="play-overlay">
<q-icon name="play_circle" size="64px" color="white" />
</div>
</div>
<q-card-section class="q-py-sm">
<div class="text-subtitle2 ellipsis">{{ video.filename }}</div>
<div class="text-caption text-grey">
{{ formatFileSize(video.size) }}
{{ formatDate(video.createdAt) }}
</div>
</q-card-section>
<q-separator />
<q-card-actions>
<q-btn flat round icon="play_arrow" color="primary" @click="openVideo(video)">
<q-tooltip>Riproduci</q-tooltip>
</q-btn>
<q-btn flat round icon="download" color="secondary" @click="downloadVideo(video)">
<q-tooltip>Scarica</q-tooltip>
</q-btn>
<q-space />
<q-btn flat round icon="more_vert">
<q-menu>
<q-list dense>
<q-item v-close-popup clickable @click="renameVideo(video)">
<q-item-section avatar>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Rinomina</q-item-section>
</q-item>
<q-item v-close-popup clickable @click="moveVideo(video)">
<q-item-section avatar>
<q-icon name="drive_file_move" />
</q-item-section>
<q-item-section>Sposta</q-item-section>
</q-item>
<q-separator />
<q-item v-close-popup clickable @click="confirmDeleteVideo(video)">
<q-item-section avatar>
<q-icon name="delete" color="negative" />
</q-item-section>
<q-item-section class="text-negative">Elimina</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</q-card-actions>
</q-card>
</div>
</div>
<!-- List View -->
<q-list v-else separator class="video-list">
<q-item v-for="video in videos" :key="video.id" class="video-list-item">
<q-item-section avatar>
<q-avatar square size="60px" class="video-thumbnail">
<video :src="getVideoUrl(video.path)" preload="metadata" />
<div class="thumbnail-overlay">
<q-icon name="play_arrow" color="white" />
</div>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ video.filename }}</q-item-label>
<q-item-label caption>
{{ formatFileSize(video.size) }} {{ formatDate(video.createdAt) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<div class="row q-gutter-xs">
<q-btn flat round dense icon="play_arrow" color="primary" @click="openVideo(video)" />
<q-btn flat round dense icon="download" color="secondary" @click="downloadVideo(video)" />
<q-btn flat round dense icon="delete" color="negative" @click="confirmDeleteVideo(video)" />
</div>
</q-item-section>
</q-item>
</q-list>
</div>
<!-- Empty State -->
<div v-if="!loading && videos.length === 0 && subfolders.length === 0" class="empty-state">
<q-icon name="folder_off" size="80px" color="grey-5" />
<div class="text-h6 text-grey-6 q-mt-md">Nessun contenuto</div>
<div class="text-grey-5">
Questa cartella è vuota. Carica dei video per iniziare.
</div>
</div>
</div>
</q-card-section>
<!-- Video Player Dialog -->
<q-dialog v-model="showVideoDialog" maximized transition-show="fade" transition-hide="fade">
<q-card class="video-player-dialog">
<q-bar class="bg-grey-9">
<div class="text-white ellipsis">{{ currentVideo?.filename }}</div>
<q-space />
<q-btn v-close-popup dense flat icon="close" color="white" />
</q-bar>
<q-card-section class="video-player-container">
<video
v-if="currentVideo"
ref="videoPlayer"
:src="getVideoUrl(currentVideo.path)"
controls
autoplay
class="video-player"
/>
</q-card-section>
</q-card>
</q-dialog>
<!-- Move Video Dialog -->
<q-dialog v-model="showMoveDialog" persistent>
<q-card class="dialog-move">
<q-card-section>
<div class="text-h6">Sposta Video</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-select
v-model="moveDestination"
:options="allFolders"
label="Cartella di destinazione"
outlined
emit-value
map-options
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-close-popup flat label="Annulla" />
<q-btn color="primary" label="Sposta" :loading="moving" @click="confirmMoveVideo" />
</q-card-actions>
</q-card>
</q-dialog>
</q-card>
</template>
<script lang="ts" src="./VideoGallery.ts" />
<style lang="scss" src="./VideoGallery.scss" scoped />

View File

@@ -0,0 +1,73 @@
.video-uploader {
max-width: 800px;
margin: 0 auto;
.btn-new-folder {
height: 56px;
}
.dropzone {
border: 2px dashed #ccc;
border-radius: 8px;
min-height: 120px;
transition: border-color 0.3s ease;
&:hover {
border-color: var(--q-primary);
}
&:deep(.q-field__control) {
min-height: 120px;
}
}
.file-chip {
max-width: 100%;
.ellipsis {
max-width: 200px;
}
}
.upload-queue {
background: rgba(0, 0, 0, 0.02);
border-radius: 8px;
.q-item {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&:last-child {
border-bottom: none;
}
}
}
.dialog-new-folder {
min-width: 350px;
@media (max-width: 400px) {
min-width: 90vw;
}
}
}
// Dark mode support
.body--dark {
.video-uploader {
.dropzone {
border-color: rgba(255, 255, 255, 0.3);
&:hover {
border-color: var(--q-primary);
}
}
.upload-queue {
background: rgba(255, 255, 255, 0.05);
.q-item {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
}
}
}

View File

@@ -0,0 +1,199 @@
import { defineComponent, ref, computed, onMounted } from 'vue';
import { useQuasar } from 'quasar';
import { videoService } from '@/services/videoService';
import {
IFolder,
IFolderOption,
IUploadQueueItem,
UploadStatus,
UPLOAD_STATUS_CONFIG,
MAX_FILES
} from '@/types/video.types';
export default defineComponent({
name: 'VideoUploader',
emits: ['upload-complete'],
setup(props, { emit }) {
const $q = useQuasar();
// State
const selectedFiles = ref<File[] | null>(null);
const selectedFolder = ref<string>('');
const folders = ref<IFolder[]>([]);
const loadingFolders = ref(false);
const uploadQueue = ref<IUploadQueueItem[]>([]);
const isUploading = ref(false);
// Dialog state
const showNewFolderDialog = ref(false);
const newFolderName = ref('');
const newFolderParent = ref('');
const creatingFolder = ref(false);
// Constants
const maxFiles = MAX_FILES;
// Computed
const folderOptions = computed<IFolderOption[]>(() => [
{ label: '📁 Root (principale)', value: '' },
...folders.value.map(f => ({
label: `${' '.repeat((f.level || 1) - 1)}📂 ${f.name}`,
value: f.path
}))
]);
const parentFolderOptions = computed<IFolderOption[]>(() => [
{ label: 'Root', value: '' },
...folderOptions.value.slice(1)
]);
// Methods
const loadFolders = async (): Promise<void> => {
loadingFolders.value = true;
try {
const response = await videoService.getFolders();
folders.value = response.data?.folders || [];
} catch (error) {
$q.notify({
type: 'negative',
message: 'Errore nel caricamento delle cartelle'
});
} finally {
loadingFolders.value = false;
}
};
const onFilesSelected = (files: File[] | null): void => {
if (!files) return;
const fileArray = Array.isArray(files) ? files : [files];
uploadQueue.value = fileArray.map(file => ({
file,
progress: 0,
status: 'pending' as UploadStatus
}));
};
const startUpload = async (): Promise<void> => {
if (uploadQueue.value.length === 0) return;
isUploading.value = true;
let completedCount = 0;
for (const item of uploadQueue.value) {
if (item.status === 'complete') continue;
item.status = 'uploading';
try {
await videoService.uploadVideo(
item.file,
selectedFolder.value || 'default',
(progress: number) => {
item.progress = progress;
}
);
item.status = 'complete';
item.progress = 100;
completedCount++;
} catch (error: any) {
item.status = 'error';
item.error = error.response?.data?.error || error.message;
$q.notify({
type: 'negative',
message: `Errore upload ${item.file.name}: ${item.error}`
});
}
}
isUploading.value = false;
if (completedCount > 0) {
$q.notify({
type: 'positive',
message: `${completedCount} video caricati con successo!`
});
emit('upload-complete');
}
};
const createNewFolder = async (): Promise<void> => {
if (!newFolderName.value.trim()) {
$q.notify({
type: 'warning',
message: 'Inserisci un nome per la cartella'
});
return;
}
creatingFolder.value = true;
try {
await videoService.createFolder(newFolderName.value, newFolderParent.value);
$q.notify({ type: 'positive', message: 'Cartella creata!' });
await loadFolders();
selectedFolder.value = newFolderParent.value
? `${newFolderParent.value}/${newFolderName.value}`
: newFolderName.value;
showNewFolderDialog.value = false;
newFolderName.value = '';
newFolderParent.value = '';
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.response?.data?.error || 'Errore nella creazione della cartella'
});
} finally {
creatingFolder.value = false;
}
};
const clearQueue = (): void => {
uploadQueue.value = [];
selectedFiles.value = null;
};
const formatFileSize = (bytes: number): string => {
return videoService.formatFileSize(bytes);
};
const getStatusConfig = (status: UploadStatus) => {
return UPLOAD_STATUS_CONFIG[status];
};
// Lifecycle
onMounted(() => {
loadFolders();
});
return {
// State
selectedFiles,
selectedFolder,
loadingFolders,
uploadQueue,
isUploading,
showNewFolderDialog,
newFolderName,
newFolderParent,
creatingFolder,
maxFiles,
// Computed
folderOptions,
parentFolderOptions,
// Methods
onFilesSelected,
startUpload,
createNewFolder,
clearQueue,
formatFileSize,
getStatusConfig
};
}
});

View File

@@ -0,0 +1,181 @@
<template>
<q-card class="video-uploader">
<q-card-section>
<div class="text-h6">
<q-icon name="cloud_upload" class="q-mr-sm" />
Carica Video
</div>
</q-card-section>
<q-separator />
<q-card-section>
<!-- Selezione Cartella -->
<div class="row q-gutter-md q-mb-md">
<div class="col-12 col-md-8">
<q-select
v-model="selectedFolder"
:options="folderOptions"
label="Seleziona Cartella di Destinazione"
outlined
emit-value
map-options
:loading="loadingFolders"
>
<template #prepend>
<q-icon name="folder" />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-icon name="folder" color="amber" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-md-4">
<q-btn
color="secondary"
icon="create_new_folder"
label="Nuova Cartella"
class="full-width btn-new-folder"
@click="showNewFolderDialog = true"
/>
</div>
</div>
<!-- Dropzone -->
<q-file
v-model="selectedFiles"
label="Trascina i video qui o clicca per selezionare"
outlined
multiple
counter
accept="video/*"
:max-files="maxFiles"
class="dropzone"
@update:model-value="onFilesSelected"
>
<template #prepend>
<q-icon name="videocam" />
</template>
<template #file="{ file }">
<q-chip class="full-width q-my-xs file-chip" square>
<q-avatar>
<q-icon name="movie" />
</q-avatar>
<div class="ellipsis relative-position">
{{ file.name }}
<q-tooltip>{{ file.name }}</q-tooltip>
</div>
<q-chip dense class="q-ml-sm" color="primary" text-color="white">
{{ formatFileSize(file.size) }}
</q-chip>
</q-chip>
</template>
</q-file>
<!-- Lista Upload Queue -->
<q-list v-if="uploadQueue.length > 0" class="q-mt-md upload-queue">
<q-item v-for="(item, index) in uploadQueue" :key="index">
<q-item-section avatar>
<q-icon
:name="getStatusConfig(item.status).icon"
:color="getStatusConfig(item.status).color"
/>
</q-item-section>
<q-item-section>
<q-item-label>{{ item.file.name }}</q-item-label>
<q-item-label caption>
{{ formatFileSize(item.file.size) }}
</q-item-label>
<q-linear-progress
v-if="item.status === 'uploading'"
:value="item.progress / 100"
color="primary"
class="q-mt-sm"
/>
<q-item-label v-if="item.error" caption class="text-negative">
{{ item.error }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-badge
:color="getStatusConfig(item.status).color"
:label="getStatusConfig(item.status).label"
/>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<q-btn
flat
label="Pulisci"
color="grey"
:disable="isUploading"
@click="clearQueue"
/>
<q-btn
color="primary"
icon="cloud_upload"
label="Carica Video"
:loading="isUploading"
:disable="uploadQueue.length === 0 || !selectedFolder"
@click="startUpload"
/>
</q-card-actions>
<!-- Dialog Nuova Cartella -->
<q-dialog v-model="showNewFolderDialog" persistent>
<q-card class="dialog-new-folder">
<q-card-section>
<div class="text-h6">Nuova Cartella</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-input
v-model="newFolderName"
label="Nome Cartella"
autofocus
outlined
@keyup.enter="createNewFolder"
/>
<q-select
v-model="newFolderParent"
:options="parentFolderOptions"
label="Cartella Padre (opzionale)"
outlined
emit-value
map-options
class="q-mt-md"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-close-popup flat label="Annulla" />
<q-btn
color="primary"
label="Crea"
:loading="creatingFolder"
@click="createNewFolder"
/>
</q-card-actions>
</q-card>
</q-dialog>
</q-card>
</template>
<script lang="ts" src="./VideoUploader.ts" />
<style lang="scss" src="./VideoUploader.scss" scoped />

View File

@@ -85,6 +85,7 @@ const msg_website_it = {
eventodef: 'Evento:',
prova: 'prova',
dbop: 'Operazioni',
VideoPage: 'Video',
projall: 'Comunitari',
groups: 'Lista Gruppi',
projectsShared: 'Condivisi da me',

View File

@@ -250,6 +250,7 @@ export interface IUserFields {
made_gift?: boolean
tokens?: IToken[]
date_reg?: Date
date_deleted?: Date
lasttimeonline?: Date
profile: IUserProfile
qualified?: boolean

16
src/pages/VideosPage.scss Normal file
View File

@@ -0,0 +1,16 @@
.videos-page {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
.container {
max-width: 1400px;
margin: 0 auto;
}
}
.body--dark {
.videos-page {
background: #1d1d1d;
}
}

25
src/pages/VideosPage.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineComponent, ref } from 'vue';
import VideoUploader from '@/components/video/VideoUploader.vue';
import VideoGallery from '@/components/video/VideoGallery.vue';
export default defineComponent({
name: 'VideosPage',
components: {
VideoUploader,
VideoGallery
},
setup() {
const refreshTrigger = ref(0);
const onUploadComplete = (): void => {
refreshTrigger.value++;
};
return {
refreshTrigger,
onUploadComplete
};
}
});

11
src/pages/VideosPage.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<q-page class="videos-page">
<div class="container">
<VideoUploader @upload-complete="onUploadComplete" />
<VideoGallery :refresh-trigger="refreshTrigger" class="q-mt-lg" />
</div>
</q-page>
</template>
<script lang="ts" src="./VideosPage.ts" />
<style lang="scss" src="./VideosPage.scss" scoped />

View File

@@ -196,6 +196,15 @@
mykey="Email"
:myvalue="myuser.email"
/>
<CKeyAndValue
mykey="Registrata il"
:mydate="myuser.date_reg"
/>
<CKeyAndValue
v-if="myuser.date_deleted"
mykey="Cancellato il"
:mydate="myuser.date_deleted"
/>
<CKeyAndValue
mykey="Email Verificata"
:myvalue="myuser.verified_email"

View File

@@ -43,6 +43,19 @@ function getRoutesAd(site: ISites) {
submenu: true,
onlyAdmin: true
},
{
active: true,
order: 125,
path: '/admin/videos',
materialIcon: 'fas fa-video',
name: 'pages.VideoPage',
component: () => import('@/pages/VideosPage.vue'),
meta: { requiresAuth: true },
inmenu: false,
onlyManager: true,
onlyAdmin: true,
infooter: false,
},
{
active: true,
order: 1020,

View File

@@ -0,0 +1,167 @@
import { Api } from '@/store/Api';
const BASE_URL = process.env.VITE_MONGODB_HOST;
export const videoService = {
// ============ FOLDER METHODS ============
async getFolders() {
const response = await Api.SendReq('/api/video/folders', 'GET');
return response.data;
},
async createFolder(folderName, parentPath = '') {
const response = await Api.SendReq('/api/video/folders', 'POST', {
folderName,
parentPath,
});
return response.data;
},
async renameFolder(folderPath, newName) {
const response = await Api.SendReq(`/api/video/folders/${folderPath}`, 'PUT', {
newName,
});
return response.data;
},
async deleteFolder(folderPath) {
const response = await Api.SendReq(`/api/video/folders/${folderPath}`, 'DELETE');
return response.data;
},
// ============ VIDEO METHODS ============
async getVideos(folder = '') {
const response = await Api.SendReq('/api/video/videos', 'GET', { folder });
return response.data;
},
async uploadVideo(file, folder, onProgress) {
const formData = new FormData();
formData.append('video', file);
// ✅ Folder come query parameter nell'URL
const targetFolder = encodeURIComponent(folder || 'default');
const response = await Api.SendReq(
`/api/video/videos/upload?folder=${targetFolder}`,
'POSTFORMDATA',
null,
false,
false,
1,
5000,
formData,
null,
{
timeout: 600000,
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percentCompleted);
}
},
}
);
return response.data;
},
async uploadVideos(files, folder, onProgress) {
const formData = new FormData();
files.forEach((file) => formData.append('videos', file));
// ✅ Folder come query parameter
const targetFolder = encodeURIComponent(folder || 'default');
const response = await Api.SendReq(
`/api/video/videos/upload-multiple?folder=${targetFolder}`,
'POSTFORMDATA',
null,
false,
false,
1,
5000,
formData,
null,
{
timeout: 600000,
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percentCompleted);
}
},
}
);
return response.data;
},
async renameVideo(folder, filename, newFilename) {
const response = await Api.SendReq(
`/api/video/videos/${folder}/${filename}/rename`,
'PUT',
{ newFilename }
);
return response.data;
},
async moveVideo(folder, filename, destinationFolder) {
const response = await Api.SendReq(
`/api/video/videos/${folder}/${filename}/move`,
'PUT',
{ destinationFolder }
);
return response.data;
},
async deleteVideo(folder, filename) {
const response = await Api.SendReq(
`/api/video/videos/${folder}/${filename}`,
'DELETE'
);
return response.data;
},
// ============ UTILITY METHODS ============
getVideoUrl(videoPath) {
return `${BASE_URL}${videoPath}`;
},
getStreamUrl(folder, filename) {
return `${BASE_URL}/api/video/stream/${folder}/${filename}`;
},
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
},
formatDate(dateString) {
if (!dateString) return 'N/A';
try {
return new Date(dateString).toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return 'N/A';
}
},
};
export default videoService;

View File

@@ -1,23 +1,36 @@
import axios, {
AxiosInstance, AxiosPromise, AxiosResponse, AxiosInterceptorManager,
} from 'axios'
import { Api } from '@api'
import type * as Types from '@/store/Api/ApiTypes'
AxiosInstance,
AxiosPromise,
AxiosResponse,
AxiosInterceptorManager,
} from 'axios';
import { Api } from '@api';
import type * as Types from '@/store/Api/ApiTypes';
// Funzione che smista la richiesta in base al metodo
async function sendRequest(url, method, mydata, myformdata = null, responsedata = null, options = null) {
async function sendRequest(
url: any,
method: string,
mydata: any,
myformdata: any = null,
responsedata: any = null,
options: any = null
) {
const actions = {
get: () => Api.get(url, mydata, responsedata),
post: () => Api.post(url, mydata, responsedata, options),
postformdata: () => Api.postFormData(url, myformdata, responsedata),
postformdata: () => Api.postFormData(url, myformdata, responsedata, options), // ✅ Aggiunto options
delete: () => Api.Delete(url, mydata, responsedata),
put: () => Api.put(url, mydata, responsedata),
patch: () => Api.patch(url, mydata, responsedata),
};
const key = method.toLowerCase();
if (actions[key]) return await actions[key]();
if (actions[key]) {
return await actions[key]();
}
throw new Error(`Metodo non supportato: ${method}`);
}
export default sendRequest
export default sendRequest;

View File

@@ -145,7 +145,17 @@ async function Request(
// ✅ AGGIUNGI IL TIMEOUT DALLE OPTIONS
if (options?.timeout) {
config.timeout = options.timeout; // in millisecondi (es. 300000 = 5 minuti)
config.timeout = options.timeout;
}
// ✅ AGGIUNGI SUPPORTO PER onUploadProgress
if (options?.onUploadProgress) {
config.onUploadProgress = options.onUploadProgress;
}
// ✅ AGGIUNGI SUPPORTO PER onDownloadProgress (opzionale)
if (options?.onDownloadProgress) {
config.onDownloadProgress = options.onDownloadProgress;
}
if (options?.stream) config.responseType = 'stream';
@@ -210,7 +220,6 @@ async function Request(
},
...responsedata,
});*/
} else if (type === 'postFormData') {
response = await axiosInstance.post(path, payload, config);
} else {
@@ -221,11 +230,9 @@ async function Request(
// Gestione aggiornamento token se necessario
//const setAuthToken = path === '/updatepwd' || path === '/users/login';
const setAuthToken = !!x_auth_token;
if (
response && setAuthToken
) {
if (response && setAuthToken) {
const refreshToken = String(response.headers['x-refrtok'] || '');
const browser_random = userStore.getBrowserRandom()
const browser_random = userStore.getBrowserRandom();
if (!x_auth_token) {
userStore.setServerCode(toolsext.ERR_AUTHENTICATION);
}
@@ -237,7 +244,7 @@ async function Request(
userStore.setAuth(x_auth_token, refreshToken, browser_random);
localStorage.setItem(toolsext.localStorage.token, x_auth_token);
localStorage.setItem(toolsext.localStorage.refreshToken, refreshToken);
localStorage.setItem(toolsext.localStorage. browser_random, browser_random);
localStorage.setItem(toolsext.localStorage.browser_random, browser_random);
}
globalStore.setStateConnection('online');
@@ -245,7 +252,7 @@ async function Request(
return new Types.AxiosSuccess(response.data, response.status);
} catch (error) {
// Aggiornamento asincrono dello stato di connessione (setTimeout per dare tempo a eventuali animazioni)
console.error('Errore funzione Request', error)
console.error('Errore funzione Request', error);
setTimeout(() => {
if (['get'].includes(type.toLowerCase())) {
globalStore.connData.downloading_server =

View File

@@ -49,11 +49,11 @@ export const Api = {
return await Request('post', path, payload, responsedata, options);
},
async postFormData(path: string, payload?: any, responsedata?: any) {
const globalStore = useGlobalStore();
globalStore.connData.uploading_server = 1;
globalStore.connData.downloading_server = 1;
return await Request('postFormData', path, payload, responsedata);
async postFormData(path: string, payload?: any, responsedata?: any, options?: any) {
const globalStore = useGlobalStore();
globalStore.connData.downloading_server = 1;
globalStore.connData.uploading_server = 1;
return await Request('postFormData', path, payload, responsedata, options);
},
async get(path: string, payload?: any, responsedata?: any) {
@@ -241,7 +241,7 @@ export const Api = {
if (res.status === serv_constants.RIS_CODE__HTTP_INVALID_TOKEN) {
userStore.setServerCode(toolsext.ERR_AUTHENTICATION);
userStore.setAuth('', '');
userStore.setAuth('', '', '');
// throw { code: toolsext.ERR_AUTHENTICATION };
throw { status: toolsext.ERR_RETRY_LOGIN };
}

100
src/types/video.types.ts Normal file
View File

@@ -0,0 +1,100 @@
// ============ INTERFACES ============
export interface IFolder {
name: string;
path: string;
level?: number;
createdAt?: string;
}
export interface IVideo {
id: string;
filename: string;
originalName?: string;
folder: string;
path: string;
size: number;
mimetype?: string;
createdAt: string;
modifiedAt?: string;
uploadedAt?: string;
}
export interface IFolderOption {
label: string;
value: string;
level?: number;
}
export interface IUploadQueueItem {
file: File;
progress: number;
status: UploadStatus;
error?: string;
}
export interface IVideoResponse {
success: boolean;
data?: {
videos?: IVideo[];
folders?: IFolder[];
video?: IVideo;
folder?: IFolder;
currentPath?: string;
totalVideos?: number;
newPath?: string;
};
message?: string;
error?: string;
}
export interface IFolderResponse {
success: boolean;
data?: {
folders?: IFolder[];
folder?: IFolder;
};
message?: string;
error?: string;
}
// ============ TYPES ============
export type UploadStatus = 'pending' | 'uploading' | 'complete' | 'error';
// ============ CONSTANTS ============
export const UPLOAD_STATUS_CONFIG = {
pending: {
icon: 'schedule',
color: 'grey',
label: 'In attesa'
},
uploading: {
icon: 'cloud_upload',
color: 'primary',
label: 'Caricamento...'
},
complete: {
icon: 'check_circle',
color: 'positive',
label: 'Completato'
},
error: {
icon: 'error',
color: 'negative',
label: 'Errore'
}
} as const;
export const ALLOWED_VIDEO_TYPES = [
'video/mp4',
'video/webm',
'video/ogg',
'video/quicktime',
'video/x-msvideo',
'video/x-matroska'
];
export const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
export const MAX_FILES = 10;

View File

@@ -6,7 +6,6 @@
"target": "ESNext",
"jsx": "react-jsx",
"strict": true,
"baseUrl": "./",
"skipLibCheck": true,
"allowJs": true,
@@ -88,6 +87,12 @@
"@icons": [
"src/public/myicons/*"
],
"@types/*": [
"src/types/*"
],
"@services/*": [
"src/services/*"
],
"@images": [
"src/public/images/*"
],