- invita amico

This commit is contained in:
Surya Paolo
2025-11-19 10:09:45 +01:00
parent 05dc22dac6
commit 05a3617103
10 changed files with 1496 additions and 1211 deletions

View File

@@ -1442,6 +1442,7 @@ export const shared_consts = {
MSG_BENVENUTO: 2010, //MsgBenvenuto
MS_SHARE_LINK: 2000,
MSG_BENV_REGISTRATO: 2020,
MSG_INVITE_WHATSAPP: 2040,
},
TypeSend: {

View File

@@ -1202,7 +1202,10 @@
</CShareSocial>
</q-dialog>
<q-dialog v-model="mostraInviti" maximized>
<q-card style="min-width: 350px; max-width: 600px">
<q-card :style="{
minWidth: '350px',
maxWidth: $q.screen.lt.sm ? '100vw' : '800px',
}">
<!-- Header con bottone chiudi -->
<q-bar class="bg-primary text-white">
<q-space />

View File

@@ -4,6 +4,7 @@
justify-content: center;
min-height: 120vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@media (max-width: $breakpoint-sm-max) {
min-height: 100vh;
}
@@ -52,20 +53,17 @@
overflow: hidden;
}
// Bottoni selezione metodo
.selection-buttons {
display: flex;
gap: 16px;
gap: 12px;
flex-wrap: wrap;
@media (max-width: $breakpoint-xs-max) {
flex-direction: column;
}
justify-content: stretch;
}
.selection-btn {
flex: 1;
min-height: 180px;
flex: 0 1 calc(33.33% - 8px); // Desktop: max 1/3 dello spazio, non cresce
min-width: 140px;
min-height: 140px;
border-radius: 12px;
border: 2px solid #e0e0e0;
background: white;
@@ -82,7 +80,8 @@
}
@media (max-width: $breakpoint-xs-max) {
min-height: 120px;
flex: 0 1 calc(50% - 6px); // Mobile: max 50%, 2 per riga
min-height: 130px;
}
}
@@ -91,10 +90,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
padding: 8px;
width: 100%;
text-align: center;
@media (max-width: 768px) {
padding: 8px 24px;
}
}

View File

@@ -4,9 +4,11 @@ import { useInvitaAmicoStore } from '../../stores/useInvitaAmicoStore';
import type { InvitoAmicoForm } from '../../types/invita-amico.types.ts';
import { tools } from 'app/src/store/Modules/tools';
import { useI18n } from 'vue-i18n';
import { shared_consts } from '@src/common/shared_vuejs';
// Chiave localStorage
const MESSAGGIO_STORAGE_KEY = 'invita-amico-messaggio-personalizzato';
const MESSAGGIO_STORAGE_KEY = 'invita-amico-email';
const MESSAGGIO_STORAGE_KEY_WHATSAPP = 'invita-amico-whatsapp';
export default defineComponent({
name: 'InvitaAmico',
@@ -19,9 +21,12 @@ export default defineComponent({
const { t } = useI18n();
const invitaStore = useInvitaAmicoStore();
const rismsgShareLink = ref({ body: '', title: '' });
// State
const mostraCronologia = ref(false);
const metodoSelezionato = ref<'email' | 'telegram' | null>(null);
const metodoSelezionato = ref<'email' | 'telegram' | 'whatsapp' | null>(null);
const form = reactive<InvitoAmicoForm & { usernameInvitante?: string }>({
email: '',
messaggio: '',
@@ -122,6 +127,72 @@ export default defineComponent({
}
};
/**
* Gestione invio WhatsApp (con Share API fallback)
*/
const onInviaWhatsApp = async () => {
const messaggioBase = form.messaggio;
if (form.messaggio) {
localStorage.setItem(MESSAGGIO_STORAGE_KEY_WHATSAPP, form.messaggio.trim());
}
// Prova prima con Share API (mobile)
if (navigator.share) {
try {
await navigator.share({
title: 'Progetto RISO',
text: messaggioBase,
});
$q.notify({
type: 'positive',
message: 'Messaggio condiviso! 📤',
icon: 'share',
timeout: 2000,
});
return;
} catch (err) {
// Utente ha annullato o errore, prova WhatsApp diretto
}
}
// Fallback: apri WhatsApp diretto
const messaggioCodificato = encodeURIComponent(messaggioBase);
window.open(`https://wa.me/?text=${messaggioCodificato}`, '_blank');
$q.notify({
type: 'positive',
message: 'WhatsApp aperto! 💬',
icon: 'whatsapp',
timeout: 2000,
});
};
/**
* Copia messaggio negli appunti
*/
const copiaMessaggio = async () => {
const messaggioDaCopiare = form.messaggio;
try {
await navigator.clipboard.writeText(messaggioDaCopiare);
$q.notify({
type: 'positive',
message: 'Messaggio copiato! 📋',
caption: 'Ora incollalo dove preferisci',
icon: 'content_copy',
timeout: 2000,
});
} catch (err) {
$q.notify({
type: 'negative',
message: 'Errore nella copia',
icon: 'error',
timeout: 2000,
});
}
};
/**
* Conferma eliminazione cronologia
*/
@@ -168,19 +239,68 @@ export default defineComponent({
};
// Carica messaggio all'apertura
onMounted(() => {
onMounted(async () => {
// ricevi il msg
rismsgShareLink.value = await invitaStore.ottieniMsg(
shared_consts.TypeMsgTemplate.MSG_INVITE_WHATSAPP
);
const salvato = localStorage.getItem(MESSAGGIO_STORAGE_KEY);
const salvatowa = localStorage.getItem(MESSAGGIO_STORAGE_KEY_WHATSAPP);
if (salvato) {
form.messaggio = salvato;
if (metodoSelezionato.value === 'whatsapp') {
form.messaggio = salvatowa;
} else {
form.messaggio = salvato;
}
} else {
if (rismsgShareLink.value) {
form.messaggio = rismsgShareLink.value.body;
}
}
});
// Cancella
const cancellaMessaggioSalvato = () => {
localStorage.removeItem(MESSAGGIO_STORAGE_KEY);
form.messaggio = '';
if (metodoSelezionato.value === 'whatsapp') {
localStorage.removeItem(MESSAGGIO_STORAGE_KEY_WHATSAPP);
} else {
localStorage.removeItem(MESSAGGIO_STORAGE_KEY);
}
$q.notify({ type: 'info', message: 'Messaggio cancellato' });
};
const ripristinaMessaggioStandard = () => {
$q.dialog({
title: 'Conferma',
message: 'Vuoi ripristinare il messaggio standard?',
persistent: false,
ok: {
label: 'Sì',
color: 'primary',
},
cancel: {
label: 'No',
flat: true,
},
}).onOk(() => {
form.messaggio = '';
if (metodoSelezionato.value === 'whatsapp') {
localStorage.removeItem(MESSAGGIO_STORAGE_KEY_WHATSAPP);
if (rismsgShareLink.value) {
form.messaggio = rismsgShareLink.value.body;
}
} else {
localStorage.removeItem(MESSAGGIO_STORAGE_KEY);
}
$q.notify({
type: 'info',
message: 'Messaggio standard ripristinato',
icon: 'restore',
});
});
};
// RETURN
return {
@@ -191,10 +311,13 @@ export default defineComponent({
form,
onInviaEmail,
onInviaTelegram,
onInviaWhatsApp,
copiaMessaggio,
confermaEliminaCronologia,
formatDate,
invitaStore,
cancellaMessaggioSalvato,
ripristinaMessaggioStandard,
tools,
};
},

View File

@@ -20,7 +20,7 @@
<!-- Schermata Selezione Metodo -->
<q-card-section v-if="!metodoSelezionato">
<div class="text-center q-mb-lg">
<div class="text-h6 text-grey-8 q-mb-xs">Come vuoi invitare?</div>
<div class="text-h7 text-grey-8 q-mb-xs">Come vuoi invitare?</div>
</div>
<div class="selection-buttons">
@@ -33,13 +33,10 @@
<div class="selection-btn-content">
<q-icon
name="email"
size="48px"
size="40px"
color="primary"
/>
<div class="text-h6 q-mt-xs text-grey-9">Email</div>
<div class="text-caption text-grey-7">
Invia un invito diretto via email
</div>
<div class="text-h7 q-mt-xs text-grey-9">Email</div>
</div>
</q-btn>
@@ -52,13 +49,25 @@
<div class="selection-btn-content">
<q-icon
name="telegram"
size="48px"
size="40px"
color="blue-9"
/>
<div class="text-h6 q-mt-md text-grey-9">Telegram</div>
<div class="text-caption text-grey-7">
Condividi tramite Telegram
</div>
<div class="text-h7 q-mt-md text-grey-9">Telegram</div>
</div>
</q-btn>
<q-btn
@click="selezionaMetodo('whatsapp')"
class="selection-btn"
unelevated
no-caps
>
<div class="selection-btn-content">
<q-icon
name="fab fa-whatsapp"
size="40px"
color="green-7"
/>
<div class="text-h7 q-mt-md text-grey-9">WhatsApp o altri</div>
</div>
</q-btn>
</div>
@@ -68,8 +77,8 @@
<q-card-section v-if="metodoSelezionato === 'email'">
<div class="q-mb-md">
<q-btn
flat
dense
outline
icon="arrow_back"
label="Cambia metodo"
color="grey-7"
@@ -121,7 +130,7 @@
outlined
:rows="tools.isMobile() ? 6 : 9"
counter
maxlength="500"
maxlength="2000"
:disable="invitaStore.loading"
>
<!-- Bottone per cancellare messaggio salvato -->
@@ -164,7 +173,7 @@
icon="email"
color="primary"
size="lg"
class="full-width q-mt-md"
class="full-width q-my-md"
outline
:loading="invitaStore.loading"
:disable="invitaStore.loading || !form.email"
@@ -185,12 +194,7 @@
/>
</div>
<div class="text-center q-mb-md">
<div class="text-subtitle1 text-grey-8 q-mb-xs">
Invita tramite Telegram
</div>
<div class="text-caption text-grey-7">
Genera un messaggio da condividere su Telegram
</div>
<div class="text-subtitle1 text-grey-8 q-mb-xs">Invita tramite Telegram</div>
</div>
<q-btn
@@ -199,12 +203,92 @@
icon="telegram"
color="blue-9"
size="lg"
class="full-width"
class="full-width q-my-sm"
outline
:disable="invitaStore.loading"
/>
</q-card-section>
<!-- Sezione WhatsApp (mostrata solo se selezionata) -->
<q-card-section v-if="metodoSelezionato === 'whatsapp'">
<div class="q-mb-md">
<q-btn
outline
dense
icon="arrow_back"
label="Cambia metodo"
color="grey-7"
size="sm"
@click="tornaAllaScelta"
/>
</div>
<div class="text-center q-mb-md">
<div class="text-subtitle1 text-grey-8 q-mb-xs">Invita tramite WhatsApp</div>
</div>
<!-- Messaggio Personalizzato (opzionale) -->
<q-input
v-model="form.messaggio"
type="textarea"
label="Messaggio personalizzato (opzionale)"
hint="Aggiungi un messaggio personale al tuo invito"
outlined
:rows="tools.isMobile() ? 6 : 9"
counter
maxlength="2000"
>
<!-- Bottone per cancellare messaggio salvato -->
<template
v-slot:append
>
<q-btn
v-if="form.messaggio"
flat
dense
round
icon="clear"
@click.stop="cancellaMessaggioSalvato"
>
<q-tooltip>Cancella messaggio salvato</q-tooltip>
</q-btn>
<q-btn
dense
round
icon="undo"
@click.stop="ripristinaMessaggioStandard"
>
<q-tooltip>Ripristina messaggio standard</q-tooltip>
</q-btn>
</template>
<template v-slot:prepend>
<q-icon name="message" />
</template>
</q-input>
<!-- Bottone WhatsApp principale -->
<q-btn
@click="onInviaWhatsApp"
label="Apri WhatsApp"
icon="fab fa-whatsapp"
color="green-7"
class="full-width q-my-sm"
unelevated
:disable="invitaStore.loading || !form.messaggio"
/>
<!-- Bottone alternativo: Copia -->
<q-btn
@click="copiaMessaggio"
label="Copia messaggio"
icon="content_copy"
color="primary"
size="md"
class="full-width"
outline
/>
</q-card-section>
<!-- Info Section (solo per Telegram) -->
<q-card-section
v-if="metodoSelezionato === 'telegram'"
@@ -225,7 +309,11 @@
</q-card-section>
<!-- Cronologia Inviti (opzionale) -->
<q-card-section v-if="invitaStore.hasCronologia && mostraCronologia">
<q-card-section
v-if="
metodoSelezionato === 'email' && invitaStore.hasCronologia && mostraCronologia
"
>
<div class="text-subtitle2 text-grey-8 q-mb-sm">
<q-icon
name="history"
@@ -299,7 +387,7 @@
<!-- Stats Badge -->
<div
v-if="invitaStore.totaleInviti > 0"
v-if="metodoSelezionato === 'email' && invitaStore.totaleInviti > 0"
class="stats-badge q-mt-md"
>
<q-chip
@@ -321,7 +409,13 @@
</q-chip>-->
</div>
<!-- Bottone per mostrare cronologia -->
<q-card-section v-if="invitaStore.hasCronologia && !mostraCronologia">
<q-card-section
v-if="
metodoSelezionato === 'email' &&
invitaStore.hasCronologia &&
!mostraCronologia
"
>
<q-btn
flat
dense

View File

@@ -5,6 +5,7 @@ import type {
InvitoAmicoForm,
InvitoAmicoRequest,
InvitoAmicoResponse,
InvitoGetMsg,
InvitoGetResponse,
} from '../types/invita-amico.types.ts';
import { useUserStore } from '../store/index.js';
@@ -225,6 +226,38 @@ export const useInvitaAmicoStore = defineStore('invitaAmico', () => {
}
};
const ottieniMsg = async (
idTemplate: number,
): Promise<InvitoGetMsg> => {
// Reset errori
error.value = null;
loading.value = true;
const userStore = useUserStore();
try {
if (!idTemplate) {
throw new Error('idTemplate non valido');
}
// Chiamata API
const response = await Api.SendReq('/inviti/getmsgtempl', 'POST', { idTemplate });
if (response.data.success) {
return response.data.rec; // { body e title }
}
} catch (err) {
// Gestione errori
const errorMessage = err instanceof Error ? err.message : 'Errore sconosciuto';
error.value = errorMessage;
// Ritorna risposta di errore
return null;
} finally {
loading.value = false;
}
};
/**
* Invia invito via Telegram
* (placeholder - integra con la tua logica esistente)
@@ -422,6 +455,7 @@ export const useInvitaAmicoStore = defineStore('invitaAmico', () => {
ottieniInvitoByToken,
inviaInvitoTelegram,
aggiungiACronologia,
ottieniMsg,
rimuoviDaCronologia,
svuotaCronologia,
resetStato,

View File

@@ -21,6 +21,10 @@ export interface InvitoGetResponse {
email: string;
usernameInvitante: string;
}
export interface InvitoGetMsg {
title: string;
body: string;
}
export interface EmailInvitoTemplate {
to: string;

File diff suppressed because it is too large Load Diff

View File

@@ -62,9 +62,13 @@ export default defineComponent({
const tabevents = ref('new');
const circuitSel = ref('');
const spinner_visible = ref(false)
const showrules = ref(false);
const sendRIS = computed(() => ($route.query.sr ? $route.query.sr : ''));
const causalDest = computed(() => ($route.query.cd ? $route.query.cd : ''));
const requestToEnterCircuit = ref(false);
const mycards = computed(() => {
@@ -183,6 +187,9 @@ export default defineComponent({
async function mounted() {
loading.value = true;
if (sendRIS.value) spinner_visible.value = true;
await loadGroup();
searchList.value = [];
@@ -299,7 +306,7 @@ export default defineComponent({
function getRegulation(reg: string) {
const strreg = reg + '';
if (!reg) {
let name = CircuitSel.value
let name = CircuitSel.value;
const mystringa = t('circuit.regolamento', { nomecircuito: name });
return mystringa;
} else {
@@ -307,6 +314,10 @@ export default defineComponent({
}
}
function showed() {
spinner_visible.value = false
}
onMounted(mounted);
return {
@@ -364,6 +375,10 @@ export default defineComponent({
showrules,
circuit,
getRegulation,
spinner_visible,
sendRIS,
causalDest,
showed,
};
},
});

View File

@@ -856,6 +856,12 @@
<div v-if="!tools.isLogged()">
<CCheckIfIsLogged></CCheckIfIsLogged>
</div>
<q-inner-loading :showing="spinner_visible">
<q-spinner-tail
size="3em"
color="primary"
/>
</q-inner-loading>
<q-dialog
v-model="showPic"
full-height
@@ -941,12 +947,17 @@
</q-card-actions>
</q-card>
</q-dialog>
<div v-if="showsendCoinTo">
<div v-if="showsendCoinTo || sendRIS">
<CSendCoins
:showprop="showsendCoinTo"
:to_group="mygrp"
circuitname=""
@close="showsendCoinTo = false"
:sendRIS="sendRIS"
:causalDest="causalDest"
@close="
showsendCoinTo = false;
loading = false;
"
@showed="showed"
>
</CSendCoins>
</div>