- verifica email se non è stata verificata (componente)

- altri aggiornamenti grafica PAGERIS.
- OLLAMA AI
This commit is contained in:
Surya Paolo
2025-12-11 18:34:39 +01:00
parent 6fdb101092
commit 89a8d10eae
44 changed files with 7915 additions and 3565 deletions

View File

@@ -13,15 +13,15 @@
<div class="card-icon-wrapper">
<q-icon name="fas fa-sign-in-alt" size="48px" class="card-icon" />
</div>
<div class="card-content">
<h3 class="card-title">Benvenuto su RISO</h3>
<h3 class="card-title">Benvenuto su {{ tools.sitename() }}</h3>
<p class="card-description">
Accedi con le tue credenziali per utilizzare la APP e per unirti
al Circuito di scambio RIS del tuo territorio
ai membri della Community
</p>
</div>
<div class="card-actions">
<q-btn
unelevated
@@ -47,7 +47,7 @@
<span class="telegram-subtitle">Unisciti al gruppo Provinciale</span>
</div>
</div>
<q-btn
rounded
unelevated
@@ -71,7 +71,7 @@
<span class="help-subtitle">Consulta la guida completa</span>
</div>
</div>
<q-btn
rounded
unelevated

View File

@@ -3,7 +3,10 @@
<div class="key-label">
{{ mykey }}
</div>
<div class="value-content" :style="color ? `background-color: ${color}; color: white;` : ''">
<div
class="value-content"
:style="color ? `background-color: ${color}; color: white;` : ''"
>
<span v-if="mydate">
<CDateTime
v-model:value="mydate"
@@ -11,10 +14,17 @@
:canEdit="false"
/>
</span>
<span v-else class="value-text">
<span
v-else
class="value-text"
>
{{ myvalue || '-' }}
</span>
</div>
<div
class="value-content"
:style="color ? `background-color: ${color}; color: white;` : ''"
>
<q-btn
v-if="showSetButton && onSetValue"
rounded
@@ -30,8 +40,7 @@
</div>
</template>
<script lang="ts" src="./CKeyAndValue.ts">
</script>
<script lang="ts" src="./CKeyAndValue.ts"></script>
<style lang="scss" scoped>
@import './CKeyAndValue.scss';

View File

@@ -32,6 +32,7 @@ import { shared_consts } from '@/common/shared_vuejs';
import { LandingFooter } from '@/components/LandingFooter';
import { CMyActivities } from '@/components/CMyActivities';
import { CECommerce } from '@/components/CECommerce';
import { CheckEmail } from '@/components/CheckEmail';
import { HomeRiso } from '@/components/HomeRiso';
import mycircuits from '@/views/user/mycircuits/mycircuits.vue';
import PageRis from '@/components/pageris/pageris.vue';
@@ -118,6 +119,7 @@ export default defineComponent({
CCheckIfIsLogged,
CStatusReg,
CDashboard,
CheckEmail,
CMainView,
CNotifAtTop,
CPresentazione,

View File

@@ -904,14 +904,7 @@
>
Msg di Controllo Verifica Email
</div>
<div class="q-pa-xs q-gutter-md">
<div
v-if="tools.isLogged() && !tools.isVerified()"
class="text-verified"
>
{{ t('components.authentication.email_verification.link_sent') }}
</div>
</div>
<CheckEmail />
</div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.PRESENTAZIONE">
<div
@@ -1090,10 +1083,10 @@
Check Sito di Test
</div>
<q-banner
v-if="tools.isTest() && false"
v-if="tools.isTest()"
rounded
dense
class="bg-negative text-white"
class="bg-negative text-white q-ma-sm"
color="primary q-title"
style="text-align: center"
>
@@ -1104,7 +1097,7 @@
size="xs"
/>
</template>
<span class="mybanner"> TEST !</span>
<span class="mybanner"> AMBIENTE DI TEST !</span>
</q-banner>
</div>
<div v-else-if="myel.type === shared_consts.ELEMTYPE.CHECKNEWVERSION">

View File

@@ -588,4 +588,48 @@
padding: 8px 12px;
font-weight: 500;
}
}
.annuncio-location {
font-size: 1rem;
color: #718096;
align-items: center;
gap: 4px;
&::before {
content: '📍';
}
}
// Il contenitore padre (q-item o card) deve avere position relative
.q-item,
.event-card {
position: relative;
}
// Bottone overlay fisso a destra
.action-menu-btn-overlay {
position: absolute !important;
top: 8px;
right: 0px;
transform: translateY(-50%);
z-index: 10;
// Sfondo semi-trasparente per visibilità
background: rgba(255, 255, 255, 0.8) !important;
// Ombra leggera
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
&:hover {
background: rgba(255, 255, 255, 1) !important;
}
}
// Alternativa: in alto a destra
.action-menu-btn-overlay--top {
position: absolute !important;
top: 4px;
right: 4px;
z-index: 10;
}

View File

@@ -340,8 +340,9 @@
<span
v-for="(rec, ind) of myrec.mycities"
:key="ind"
class="annuncio-location"
>
<span v-if="ind > 0">, </span>
<span v-if="ind > 0"></span>
<span v-if="table === shared_consts.TABLES_MYHOSPS" class="cities-text-bold">{{ rec.comune }} ({{ rec.prov }})</span>
<span v-else>{{ rec.comune }} ({{ rec.prov }})</span>
</span>
@@ -353,12 +354,11 @@
<q-item-section
v-if="tools.canModifyThisRec(myrec, table) || editOn"
side
top
class="actions-section"
>
<q-btn
round
flat
dense
icon="more_vert"
size="sm"
class="action-menu-btn"

View File

@@ -459,7 +459,7 @@
<q-item-label>
<q-btn rounded icon="fas fa-ellipsis-h">
<q-menu>
<q-list
<!--<q-list
v-if="!userStore.IsMyFriendByUsername(contact.username)"
style="min-width: 200px"
>
@@ -481,7 +481,7 @@
t('friends.ask_friend')
}}</q-item-section>
</q-item>
</q-list>
</q-list>-->
<q-list style="min-width: 200px">
<q-item
clickable
@@ -515,7 +515,7 @@
>
<q-menu>
<q-list style="min-width: 200px">
<q-item
<!--<q-item
v-if="
!userStore.IsMyFriendByUsername(contact.username) &&
!userStore.IsAskedFriendByUsername(contact.username)
@@ -538,9 +538,9 @@
<q-item-section>{{
t('friends.ask_friend')
}}</q-item-section>
</q-item>
</q-item>-->
<q-item
v-else-if="
v-if="
!userStore.IsMyFriendByUsername(contact.username) &&
userStore.IsAskedFriendByUsername(contact.username)
"

View File

@@ -35,6 +35,7 @@ export default defineComponent({
});
const handleInput = (value: string | number) => {
// console.log('value', value)
if (value === '⌫') {
inputValue.value = inputValue.value.slice(0, -1);
} else if (value === '.' && !inputValue.value.includes('.')) {
@@ -43,12 +44,14 @@ export default defineComponent({
inputValue.value += value.toString();
}
// console.log('inputValue.value', inputValue.value)
// Verifica se inputValue contiene più di due cifre decimali
const decimalPattern = /^\d+(\.\d{0,2})?$/; // Regex per validare il numero
const newValue = inputValue.value;
// Se non rispetta il formato, tronca il numero a 2 cifre decimali
if (!decimalPattern.test(newValue)) {
/*if (!decimalPattern.test(newValue)) {
// Se troviamo un punto decimale, manteniamo solo le prime 2 cifre
const parts = newValue.split('.'); // Dividi il numero in parte intera e decimale
if (parts.length > 1) {
@@ -58,7 +61,7 @@ export default defineComponent({
// Nessuna parte decimale, quindi usa solo la parte intera
inputValue.value = parts[0];
}
}
}*/
emit('update:modelValue', inputValue.value);
};

View File

@@ -10,7 +10,7 @@
class="balance-text-saldo"
>Saldo:
</span>
{{ currentBalance > 0 ? '+' : '' }}{{ currentBalance }} RIS
{{ currentBalance > 0 ? '+' : '' }}{{ currentBalance.toFixed(2) }} RIS
</span>
</div>
@@ -47,14 +47,14 @@
:style="{ '--zero-position': zeroPosition + '%' }"
>
<div class="marker min-marker">
<span class="marker-value">{{ minLimit }}</span>
<span class="marker-value">{{ minLimit.toFixed(2) }}</span>
<span class="marker-label">Fido</span>
</div>
<div class="marker zero-marker-label">
<span class="marker-value">0</span>
</div>
<div class="marker max-marker">
<span class="marker-value">+{{ maxLimit }}</span>
<span class="marker-value">+{{ maxLimit.toFixed(2) }}</span>
<span class="marker-label">Max</span>
</div>
</div>
@@ -69,7 +69,7 @@
color="negative"
/>
<span class="availability-text">
Puoi dare ancora: <strong>{{ canGive }} RIS</strong>
Puoi dare ancora: <strong>{{ canGive.toFixed(2) }} RIS</strong>
</span>
</div>
<div class="availability-item">
@@ -79,7 +79,7 @@
color="positive"
/>
<span class="availability-text">
Puoi ricevere: <strong>{{ canReceive }} RIS</strong>
Puoi ricevere: <strong>{{ canReceive.toFixed(2) }} RIS</strong>
</span>
</div>
</div>

View File

@@ -1,341 +1,575 @@
.c-send-coins {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding-bottom: 80px;
// Variables
$primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
$orange-gradient: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
$ris-color: #ff5500;
$border-radius-lg: 16px;
$border-radius-md: 12px;
$border-radius-sm: 8px;
$shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
$shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
// Main Dialog
.send-coins-dialog {
border-radius: $border-radius-lg $border-radius-lg 0 0;
overflow: hidden;
max-width: 420px;
width: 100%;
@media (min-width: 600px) {
border-radius: $border-radius-lg;
max-height: 90vh;
}
}
.page-header {
.mobile-fullheight {
height: 100vh;
max-height: 100vh;
display: flex;
flex-direction: column;
border-radius: 0;
}
// Header
.dialog-header {
position: relative;
}
.header-gradient {
background: $primary-gradient;
padding: 12px 16px 14px;
position: relative;
}
.header-top-bar {
display: flex;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.1);
justify-content: space-between;
margin-bottom: 12px;
}
.close-btn {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
.back-btn {
color: white;
margin-right: 12px;
}
.header-title {
font-size: 20px;
font-weight: 600;
color: white;
&:hover {
background: rgba(255, 255, 255, 0.25);
}
}
.content-section {
padding: 20px 16px;
}
.section-title {
font-size: 24px;
font-weight: 700;
color: white;
margin-bottom: 20px;
}
.section-subtitle {
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 12px;
}
.search-input {
margin-bottom: 16px;
:deep(.q-field__control) {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
}
}
.circuit-selector {
margin-bottom: 24px;
}
.circuit-select {
:deep(.q-field__control) {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
}
}
.user-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.user-item {
.header-title-wrapper {
display: flex;
align-items: center;
padding: 12px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.85);
}
.user-info {
flex: 1;
margin-left: 12px;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.user-username {
font-size: 14px;
color: #666;
}
gap: 10px;
}
.selected-user-card {
display: flex;
align-items: center;
padding: 16px;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
margin-bottom: 20px;
.user-info {
margin-left: 12px;
flex: 1;
}
.circuit-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(103, 126, 234, 0.1);
color: #667eea;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-top: 4px;
}
}
.circuit-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.circuit-item {
display: flex;
align-items: center;
padding: 16px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&.selected {
background: rgba(103, 126, 234, 0.15);
border: 2px solid #667eea;
}
&:active {
transform: scale(0.98);
}
.circuit-info {
flex: 1;
}
.circuit-name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 4px;
}
.circuit-balance {
font-size: 14px;
color: #666;
}
}
.amount-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
}
.amount-display {
.ris-coin-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: $orange-gradient;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
margin-bottom: 16px;
cursor: pointer;
position: relative;
.currency {
font-size: 24px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
margin-right: 8px;
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.4);
.ris-logo {
width: 18px;
height: 18px;
object-fit: contain;
}
.amount-value {
font-size: 42px;
.coin-symbol {
color: white;
font-weight: 700;
color: white;
}
.keyboard-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: white;
font-size: 12px;
}
}
.limits-info {
.header-title {
color: white;
font-size: 17px;
font-weight: 600;
}
// Balance Card - Compatto
.balance-card {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
border-radius: $border-radius-sm;
padding: 10px 14px;
}
.balance-info {
display: flex;
justify-content: space-around;
padding: 12px 0;
margin-bottom: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
justify-content: space-between;
align-items: center;
}
.limit-item {
.balance-main {
display: flex;
flex-direction: column;
align-items: center;
.limit-label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.limit-value {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
gap: 2px;
}
.balance-label {
color: rgba(255, 255, 255, 0.7);
font-size: 11px;
}
.balance-value {
color: white;
font-size: 20px;
font-weight: 700;
line-height: 1.2;
.balance-symbol {
font-size: 14px;
font-weight: 500;
opacity: 0.9;
}
}
.description-input {
:deep(.q-field__control) {
border-radius: 12px;
}
.balance-fido {
text-align: right;
}
.action-btn {
border-radius: 12px;
padding: 12px;
font-size: 16px;
.fido-label {
display: block;
color: rgba(255, 255, 255, 0.7);
font-size: 10px;
}
.fido-value {
color: #4ade80;
font-size: 14px;
font-weight: 600;
text-transform: none;
}
.confirmation-card {
border-radius: 24px;
max-width: 400px;
width: 90vw;
// Content
.dialog-content {
padding: 14px 16px;
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
.confirmation-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
.header-title {
font-size: 20px;
font-weight: 600;
color: white;
// Section Block - Più compatto
.section-block {
margin-bottom: 12px;
}
.section-label {
display: block;
color: #6b7280;
font-size: 12px;
font-weight: 500;
margin-bottom: 6px;
}
// Modern Select
.modern-select {
:deep(.q-field__control) {
border-radius: $border-radius-sm;
background: #f9fafb;
border: 1.5px solid #e5e7eb;
min-height: 40px;
transition: all 0.2s ease;
&:hover {
border-color: #d1d5db;
}
}
:deep(.q-field__control-container) {
}
:deep(.q-field--focused .q-field__control) {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
}
:deep(.q-field__native) {
font-weight: 500;
font-size: 14px;
color: #374151;
}
:deep(.q-field__label) {
font-size: 13px;
}
}
.modern-input {
:deep(.q-field__control) {
border-radius: $border-radius-sm;
background: #f9fafb;
border: 1.5px solid #e5e7eb;
min-height: 40px;
}
}
// Recipient Card - Compatto
.recipient-card {
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
border: 1.5px solid #ddd6fe;
border-radius: $border-radius-md;
padding: 10px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.recipient-content {
flex: 1;
min-width: 0;
}
.recipient-view {
:deep(.q-item) {
padding: 0;
min-height: auto;
}
}
.circuit-badge {
color: white;
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
border-radius: 16px;
flex-shrink: 0;
}
// Amount Input Row - Compatto
.amount-input-row {
cursor: pointer;
}
.amount-input {
:deep(.q-field__control) {
border-radius: $border-radius-sm;
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
border: none;
min-height: 48px;
padding: 4px 12px;
}
:deep(.q-field__native) {
color: white !important;
font-size: 22px !important;
font-weight: 700 !important;
text-align: center;
}
}
.confirmation-content {
padding: 24px;
text-align: center;
}
:deep(.q-field__prepend) {
padding-right: 0;
}
.amount-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.amount-display-large {
font-size: 36px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 24px;
}
.users-row {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-bottom: 32px;
}
.user-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.user-name-small {
font-size: 14px;
color: #666;
}
.verify-title {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 12px;
}
.verify-text {
font-size: 15px;
color: #666;
line-height: 1.5;
.amount-highlight {
color: #667eea;
font-weight: 700;
:deep(.q-field__append) {
padding-left: 0;
}
}
.confirmation-actions {
display: flex;
gap: 12px;
padding: 16px 24px;
.currency-symbol {
color: rgba(255, 255, 255, 0.5);
font-size: 18px;
font-weight: 400;
}
.cancel-btn,
.confirm-btn {
flex: 1;
border-radius: 12px;
padding: 12px;
font-size: 16px;
font-weight: 600;
text-transform: none;
.coin-badge {
width: 32px;
height: 32px;
border-radius: 20%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 12px;
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.3);
}
.keyboard-btn {
color: rgba(255, 255, 255, 0.7);
&:hover {
color: white;
}
}
// Banners
.warning-banner {
background: $orange-gradient;
color: white;
font-weight: 500;
font-size: 13px;
}
.error-banner {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
font-weight: 500;
font-size: 13px;
}
// Note Input - Compatto
.modern-textarea {
:deep(.q-field__control) {
border-radius: $border-radius-sm;
background: #f9fafb;
border: 1.5px solid #e5e7eb;
transition: all 0.2s ease;
}
:deep(.q-field--focused .q-field__control) {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
}
:deep(.q-field__native) {
resize: none;
font-size: 14px;
}
:deep(.q-field__counter) {
color: #9ca3af;
font-size: 11px;
}
:deep(.q-field__label) {
font-size: 13px;
}
}
// Actions
.dialog-actions {
display: flex;
gap: 10px;
padding: 12px 16px;
background: white;
border-top: 1px solid #f3f4f6;
}
.fixed-bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
}
.cancel-btn {
background: rgba(0, 0, 0, 0.05);
flex: 1;
background: #f3f4f6;
color: #6b7280;
font-weight: 600;
font-size: 14px;
padding: 10px 16px;
border-radius: $border-radius-sm;
text-transform: none;
&:hover {
background: #e5e7eb;
}
}
.send-btn {
flex: 2;
background: $orange-gradient;
color: white;
font-weight: 600;
font-size: 14px;
padding: 10px 20px;
border-radius: $border-radius-sm;
text-transform: none;
box-shadow: 0 2px 8px rgba(249, 115, 22, 0.3);
transition: all 0.2s ease;
&:hover:not(.btn-disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4);
}
&.btn-disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.send-btn-text {
margin-right: 6px;
}
.send-btn-icon {
width: 20px;
height: 20px;
object-fit: contain;
}
// Keyboard Dialog
.keyboard-dialog {
width: 100%;
max-width: 420px;
border-radius: $border-radius-lg $border-radius-lg 0 0;
margin: 0;
}
.keyboard-header {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
padding: 12px 16px;
}
.keyboard-header-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.keyboard-title {
color: #6b7280;
font-weight: 500;
font-size: 14px;
}
.done-btn {
font-weight: 600;
font-size: 14px;
}
.keyboard-display {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
border-radius: $border-radius-sm;
padding: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.keyboard-amount {
color: white;
font-size: 28px;
font-weight: 700;
}
.keyboard-coin-badge {
width: 32px;
height: 32px;
border-radius: 20%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 12px;
}
.keyboard-section {
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
}
// Dark mode support
.body--dark {
.send-coins-dialog {
background: #1f2937;
}
.dialog-content {
background: #1f2937;
}
.section-label {
color: #9ca3af;
}
.modern-select,
.modern-input {
:deep(.q-field__control) {
background: #374151;
border-color: #4b5563;
}
:deep(.q-field__native) {
color: #f3f4f6;
}
}
.recipient-card {
background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
border-color: #6b7280;
}
.modern-textarea {
:deep(.q-field__control) {
background: #374151;
border-color: #4b5563;
}
:deep(.q-field__native) {
color: #f3f4f6;
}
}
.dialog-actions {
background: #1f2937;
border-top-color: #374151;
}
.fixed-bottom-actions {
background: #1f2937;
}
.cancel-btn {
background: #374151;
color: #d1d5db;
}
.keyboard-dialog {
background: #1f2937;
}
.keyboard-header {
background: #374151;
border-color: #4b5563;
}
.keyboard-title {
color: #d1d5db;
}
}
// Responsive - Ultra compatto per mobile piccoli
@media (max-width: 360px) {
.header-gradient {
padding: 10px 12px 12px;
}
.balance-value {
font-size: 18px;
}
.dialog-content {
padding: 12px;
}
.section-block {
margin-bottom: 10px;
}
.amount-input {
:deep(.q-field__native) {
font-size: 20px !important;
}
}
}

View File

@@ -1,43 +1,47 @@
import type { PropType } from 'vue';
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
import type { IAccount, ICircuit, IMyGroup, ISendCoin, IUserFields } from '../../model';
import { IOperators, ISpecialField } from '../../model'
import { tools } from '@tools'
import { CSaldo } from '@/components/CSaldo'
import { useUserStore } from '@store/UserStore'
import { useCircuitStore } from '@store/CircuitStore'
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { CNumericKeyboard } from '@/components/CNumericKeyboard'
import { CMyUserOnlyView } from '@/components/CMyUserOnlyView'
import { CMyGroupOnlyView } from '@/components/CMyGroupOnlyView'
import { CCheckCircuitsEnabled } from '@/components/CCheckCircuitsEnabled'
import { costanti } from '@costanti'
import { useRouter } from 'vue-router'
import { shared_consts } from '@/common/shared_vuejs'
import { IOperators, ISpecialField } from '../../model';
import { tools } from '@tools';
import { CSaldo } from '@/components/CSaldo';
import { useUserStore } from '@store/UserStore';
import { useCircuitStore } from '@store/CircuitStore';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { CNumericKeyboard } from '@/components/CNumericKeyboard';
import { CMyUserOnlyView } from '@/components/CMyUserOnlyView';
import { CMyGroupOnlyView } from '@/components/CMyGroupOnlyView';
import { CCheckCircuitsEnabled } from '@/components/CCheckCircuitsEnabled';
import { costanti } from '@costanti';
import { useRouter } from 'vue-router';
import { shared_consts } from '@/common/shared_vuejs';
export default defineComponent({
name: 'CSendCoins',
emits: ['close', 'showed'],
props: {
loadprofile: {
type: Boolean,
default: false,
},
showprop: {
type: Boolean,
default: false,
},
circuitname: {
type: String,
default: ''
default: '',
},
qtydefault: {
type: String,
required: false,
default: ''
default: '',
},
to_user: {
type: Object as PropType<IUserFields>,
required: false,
default: null
default: null,
},
to_group: {
type: Object as PropType<IMyGroup>,
@@ -65,99 +69,133 @@ export default defineComponent({
default: '',
},
},
components: { CSaldo, CMyUserOnlyView, CMyGroupOnlyView, CCheckCircuitsEnabled, CNumericKeyboard },
components: {
CSaldo,
CMyUserOnlyView,
CMyGroupOnlyView,
CCheckCircuitsEnabled,
CNumericKeyboard,
},
setup(props, { emit }) {
const $q = useQuasar()
const { t } = useI18n()
const showpage = ref(false)
const userStore = useUserStore()
const circuitStore = useCircuitStore()
const $router = useRouter()
const $q = useQuasar();
const { t } = useI18n();
const showpage = ref(false);
const userStore = useUserStore();
const circuitStore = useCircuitStore();
const $router = useRouter();
const from_username = ref(userStore.my.username)
const from_groupname = ref('')
const from_contocom = ref('')
const circuitsel = ref('')
const qty = ref(<string | number>'')
const causal = ref('')
const loading = ref(false)
const visubanner = ref(true)
const bothcircuits = ref(<any>[])
const to_user_real = ref(<IUserFields>{});
const showProvinceToSelect = ref(false)
const from_username = ref(userStore.my.username);
const from_groupname = ref('');
const from_contocom = ref('');
const circuitsel = ref('');
const qty = ref(<string | number>'');
const causal = ref('');
const loading = ref(false);
const visubanner = ref(true);
const bothcircuits = ref(<any>[]);
const groupSel = ref(<IMyGroup | null | undefined>null)
const showProvinceToSelect = ref(false);
const showKeyboard = ref(false);
const datasaved = ref(<any>null)
const step = ref(0)
const sendCoinDialog = ref(null)
const groupSel = ref(<IMyGroup | null | undefined>null);
const circuittoload = ref(<ICircuit | undefined>undefined)
const circuitloaded = ref(<ICircuit>{})
const circuitdest = ref(<ICircuit | undefined>undefined)
const accountloaded = ref(<IAccount | undefined | null>undefined)
const accountdest = ref(<IAccount | undefined>undefined)
const remainingCoins = ref(0)
const maxsendable = ref(0)
const numstep = ref(0)
const arrTypesAccounts = ref(<any>[])
const tipoConto = ref(shared_consts.AccountType.USER)
const datasaved = ref(<any>null);
const step = ref(0);
const sendCoinDialog = ref(null);
const priceLabel = computed(() => circuitloaded.value ? `${qty.value} ` + circuitloaded.value.symbol : '')
const arrayMarkerLabel = ref(<any>[])
const circuittoload = ref(<ICircuit | undefined>undefined);
const circuitloaded = ref(<ICircuit>{});
const circuitdest = ref(<ICircuit | undefined>undefined);
const accountloaded = ref(<IAccount | undefined | null>undefined);
const accountdest = ref(<IAccount | undefined>undefined);
const remainingCoins = ref(0);
const maxsendable = ref(0);
const numstep = ref(0);
const arrTypesAccounts = ref(<any>[]);
const tipoConto = ref(shared_consts.AccountType.USER);
const qtyRef = ref(<any>null)
const causalRef = ref(<any>null)
const priceLabel = computed(() =>
circuitloaded.value ? `${qty.value} ` + circuitloaded.value.symbol : ''
);
const arrayMarkerLabel = ref(<any>[]);
const groupsListAdmin = ref(<IMyGroup[]>[])
const qtyRef = ref(<any>null);
const causalRef = ref(<any>null);
const arrGroupsList = ref(<any[]>[])
const groupsListAdmin = ref(<IMyGroup[]>[]);
watch(() => circuitsel.value, (newval, oldval) => {
tools.setCookie(tools.CIRCUIT_USE, newval)
aggiorna()
})
const arrGroupsList = ref(<any[]>[]);
watch(() => tipoConto.value, (newval, oldval) => {
watch(
() => circuitsel.value,
(newval, oldval) => {
tools.setCookie(tools.CIRCUIT_USE, newval);
aggiorna(true);
}
);
if (tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO) {
if (arrGroupsList.value.length >= 1)
from_groupname.value = arrGroupsList.value[0].value
watch(
() => tipoConto.value,
(newval, oldval) => {
if (tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO) {
if (arrGroupsList.value.length >= 1)
from_groupname.value = arrGroupsList.value[0].value;
}
tools.setCookie(tools.COOK_TIPOCONTO, tipoConto.value.toString());
aggiorna(true);
}
);
watch(
() => from_groupname.value,
(newval, oldval) => {
aggiorna(true);
}
);
watch(
() => from_username.value,
(newval, oldval) => {
aggiorna(true);
}
);
watch(
() => props.showprop,
(newval, oldval) => {
showpage.value = newval;
}
);
async function aggiorna(load: boolean = false) {
if (load) {
inizio_caricamento();
}
tools.setCookie(tools.COOK_TIPOCONTO, tipoConto.value.toString())
groupSel.value = null;
from_contocom.value = '';
accountloaded.value = null;
aggiorna()
})
watch(() => from_groupname.value, (newval, oldval) => {
aggiorna()
})
watch(() => from_username.value, (newval, oldval) => {
aggiorna()
})
watch(() => props.showprop, (newval, oldval) => {
console.log('props.showprop', props.showprop, newval)
showpage.value = newval
})
async function aggiorna() {
groupSel.value = null
from_contocom.value = ''
accountloaded.value = null
if (!circuittoload.value || (circuittoload.value && circuittoload.value.name !== circuitsel.value)) {
circuittoload.value = circuitStore.listcircuits.find((rec: ICircuit) => rec.name === circuitsel.value)
if (
!circuittoload.value ||
(circuittoload.value && circuittoload.value.name !== circuitsel.value)
) {
circuittoload.value = circuitStore.listcircuits.find(
(rec: ICircuit) => rec.name === circuitsel.value
);
if (circuittoload.value) {
loading.value = true
await userStore.loadCircuit(circuittoload.value.path, '').then(({ data, status }: { data: any, status: number }) => {
datasaved.value = data
})
loading.value = true;
await userStore
.loadCircuit(circuittoload.value.path, '')
.then(({ data, status }: { data: any; status: number }) => {
datasaved.value = data;
});
}
}
@@ -165,201 +203,232 @@ export default defineComponent({
try {
arrTypesAccounts.value = [
{
label: t('circuit.personale') + ' ' + from_username.value,
label: '👤 ' + t('circuit.personale') + ' - ' + from_username.value,
value: shared_consts.AccountType.USER,
},
]
];
if (datasaved.value.circuit) {
circuitloaded.value = datasaved.value.circuit
circuitloaded.value = datasaved.value.circuit;
if (tipoConto.value === shared_consts.AccountType.USER) {
accountloaded.value = userStore.getAccountByCircuitId(circuitloaded.value._id)
accountloaded.value = userStore.getAccountByCircuitId(
circuitloaded.value._id
);
} else if (tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO) {
groupSel.value = userStore.my.profile.manage_mygroups.find((group: IMyGroup) => from_groupname.value === group.groupname)
groupSel.value = userStore.my.profile.manage_mygroups.find(
(group: IMyGroup) => from_groupname.value === group.groupname
);
accountloaded.value = groupSel.value ? groupSel.value.account : null
accountloaded.value = groupSel.value ? groupSel.value.account : null;
} else if (tipoConto.value === shared_consts.AccountType.COMMUNITY_ACCOUNT) {
from_contocom.value = circuitloaded.value.path
accountloaded.value = circuitloaded.value ? circuitloaded.value.account : null
from_contocom.value = circuitloaded.value.path;
accountloaded.value = circuitloaded.value
? circuitloaded.value.account
: null;
}
groupsListAdmin.value = userStore.GroupsListWhereIAmAdminInTheCircuit(circuitloaded.value.name)
// console.log('groupsListAdmin.value', groupsListAdmin.value)
groupsListAdmin.value = userStore.GroupsListWhereIAmAdminInTheCircuit(
circuitloaded.value.name
);
arrGroupsList.value = []
arrGroupsList.value = [];
if (groupsListAdmin.value) {
for (const group of groupsListAdmin.value) {
let aggiungi = true
let aggiungi = true;
if (props.to_group && props.to_group.groupname === group.groupname)
aggiungi = false
aggiungi = false;
if (aggiungi)
arrGroupsList.value.push({ label: group.groupname, value: group.groupname });
arrGroupsList.value.push({
label: group.groupname,
value: group.groupname,
});
}
}
if (arrGroupsList.value.length > 0) {
arrTypesAccounts.value.push(
{
label: t('circuit.conticollettivi'),
value: shared_consts.AccountType.CONTO_DI_GRUPPO,
})
}
if (tools.iCanSendCoinsSuperUserCircuit(circuitsel.value) && (!props.to_contocom)) {
arrTypesAccounts.value.push({
label: t('circuit.contocom'),
value: shared_consts.AccountType.COMMUNITY_ACCOUNT,
})
label: '👥 ' + t('circuit.conticollettivi'),
value: shared_consts.AccountType.CONTO_DI_GRUPPO,
});
}
// if (tools.iAmAdminCircuit(circuitloaded.value.name))
// arrGroupsList.value.push({ label: circuitloaded.value.name, value: circuitloaded.value.path });
if (
tools.iCanSendCoinsSuperUserCircuit(circuitsel.value) &&
!props.to_contocom
) {
arrTypesAccounts.value.push({
label: '🏛️ ' + t('circuit.contocom'),
value: shared_consts.AccountType.COMMUNITY_ACCOUNT,
});
}
// accountdest.value = userStore.getAccountByCircuitId(circuitloaded.value._id)
if (accountloaded.value) {
remainingCoins.value = circuitStore.getRemainingCoinsToSend(accountloaded.value)
remainingCoins.value = circuitStore.getRemainingCoinsToSend(
accountloaded.value
);
if (accountloaded.value.saldo > 0) {
maxsendable.value = accountloaded.value.saldo + accountloaded.value.fidoConcesso
maxsendable.value =
accountloaded.value.saldo + accountloaded.value.fidoConcesso;
} else {
maxsendable.value = accountloaded.value.fidoConcesso
maxsendable.value = accountloaded.value.fidoConcesso;
}
if (remainingCoins.value < 10) {
remainingCoins.value = 10
remainingCoins.value = 10;
}
numstep.value = Math.round(maxsendable.value / 10)
numstep.value = Math.round(maxsendable.value / 10);
if (numstep.value < 1) {
numstep.value = 1
numstep.value = 1;
}
const quanti = [...Array(20).keys()].map(i => i + 1)
const quanti = [...Array(20).keys()].map((i) => i + 1);
for (const ind of quanti) {
const valuenorm = ind * numstep.value
const value = ind * numstep.value
const valuenorm = ind * numstep.value;
const value = ind * numstep.value;
if (value > remainingCoins.value) {
break
break;
} else {
const label = valuenorm.toString()
arrayMarkerLabel.value.push({ value, label })
const label = valuenorm.toString();
arrayMarkerLabel.value.push({ value, label });
}
}
}
}
} finally {
loading.value = false
loading.value = false;
if (load) {
fine_caricamento();
}
}
}
}
function inizio_caricamento() {
$q.loading.show({
message: 'Caricamento in corso...',
spinnerColor: 'white',
backgroundColor: 'primary',
messageColor: 'white',
});
}
function fine_caricamento() {
$q.loading.hide();
}
async function mounted() {
inizio_caricamento();
loading.value = true
to_user_real.value = props.to_user;
loading.value = true;
arrTypesAccounts.value = [
{
label: t('circuit.personale') + ' ' + from_username.value,
label: '👤 ' + t('circuit.personale') + ' - ' + from_username.value,
value: shared_consts.AccountType.USER,
},
]
];
let salvatoprec = tools.getCookie(tools.COOK_TIPOCONTO, -2, true)
let salvatoprec = tools.getCookie(tools.COOK_TIPOCONTO, -2, true);
// ....
if (props.to_user) {
console.log('user', props.to_user)
bothcircuits.value = userStore.getMyCircuitsInCommonByUser(props.to_user)
if (to_user_real.value) {
if (props.loadprofile) {
await userStore
.loadUserProfile({ username: to_user_real.value.username })
.then((ris) => {
to_user_real.value = ris;
});
}
bothcircuits.value = userStore.getMyCircuitsInCommonByUser(to_user_real.value);
if (props.circuitname) {
circuitsel.value = props.circuitname
circuitsel.value = props.circuitname;
} else {
const circcookie = tools.getCookie(tools.CIRCUIT_USE, bothcircuits.value[0])
if (circcookie && bothcircuits.value.findIndex((circ: ICircuit) => circ.name === circcookie) >= 0)
circuitsel.value = circcookie
const circcookie = tools.getCookie(tools.CIRCUIT_USE, bothcircuits.value[0]);
if (
circcookie &&
bothcircuits.value.findIndex((circ: ICircuit) => circ.name === circcookie) >=
0
)
circuitsel.value = circcookie;
}
if (bothcircuits.value && bothcircuits.value.find((name: any) => name !== circuitsel.value)) {
circuitsel.value = bothcircuits.value[0]
if (
bothcircuits.value &&
bothcircuits.value.find((name: any) => name !== circuitsel.value)
) {
circuitsel.value = bothcircuits.value[0];
}
if (props.qtydefault)
qty.value = props.qtydefault
if (props.qtydefault) qty.value = props.qtydefault;
if (props.sendRIS) {
if (props.sendRIS !== '0')
qty.value = props.sendRIS
if (props.sendRIS !== '0') qty.value = props.sendRIS;
}
await aggiorna()
await aggiorna();
// Se quello scelto c'è ancora sulla lista, allora lo imposto
if (arrTypesAccounts.value.findIndex((rec: any) => rec.value === salvatoprec) >= 0) {
tipoConto.value = salvatoprec
if (
arrTypesAccounts.value.findIndex((rec: any) => rec.value === salvatoprec) >= 0
) {
tipoConto.value = salvatoprec;
} else {
tipoConto.value = shared_consts.AccountType.USER
tipoConto.value = shared_consts.AccountType.USER;
}
// tipoConto.value = tools.getCookie(tools.COOK_TIPOCONTO, shared_consts.AccountType.USER, true)
showpage.value = true
showpage.value = true;
fine_caricamento();
}
if (props.to_group) {
console.log('group', props.to_group)
bothcircuits.value = userStore.getMyCircuitsInCommonByGroup(props.to_group)
console.log('bothcircuits', bothcircuits.value)
bothcircuits.value = userStore.getMyCircuitsInCommonByGroup(props.to_group);
if (props.circuitname) {
circuitsel.value = props.circuitname
circuitsel.value = props.circuitname;
} else {
circuitsel.value = tools.getCookie(tools.CIRCUIT_USE, bothcircuits.value[0])
circuitsel.value = tools.getCookie(tools.CIRCUIT_USE, bothcircuits.value[0]);
}
if (!userStore.IsMyCircuitByName(circuitsel.value)) {
circuitsel.value = bothcircuits.value[0]
circuitsel.value = bothcircuits.value[0];
}
await aggiorna()
await aggiorna();
showpage.value = true
showpage.value = true;
}
if (props.to_contocom) {
bothcircuits.value = userStore.getMyCircuits()
bothcircuits.value = userStore.getMyCircuits();
console.log('to_contocom', props.to_contocom)
circuitsel.value = props.circuitname
circuitsel.value = props.circuitname;
if (!userStore.IsMyCircuitByName(circuitsel.value)) {
circuitsel.value = bothcircuits.value[0]
circuitsel.value = bothcircuits.value[0];
}
await aggiorna()
await aggiorna();
showpage.value = true
showpage.value = true;
}
loading.value = false
emit('showed')
loading.value = false;
emit('showed');
}
function hide() {
emit('close', true)
showpage.value = false
emit('close', true);
showpage.value = false;
}
function sendCoin() {
console.log('sendcoin', qty.value, props.to_group ? props.to_group.groupname : (props.to_user ? props.to_user.username : props.to_contocom))
const ok = (props.to_user && props.to_user.username) ||
const ok =
(to_user_real.value && to_user_real.value.username) ||
(props.to_group && props.to_group.groupname) ||
(props.to_contocom)
props.to_contocom;
if (ok && qty.value && circuitloaded.value) {
const myrecsendcoin: ISendCoin = {
@@ -371,122 +440,104 @@ export default defineComponent({
causal: causal.value,
symbol: circuitloaded.value.symbol,
causalDest: props.causalDest,
}
};
myrecsendcoin.groupdest = props.to_group ? props.to_group.groupname : ''
myrecsendcoin.contoComDest = props.to_contocom
myrecsendcoin.grouporig = tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO ? from_groupname.value : ''
myrecsendcoin.contoComOrig = tipoConto.value === shared_consts.AccountType.COMMUNITY_ACCOUNT ? from_contocom.value : ''
myrecsendcoin.dest = props.to_user ? props.to_user.username : ''
myrecsendcoin.groupdest = props.to_group ? props.to_group.groupname : '';
myrecsendcoin.contoComDest = props.to_contocom;
myrecsendcoin.grouporig =
tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO
? from_groupname.value
: '';
myrecsendcoin.contoComOrig =
tipoConto.value === shared_consts.AccountType.COMMUNITY_ACCOUNT
? from_contocom.value
: '';
myrecsendcoin.dest = to_user_real.value ? to_user_real.value.username : '';
if (myrecsendcoin) {
tools.sendCoinsByCircuit($q, $router, circuitloaded.value, myrecsendcoin)
tools
.sendCoinsByCircuit($q, $router, circuitloaded.value, myrecsendcoin)
.then((ris: any) => {
if (ris) {
showpage.value = false
showpage.value = false;
}
})
});
}
}
}
function ifNextCheck(actualstep: number) {
let fase1ok = false
let fase1ok = false;
if (circuitloaded.value && !!circuitloaded.value._id) {
fase1ok = !(
!circuitloaded.value.transactionsEnabled ||
(tipoConto.value === shared_consts.AccountType.USER &&
props.to_user &&
from_username.value === props.to_user.username) ||
(tipoConto.value ===
shared_consts.AccountType.CONTO_DI_GRUPPO &&
to_user_real.value &&
from_username.value === to_user_real.value.username) ||
(tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO &&
!from_groupname.value) ||
(tipoConto.value ===
shared_consts.AccountType.CONTO_DI_GRUPPO &&
(tipoConto.value === shared_consts.AccountType.CONTO_DI_GRUPPO &&
props.to_group &&
from_groupname.value &&
props.to_group.groupname === from_groupname.value) ||
(tipoConto.value ===
shared_consts.AccountType.COMMUNITY_ACCOUNT &&
(tipoConto.value === shared_consts.AccountType.COMMUNITY_ACCOUNT &&
!from_contocom.value)
)
}
if (actualstep === 0) {
return fase1ok
} else if (actualstep === 1) {
return fase1ok && checkRisValid()
} else if (actualstep === 2) {
return fase1ok && checkRisValid()
);
}
return fase1ok && checkRisValid() && causal.value;
}
function checkRisValid() {
return qty.value ? qty.value && getQty() >= 0.01 && accountloaded.value
&& getQty() <= circuitStore.getRemainingCoinsToSend(accountloaded.value) : false
return qty.value
? qty.value &&
getQty() >= 0.01 &&
accountloaded.value &&
getQty() <= circuitStore.getRemainingCoinsToSend(accountloaded.value)
: false;
}
function getQty(): number {
let myqty: number | null = null
let myqty: number | null = null;
try {
if (qty.value) {
myqty = parseFloat(String(qty.value))
myqty = parseFloat(String(qty.value));
}
} catch (e) {
return 0
return 0;
}
return myqty ? myqty : 0
return myqty ? myqty : 0;
}
function getTitle(step: number) {
if (step === 0) {
return 'Circuito'
return 'Circuito';
} else if (step === 1) {
return 'Quantità'
return 'Quantità';
} else if (step === 2) {
return 'Causale'
return 'Causale';
}
}
function getIcon(step: number) {
if (step === 0) {
return 'circuit'
return 'circuit';
} else if (step === 1) {
return 'attach_money'
return 'attach_money';
} else if (step === 2) {
return 'description'
return 'description';
}
}
function clickAvanti(actualstep: number) {
if (actualstep === 0) {
step.value = 1
} else if (actualstep === 1) {
step.value = 2
} else if (actualstep === 2) {
sendCoin()
}
}
function clickIndietro(actualstep: number) {
if (actualstep === 1) {
step.value = 0
} else if (actualstep === 2) {
step.value = 1
} else if (actualstep === 0) {
hide()
}
function setQty(value: string | number) {
qty.value = value;
}
onMounted(mounted)
onMounted(mounted);
return {
t,
@@ -522,12 +573,13 @@ export default defineComponent({
shared_consts,
step,
ifNextCheck,
clickIndietro,
clickAvanti,
getTitle,
getIcon,
sendCoinDialog,
visubanner,
}
showKeyboard,
setQty,
to_user_real,
};
},
})
});

View File

@@ -5,375 +5,351 @@
:maximized="$q.screen.lt.sm"
@hide="hide"
@show="qtyRef ? qtyRef.focus() : ''"
transition-show="slide-up"
transition-hide="slide-down"
>
<q-card
class="dialog_card"
style="display: flex; flex-direction: column; height: 100%"
class="send-coins-dialog"
:class="{ 'mobile-fullheight': $q.screen.lt.sm }"
>
<q-bar class="bg-primary text-white">
{{ $t("circuit.sendcoins") }}
<q-space />
<q-btn flat round color="white" icon="close" v-close-popup></q-btn>
</q-bar>
<!-- Header con gradiente -->
<q-card-section class="dialog-header q-pa-none">
<div class="header-gradient">
<!-- Top bar -->
<div class="header-top-bar">
<div class="header-title-wrapper">
<div class="ris-coin-icon">
<img
v-if="circuitloaded.symbol === 'RIS'"
src="/images/1ris_rosso_100.png"
alt="RIS"
class="ris-logo"
/>
<span v-else class="coin-symbol">{{ circuitloaded.symbol }}</span>
</div>
<span class="header-title">Invia {{ circuitloaded.symbol || 'Crediti' }}</span>
</div>
<q-btn
flat
round
dense
icon="close"
color="white"
class="close-btn"
v-close-popup
/>
</div>
<q-card-section>
<q-stepper
v-model="step"
ref="stepper"
color="primary"
animated
class="mystepper"
<!-- Balance Card compatto -->
<div class="balance-card">
<div class="balance-info">
<div class="balance-main">
<span class="balance-label">Saldo disponibile</span>
<span class="balance-value">
{{ accountloaded ? circuitStore.getRemainingCoinsToSend(accountloaded).toFixed(2) : '0.00' }}
<span class="balance-symbol">{{ circuitloaded.symbol }}</span>
</span>
</div>
<div class="balance-fido" v-if="accountloaded?.fidoConcesso > 0">
<span class="fido-label">Fido</span>
<span class="fido-value">+{{ accountloaded.fidoConcesso.toFixed(2) }}</span>
</div>
</div>
</div>
</div>
</q-card-section>
<!-- Content -->
<q-card-section
class="dialog-content scroll"
:style="$q.screen.lt.sm ? 'padding-bottom: 80px;' : ''"
>
<!-- Circuit Check -->
<CCheckCircuitsEnabled
:to_user="to_user_real"
:to_group="to_group"
/>
<!-- Circuit Selector -->
<div v-if="circuitloaded.symbol && circuitname === ''" class="section-block">
<q-select
v-model="circuitsel"
:options="bothcircuits"
:behavior="$q.platform.is.ios === true ? 'dialog' : 'menu'"
outlined
dense
label="Circuito"
class="modern-select"
popup-content-class="modern-select-popup"
>
<template v-slot:prepend>
<q-icon name="account_balance_wallet" color="primary" />
</template>
</q-select>
</div>
<!-- Province Banner -->
<q-banner
v-if="showProvinceToSelect"
rounded
class="warning-banner q-mb-sm"
>
<q-step
:name="0"
:title="getTitle(0)"
:icon="getIcon(0)"
:done="ifNextCheck(0)"
<template v-slot:avatar>
<q-icon name="warning" color="white" />
</template>
{{ $t('circuit.insertprovince_text') }}
</q-banner>
<!-- Sender Section -->
<div v-if="circuitsel" class="section-block">
<q-select
v-if="arrTypesAccounts.length > 0"
v-model="tipoConto"
:options="arrTypesAccounts"
outlined
dense
emit-value
map-options
label="Mittente"
class="modern-select"
popup-content-class="modern-select-popup"
>
<CCheckCircuitsEnabled :to_user="to_user" :to_group="to_group">
</CCheckCircuitsEnabled>
<template v-slot:prepend>
<q-icon name="person" color="primary" />
</template>
</q-select>
<div v-if="circuitloaded.symbol">
<q-select
v-if="circuitname === ''"
:behavior="$q.platform.is.ios === true ? 'dialog' : 'menu'"
rounded
dense
outlined
v-model="circuitsel"
:options="bothcircuits"
label="Circuito"
>
</q-select>
<div v-else>{{ circuitname }}</div>
</div>
<q-banner
v-if="showProvinceToSelect"
rounded
class="bg-red text-white"
style="text-align: center"
>
<div>
<em style="font-weight: bold">{{
$t("circuit.insertprovince_text")
}}</em>
</div>
<br />
</q-banner>
<br />
<div v-if="circuitsel">
<q-banner
rounded
dense
class="shadow-5 q-my-sm"
color="primary q-title"
style="text-align: center"
>
<div class="mybanner_left q-mb-sm">
<q-select
v-if="arrTypesAccounts.length > 0"
v-model="tipoConto"
class="my-custom-select"
outlined
bg-color="light-blue-2"
emit-value
map-options
:options="arrTypesAccounts"
:label="$t('circuit.sender')"
></q-select>
</div>
<div
v-if="
tipoConto === shared_consts.AccountType.CONTO_DI_GRUPPO
"
>
<q-select
v-model="from_groupname"
:options="arrGroupsList"
:label="$t('circuit.choosecontocom')"
rounded
emit-value
map-options
>
<!-- Mostra i gruppi su cui sei Admin -->
</q-select>
</div>
<div
v-else-if="
tipoConto === shared_consts.AccountType.COMMUNITY_ACCOUNT
"
>
<q-input
v-model="from_contocom"
:label="$t('circuit.contocom')"
readonly
class="q-my-sm"
>
</q-input>
</div>
<CSaldo
v-if="circuitloaded && circuitloaded.symbol"
:symbol="circuitloaded.symbol"
:color="circuitloaded.color"
:saldo="accountloaded ? accountloaded.saldo : 0"
:qtarem="
accountloaded
? circuitStore.getRemainingCoinsToSend(accountloaded)
: 0
"
>
</CSaldo>
</q-banner>
<q-banner
rounded
dense
class="shadow-5 q-my-sm"
color="primary q-title"
>
<div class="mybanner_left bg-green text-white q-mb-sm">
{{ $t("circuit.dest") }}
</div>
<!-- Destination -->
<CMyUserOnlyView
v-if="to_user"
:mycontact="to_user"
:visu="costanti.FIND_PEOPLE"
@setCmd="tools.setCmd"
>
</CMyUserOnlyView>
<CMyGroupOnlyView
v-if="to_group"
:mygrp="to_group"
:visu="costanti.USER_GROUPS"
:circuitname="circuitloaded.name"
>
</CMyGroupOnlyView>
<CMyGroupOnlyView
v-if="circuitloaded && !!circuitloaded._id && to_contocom"
:mygrp="{ groupname: to_contocom }"
:visu="costanti.USER_GROUPS"
:circuitname="circuitloaded.name"
>
</CMyGroupOnlyView>
</q-banner>
</div>
<q-inner-loading id="spinner" :showing="loading">
<q-spinner-tail size="6em" color="primary" />
</q-inner-loading>
</q-step>
<q-step
:name="1"
:title="getTitle(1)"
:icon="getIcon(1)"
:done="ifNextCheck(1)"
<!-- Group Account Selector -->
<q-select
v-if="tipoConto === shared_consts.AccountType.CONTO_DI_GRUPPO"
v-model="from_groupname"
:options="arrGroupsList"
:label="$t('circuit.choosecontocom')"
outlined
dense
emit-value
map-options
class="modern-select q-mt-sm"
>
<div v-if="circuitloaded && !!circuitloaded._id">
<q-banner
v-if="!circuitloaded.transactionsEnabled"
rounded
class="bg-red text-white"
style="text-align: center"
>
<em style="font-weight: bold">{{
$t("circuit.transactionsEnabled_text")
}}</em
><br />
</q-banner>
<template v-slot:prepend>
<q-icon name="groups" color="primary" />
</template>
</q-select>
<q-input
ref="qtyRef"
class="q-py-sm text-h5"
outlined
v-model="qty"
:type="$q.platform.is.mobile ? 'string' : 'number'"
:rules="[
(val) =>
val <=
circuitStore.getRemainingCoinsToSend(accountloaded) ||
t('circuit.qta_remaining_to_send', {
maxqta:
circuitStore.getRemainingCoinsToSend(accountloaded),
symbol: circuitloaded.symbol,
}),
(val) => val > 0 || t('circuit.qta_not_valid'),
]"
:label="
t('movement.amount_to_send', {
qtamax: circuitStore.getRemainingCoinsToSend(accountloaded)
? circuitStore
.getRemainingCoinsToSend(accountloaded)
.toFixed(2)
: 0 + ` ` + circuitloaded.symbol,
})
"
input-class="text-right"
input-style="padding-bottom: 24px !important;"
v-on:keyup.enter="$event.target.nextElementSibling.focus()"
>
<!--val => val > circuitStore.getMaxCoinsToSend(accountloaded) || t('circuit.qta_max_to_send', { maxqta: tools.getRemainingCoinsToSend(accountloaded), symbol: circuitloaded.symbol })]" -->
<template v-slot:append>
<div class="text-h5">
<em
class="q-px-sm text-white rounded-borders"
:style="
`background-color: ` +
(circuitloaded.color ? circuitloaded.color : '#ff5500')
"
>{{ circuitloaded.symbol }}</em
>
</div>
</template>
</q-input>
<div class="q-mt-md">
<CNumericKeyboard v-model="qty" :showInput="false" />
</div>
</div>
</q-step>
<q-step
:name="2"
:title="getTitle(2)"
:icon="getIcon(2)"
:done="ifNextCheck(2)"
<!-- Community Account -->
<q-input
v-if="tipoConto === shared_consts.AccountType.COMMUNITY_ACCOUNT"
v-model="from_contocom"
:label="$t('circuit.contocom')"
outlined
dense
readonly
class="modern-input q-mt-sm"
>
<!-- Destination -->
<CMyUserOnlyView
v-if="to_user"
:mycontact="to_user"
:visu="costanti.FIND_PEOPLE"
@setCmd="tools.setCmd"
>
</CMyUserOnlyView>
<CMyGroupOnlyView
v-if="to_group"
:mygrp="to_group"
:visu="costanti.USER_GROUPS"
:circuitname="circuitloaded.name"
>
</CMyGroupOnlyView>
<CMyGroupOnlyView
v-if="circuitloaded && !!circuitloaded._id && to_contocom"
:mygrp="{ groupname: to_contocom }"
:visu="costanti.USER_GROUPS"
:circuitname="circuitloaded.name"
>
</CMyGroupOnlyView>
<template v-slot:prepend>
<q-icon name="account_balance" color="primary" />
</template>
</q-input>
</div>
<div v-if="causalDest">
<q-field
rounded
filled
:label="t('circuit.descrizione_destinatario')"
stack-label
>
<template v-slot:append>
<q-icon name="description" />
</template>
<template v-slot:control>
<div class="self-center full-width no-outline" tabindex="0">
{{ causalDest }}
</div>
</template>
</q-field>
</div>
<!-- Recipient Section -->
<div v-if="circuitsel" class="section-block">
<label class="section-label">Destinatario</label>
<div class="recipient-card">
<div class="recipient-content">
<!-- User Recipient -->
<CMyUserOnlyView
v-if="to_user_real"
:mycontact="to_user_real"
:visu="costanti.FIND_PEOPLE"
@setCmd="tools.setCmd"
class="recipient-view"
/>
<!--<q-banner v-if="visubanner" rounded class="text-center text-bold">
{{ t('circuit.descr_casuale') }}
<template v-slot:action>
<q-btn label="Chiudi" flat @click="visubanner = false"></q-btn>
</template>
</q-banner>-->
<div class="q-mt-sm text-italic text-blue text-bold">
{{ $t("circuit.descr_casuale") }}
<!-- Group Recipient -->
<CMyGroupOnlyView
v-if="to_group"
:mygrp="to_group"
:visu="costanti.USER_GROUPS"
:circuitname="circuitloaded.name"
class="recipient-view"
/>
<!-- Community Account Recipient -->
<CMyGroupOnlyView
v-if="circuitloaded && !!circuitloaded._id && to_contocom"
:mygrp="{ groupname: to_contocom }"
:visu="costanti.USER_GROUPS"
:circuitname="circuitloaded.name"
class="recipient-view"
/>
</div>
</div>
</div>
<!-- Amount Section - Compatto -->
<div v-if="circuitsel" class="section-block">
<label class="section-label">Importo</label>
<div class="amount-input-row" @click="$q.screen.lt.sm ? showKeyboard = true : qtyRef?.focus()">
<q-input
ref="causalRef"
v-model="causal"
autogrow
rounded
filled
maxlength="200"
counter
:label="$t('circuit.note')"
class="q-my-sm full-width"
ref="qtyRef"
v-model="qty"
:type="$q.screen.lt.sm ? 'text' : 'number'"
outlined
dense
input-class="amount-input-field"
:readonly="$q.screen.lt.sm"
:rules="[
(val) => !isNaN(parseFloat(val)) || t('circuit.qta_not_valid'),
(val) => parseFloat(val) <= circuitStore.getRemainingCoinsToSend(accountloaded) || t('circuit.qta_remaining_to_send', { maxqta: circuitStore.getRemainingCoinsToSend(accountloaded), symbol: circuitloaded.symbol }),
(val) => parseFloat(val) > 0 || t('circuit.qta_not_valid'),
]"
hide-bottom-space
class="amount-input"
@keyup.enter="causalRef?.focus()"
>
<template v-slot:after>
<q-avatar>
<img
:src="
userStore.my.profile
? userStore.getImgByProfile(userStore.my)
: ''
"
:alt="userStore.my.username"
/>
</q-avatar>
</template>
<template v-slot:prepend>
<q-icon name="comment" />
<span class="currency-symbol"></span>
</template>
<template v-slot:append>
<div class="coin-badge" :style="`background: ${circuitloaded.color || '#ff5500'}`">
{{ circuitloaded.symbol }}
</div>
<q-btn
v-if="$q.screen.lt.sm"
flat
round
dense
icon="keyboard"
size="sm"
class="keyboard-btn q-ml-xs"
@click.stop="showKeyboard = true"
/>
</template>
</q-input>
</div>
</div>
<div class="sendris">
{{
$t("circuit.sendcoinsto", {
qty,
coin: circuitloaded.symbol,
dest: to_group
? to_group.groupname
: to_user
? tools.getNomeUtenteByRecUser(to_user)
: to_contocom,
})
}}
sul {{ circuitsel }}
</div>
</q-step>
</q-stepper>
<!-- Transactions Disabled Banner -->
<q-banner
v-if="circuitloaded && !!circuitloaded._id && !circuitloaded.transactionsEnabled"
rounded
class="error-banner q-mb-sm"
>
<template v-slot:avatar>
<q-icon name="error" color="white" />
</template>
{{ $t('circuit.transactionsEnabled_text') }}
</q-banner>
<!-- Note Section - Compatto -->
<div v-if="circuitsel" class="section-block">
<q-input
ref="causalRef"
v-model="causal"
outlined
dense
type="textarea"
rows="3"
maxlength="200"
counter
label="Nota per il destinatario"
placeholder="Scrivi un messaggio..."
class="modern-textarea"
:rules="[
(val) => !!val?.trim() || 'Inserisci un messaggio',
(val) => val.trim().length >= 2 || 'Minimo 2 caratteri',
]"
lazy-rules
>
<template v-slot:prepend>
<q-icon name="message" color="grey-6" class="self-start q-mt-xs" />
</template>
<template v-slot:after>
<q-avatar size="32px">
<img
:src="userStore.my.profile ? userStore.getImgByProfile(userStore.my) : ''"
:alt="userStore.my.username"
/>
</q-avatar>
</template>
</q-input>
</div>
</q-card-section>
<q-card-actions align="center" style="row justify-between">
<!-- Fixed Bottom Actions -->
<q-card-actions
class="dialog-actions"
:class="{ 'fixed-bottom-actions': $q.screen.lt.sm }"
>
<q-btn
class="col"
flat
:label="$t('dialog.indietro')"
color="primary"
icon="navigate_before"
@click="clickIndietro(step)"
></q-btn>
:label="t('dialog.cancel')"
class="cancel-btn"
v-close-popup
/>
<q-btn
class="col"
:disable="!ifNextCheck(step)"
:label="
step === 0 || step === 1
? t('dialog.avanti')
: t('circuit.sendcoins', {
qty,
coin: circuitloaded.symbol,
dest: to_group
? to_group.groupname
: to_user
? tools.getNomeUtenteByRecUser(to_user)
: to_contocom,
})
"
color="positive"
:icon-right="
step === 2 ? 'img: /images/1ris_rosso_100.png' : 'navigate_next'
"
@click="clickAvanti(step)"
></q-btn>
class="send-btn"
:class="{ 'btn-disabled': !ifNextCheck(step) }"
@click="sendCoin"
>
<span class="send-btn-text">Invia {{ qty || 0 }} {{ circuitloaded.symbol }}</span>
<img
v-if="circuitloaded.symbol === 'RIS'"
src="/images/1ris_rosso_100.png"
alt="RIS"
class="send-btn-icon"
/>
<q-icon v-else name="send" class="q-ml-sm" />
</q-btn>
</q-card-actions>
</q-card>
</q-dialog>
<!-- Numeric Keyboard Dialog -->
<q-dialog
v-model="showKeyboard"
position="bottom"
seamless
no-backdrop-dismiss
>
<q-card class="keyboard-dialog">
<q-card-section class="keyboard-header">
<div class="keyboard-header-content">
<span class="keyboard-title">Inserisci importo</span>
<q-btn
flat
dense
label="Fatto"
color="primary"
class="done-btn"
v-close-popup
/>
</div>
<div class="keyboard-display">
<span class="keyboard-amount">{{ qty || '0' }}</span>
<div class="keyboard-coin-badge" :style="`background: ${circuitloaded.color || '#ff5500'}`">
{{ circuitloaded.symbol }}
</div>
</div>
</q-card-section>
<q-card-section class="keyboard-section q-pa-sm">
<CNumericKeyboard
v-model="qty"
:showInput="false"
@update:model-value="setQty"
/>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script lang="ts" src="./CSendCoins.ts">
</script>
<script lang="ts" src="./CSendCoins.ts"></script>
<style lang="scss" scoped>
@import "./CSendCoins.scss";
@import './CSendCoins.scss';
</style>

View File

@@ -0,0 +1,62 @@
.check-email-page {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.check-email-container {
max-width: 500px;
margin: 0 auto;
flex: 1;
}
.email-icon-wrapper {
text-align: center;
margin-bottom: 24px;
.q-icon {
animation: pulse 2s ease-in-out infinite;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
.main-banner,
.info-banner {
.banner-content {
font-size: 15px;
line-height: 1.5;
}
}
.resend-card {
border-radius: 12px;
.countdown-section {
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
strong {
font-family: monospace;
font-size: 18px;
margin-left: 4px;
}
}
}
.helpful-links {
padding: 16px 0;
}

View File

@@ -0,0 +1,140 @@
import { defineComponent, ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { LandingFooter } from '@/components/LandingFooter';
import { useUserStore } from '@store/UserStore';
import { tools } from '@tools';
import { Api } from '@api';
const RESEND_COOLDOWN_MINUTES = 1;
const RESEND_COOLDOWN_MS = RESEND_COOLDOWN_MINUTES * 60 * 1000;
const STORAGE_KEY = 'lastVerificationEmailSent';
export default defineComponent({
name: 'CheckEmail',
components: { LandingFooter },
setup() {
const { t } = useI18n();
const router = useRouter();
const userStore = useUserStore();
// State
const sending = ref(false);
const emailSent = ref(false);
const errorMessage = ref('');
const timeLeft = ref(0);
let countdownInterval: ReturnType<typeof setInterval> | null = null;
// Computed
const canResend = computed(() => timeLeft.value <= 0);
const formattedTimeLeft = computed(() => {
const minutes = Math.floor(timeLeft.value / 60);
const seconds = timeLeft.value % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
});
// Methods
const getLastSentTime = (): number => {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? parseInt(stored, 10) : 0;
};
const setLastSentTime = () => {
localStorage.setItem(STORAGE_KEY, Date.now().toString());
};
const calculateTimeLeft = (): number => {
const lastSent = getLastSentTime();
if (!lastSent) return 0;
const elapsed = Date.now() - lastSent;
const remaining = RESEND_COOLDOWN_MS - elapsed;
return remaining > 0 ? Math.ceil(remaining / 1000) : 0;
};
const startCountdown = () => {
timeLeft.value = calculateTimeLeft();
if (timeLeft.value > 0) {
countdownInterval = setInterval(() => {
timeLeft.value = calculateTimeLeft();
if (timeLeft.value <= 0 && countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
}, 1000);
}
};
const resendVerificationEmail = async () => {
if (!canResend.value || sending.value) return;
sending.value = true;
emailSent.value = false;
errorMessage.value = '';
try {
await userStore.resendVerificationEmail();
setLastSentTime();
emailSent.value = true;
startCountdown();
// Nascondi messaggio successo dopo 5 secondi
setTimeout(() => {
emailSent.value = false;
}, 5000);
} catch (error: any) {
errorMessage.value =
error?.message ||
t('components.authentication.email_verification.errore_invio');
// Nascondi messaggio errore dopo 5 secondi
setTimeout(() => {
errorMessage.value = '';
}, 5000);
} finally {
sending.value = false;
}
};
const goToChangeEmail = () => {
router.push({ name: 'ChangeEmail' });
};
const logout = async () => {
await userStore.logout();
router.push({ name: 'Home' });
};
// Lifecycle
onMounted(() => {
startCountdown();
});
onUnmounted(() => {
if (countdownInterval) {
clearInterval(countdownInterval);
}
});
return {
t,
tools,
sending,
emailSent,
errorMessage,
canResend,
timeLeft,
formattedTimeLeft,
resendVerificationEmail,
goToChangeEmail,
logout,
};
},
});

View File

@@ -0,0 +1,156 @@
<template>
<q-page
v-if="tools.isLogged() && !tools.isEmailVerified()"
padding
class="check-email-page"
>
<div class="check-email-container">
<!-- Icona principale -->
<div class="email-icon-wrapper">
<q-icon
name="mark_email_unread"
size="80px"
color="warning"
/>
</div>
<!-- Banner principale -->
<q-banner
rounded
class="bg-warning text-black main-banner"
>
<template v-slot:avatar>
<q-icon
name="info"
color="black"
/>
</template>
<div
class="banner-content"
v-html="
$t('components.authentication.email_verification2.link_sent', {
botname: tools.getBotName(),
})
"
></div>
</q-banner>
<!-- Sezione Reinvio Email -->
<q-card
flat
bordered
class="resend-card q-mt-lg"
>
<q-card-section>
<div class="text-subtitle1 text-weight-medium q-mb-sm">
{{ t('components.authentication.email_verification2.non_ricevuta') }}
</div>
<!-- Countdown attivo -->
<div
v-if="!canResend"
class="countdown-section"
>
<q-icon
name="schedule"
size="20px"
color="grey-6"
class="q-mr-sm"
/>
<span class="text-grey-7">
{{ t('components.authentication.email_verification2.attendi') }}
<strong>{{ formattedTimeLeft }}</strong>
</span>
</div>
<!-- Bottone Reinvio -->
<q-btn
v-else
:loading="sending"
:disable="sending"
color="primary"
icon="send"
:label="t('components.authentication.email_verification2.reinvia')"
class="full-width q-mt-sm"
@click="resendVerificationEmail"
/>
<!-- Messaggio successo -->
<q-banner
v-if="emailSent"
rounded
dense
class="bg-positive text-white q-mt-md"
>
<template v-slot:avatar>
<q-icon
name="check_circle"
color="white"
/>
</template>
{{ t('components.authentication.email_verification2.inviata_successo') }}
</q-banner>
<!-- Messaggio errore -->
<q-banner
v-if="errorMessage"
rounded
dense
class="bg-negative text-white q-mt-md"
>
<template v-slot:avatar>
<q-icon
name="error"
color="white"
/>
</template>
{{ errorMessage }}
</q-banner>
</q-card-section>
<!-- Banner istruzioni -->
<q-banner
rounded
class="bg-grey-3 text-black info-banner q-mt-md"
>
<template v-slot:avatar>
<q-icon
name="help_outline"
color="grey-7"
/>
</template>
<div
class="banner-content"
v-html="$t('components.authentication.email_verification2.se_non_ricevo')"
></div>
</q-banner>
</q-card>
<!-- Link utili -->
<div class="helpful-links q-mt-lg text-center">
<q-btn
flat
dense
color="primary"
:label="t('components.authentication.email_verification2.cambia_email')"
@click="goToChangeEmail"
/>
<span class="q-mx-sm text-grey-5">|</span>
<q-btn
flat
dense
color="grey-7"
:label="t('components.authentication.email_verification2.logout')"
@click="logout"
/>
</div>
</div>
<LandingFooter />
</q-page>
</template>
<script lang="ts" src="./CheckEmail.ts"></script>
<style lang="scss" scoped>
@import './CheckEmail.scss';
</style>

View File

@@ -0,0 +1 @@
export {default as CheckEmail} from './checkemail.vue'

View File

@@ -0,0 +1,569 @@
<template>
<q-card
class="ollama-chat"
:style="{ height: height }"
>
<!-- Header -->
<q-card-section class="q-py-sm bg-primary text-white">
<div class="row items-center justify-between">
<div class="row items-center q-gutter-sm">
<q-badge
:color="isConnected ? 'green' : 'red'"
rounded
/>
<span class="text-subtitle1 text-weight-medium"
>{{ title }} - {{ settings.model }}</span
>
</div>
<div class="row q-gutter-xs">
<q-btn
flat
dense
round
icon="settings"
@click="showSettings = true"
>
<q-tooltip>Impostazioni</q-tooltip>
</q-btn>
<q-btn
flat
dense
round
icon="delete_sweep"
@click="clearChat"
>
<q-tooltip>Pulisci Chat</q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
<!-- Messages -->
<q-card-section
ref="messagesContainer"
class="messages-container q-pa-md"
>
<div
v-if="messages.length === 0"
class="text-center text-grey-6 q-py-xl"
>
<q-icon
name="chat"
size="64px"
color="grey-4"
/>
<p class="q-mt-md">Inizia una conversazione!</p>
</div>
<div
v-for="(msg, index) in messages"
:key="index"
class="message-wrapper q-mb-md"
:class="msg.role === 'user' ? 'text-right' : 'text-left'"
>
<div
class="message-bubble q-pa-sm q-px-md"
:class="msg.role === 'user' ? 'bg-primary text-white' : 'bg-grey-3 text-dark'"
>
<!-- Puntini solo se streaming E contenuto vuoto -->
<div
v-if="msg.role === 'assistant' && msg.isStreaming && !msg.content"
class="typing-indicator"
>
<span></span><span></span><span></span>
</div>
<!-- Contenuto messaggio (anche durante streaming se già arrivato) -->
<div
v-else
class="message-content"
>
<div v-html="formatMessage(msg.content)"></div>
</div>
</div>
<div class="text-caption text-grey-6 q-mt-xs">
{{ msg.role === 'user' ? 'Tu' : modelName }} {{ formatTime(msg.timestamp) }}
</div>
</div>
</q-card-section>
<!-- Input -->
<q-card-section class="q-pa-sm border-top">
<div class="row q-gutter-sm items-end">
<q-input
v-model="inputMessage"
:disable="isGenerating"
outlined
dense
autogrow
class="col"
placeholder="Scrivi un messaggio..."
@keydown.enter.prevent="handleEnter"
>
<template v-slot:append>
<q-btn
v-if="isGenerating"
flat
dense
round
icon="stop"
color="negative"
@click="stopGeneration"
>
<q-tooltip>Ferma generazione</q-tooltip>
</q-btn>
</template>
</q-input>
<q-btn
:loading="isGenerating"
:disable="!inputMessage.trim() || isGenerating"
color="primary"
icon="send"
@click="sendMessage"
>
<q-tooltip>Invia messaggio</q-tooltip>
</q-btn>
</div>
</q-card-section>
<!-- Settings Dialog -->
<q-dialog v-model="showSettings">
<q-card style="min-width: 350px">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Impostazioni</div>
<q-space />
<q-btn
icon="close"
flat
round
dense
v-close-popup
/>
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input
v-model="settings.baseUrl"
outlined
label="Ollama URL"
hint="URL del server Ollama"
/>
<q-select
v-model="settings.model"
:options="availableModels"
outlined
label="Modello"
emit-value
map-options
/>
<div>
<div class="text-caption q-mb-sm">
Temperatura: {{ settings.temperature }}
</div>
<q-slider
v-model="settings.temperature"
:min="0"
:max="2"
:step="0.1"
label
color="primary"
/>
</div>
<q-input
v-model="settings.systemPrompt"
outlined
type="textarea"
label="System Prompt (opzionale)"
hint="Istruzioni per il comportamento dell'AI"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
label="Annulla"
color="grey"
v-close-popup
/>
<q-btn
flat
label="Salva"
color="primary"
@click="saveSettings"
v-close-popup
/>
</q-card-actions>
</q-card>
</q-dialog>
</q-card>
</template>
<script>
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue';
import OllamaService from './OllamaService.js';
import { tools } from '@tools';
export default {
name: 'OllamaChat',
props: {
title: {
type: String,
default: 'Chat AI',
},
height: {
type: String,
default: '600px',
},
baseUrl: {
type: String,
default: 'http://localhost:11434',
},
model: {
type: String,
default: '',
},
temperature: {
type: Number,
default: 0.7,
},
systemPrompt: {
type: String,
default: '',
},
initialMessages: {
type: Array,
default: () => [],
},
},
emits: ['message-sent', 'response-received', 'error', 'settings-changed'],
setup(props, { emit }) {
// State
const messages = ref([...props.initialMessages]);
const inputMessage = ref('');
const isGenerating = ref(false);
const isConnected = ref(false);
const showSettings = ref(false);
const messagesContainer = ref(null);
const abortController = ref(null);
const availableModels = ref([]);
// Settings
const settings = reactive({
baseUrl: props.baseUrl,
model: props.model,
temperature: props.temperature,
systemPrompt: props.systemPrompt,
});
// Ollama service instance
const ollama = new OllamaService(settings.baseUrl);
// Computed
const modelName = computed(() => settings.model);
// Methods
const scrollToBottom = async () => {
await nextTick();
if (messagesContainer.value) {
const container = messagesContainer.value.$el || messagesContainer.value;
container.scrollTop = container.scrollHeight;
}
};
const formatMessage = (content) => {
if (!content) return '';
return content
.replace(
/```(\w+)?\n([\s\S]*?)```/g,
'<pre class="code-block"><code>$2</code></pre>'
)
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>');
};
const formatTime = (timestamp) => {
if (!timestamp) return '';
return new Date(timestamp).toLocaleTimeString('it-IT', {
hour: '2-digit',
minute: '2-digit',
});
};
const handleEnter = (event) => {
if (!event.shiftKey) {
sendMessage();
}
};
const sendMessage = async () => {
const content = inputMessage.value.trim();
if (!content || isGenerating.value) return;
// Add user message
const userMessage = {
role: 'user',
content,
timestamp: new Date(),
};
messages.value.push(userMessage);
inputMessage.value = '';
emit('message-sent', userMessage);
// Add placeholder for assistant
const assistantMessage = {
role: 'assistant',
content: '',
timestamp: new Date(),
isStreaming: true,
};
messages.value.push(assistantMessage);
const assistantIndex = messages.value.length - 1;
isGenerating.value = true;
abortController.value = new AbortController();
try {
ollama.setBaseUrl(settings.baseUrl);
const chatMessages = messages.value
.filter((m) => !m.isStreaming)
.map((m) => ({ role: m.role, content: m.content }));
await ollama.streamChat(
{
model: settings.model,
messages: chatMessages,
temperature: settings.temperature,
system: settings.systemPrompt || undefined,
},
(chunk, full) => {
messages.value[assistantIndex] = {
...messages.value[assistantIndex],
content: full,
isStreaming: true,
};
assistantMessage.isStreaming = true;
scrollToBottom();
},
(fullContent) => {
assistantMessage.content = fullContent;
assistantMessage.isStreaming = false;
assistantMessage.timestamp = new Date();
emit('response-received', assistantMessage);
}
);
isConnected.value = true;
} catch (error) {
assistantMessage.content = `❌ Errore: ${error.message}`;
assistantMessage.isStreaming = false;
isConnected.value = false;
emit('error', error);
}
isGenerating.value = false;
scrollToBottom();
};
const stopGeneration = () => {
if (abortController.value) {
abortController.value.abort();
isGenerating.value = false;
}
};
const clearChat = () => {
messages.value = [];
};
const loadModels = async () => {
try {
ollama.setBaseUrl(settings.baseUrl);
const models = await ollama.listModels();
availableModels.value = models.map((m) => ({
label: m.name,
value: m.name,
}));
isConnected.value = true;
// prende dal cookie salvato
const savedModel = tools.getCookie('ollama_model');
if (savedModel) {
settings.model = savedModel;
}
const modelIndex = availableModels.value.findIndex(
(m) => m.value === settings.model
);
if (modelIndex === -1) {
settings.model = availableModels.value[0].value;
}
scrollToBottom();
} catch (error) {
console.error('Error loadModels', error);
isConnected.value = false;
availableModels.value = [
{ label: 'llama3.2', value: 'llama3.2' },
{ label: 'llama3.1', value: 'llama3.1' },
{ label: 'mistral', value: 'mistral' },
{ label: 'codellama', value: 'codellama' },
];
}
};
const saveSettings = () => {
emit('settings-changed', { ...settings });
};
// Lifecycle
onMounted(() => {
try {
loadModels();
} catch (e) {
console.error('Error Mounted Chat:', e);
}
});
watch(
() => settings.model,
(newModel) => {
tools.setCookie('ollama_model', newModel);
},
{ deep: true }
);
// Watch messages for scroll
watch(messages, scrollToBottom, { deep: true });
return {
messages,
inputMessage,
isGenerating,
isConnected,
showSettings,
messagesContainer,
settings,
availableModels,
modelName,
formatMessage,
formatTime,
handleEnter,
sendMessage,
stopGeneration,
clearChat,
saveSettings,
tools,
};
},
};
</script>
<style scoped>
.ollama-chat {
display: flex;
flex-direction: column;
}
.messages-container {
flex: 1;
overflow-y: auto;
}
.message-bubble {
display: inline-block;
max-width: 80%;
border-radius: 16px;
word-break: break-word;
}
.text-right .message-bubble {
border-bottom-right-radius: 4px;
}
.text-left .message-bubble {
border-bottom-left-radius: 4px;
}
.message-content :deep(pre.code-block) {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 8px;
overflow-x: auto;
margin: 8px 0;
}
.message-content :deep(.inline-code) {
background: rgba(0, 0, 0, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 8px 0;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: currentColor;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%,
60%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
30% {
opacity: 1;
transform: scale(1);
}
}
.border-top {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.streaming-cursor {
display: inline-block;
animation: blink-cursor 0.8s infinite;
color: var(--q-primary);
margin-left: 2px;
}
@keyframes blink-cursor {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,388 @@
/**
* OllamaService - Servizio per interagire con Ollama API
* Usalo in Quasar/Vue.js per tutte le chiamate AI
*/
class OllamaService {
constructor(baseUrl = 'http://localhost:11434') {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.defaultModel = '';
this.defaultTemperature = 0.7;
}
// Configura l'URL base
setBaseUrl(url) {
this.baseUrl = url.replace(/\/$/, '');
}
// Configura il modello di default
setDefaultModel(model) {
this.defaultModel = model;
}
/**
* Chat con cronologia messaggi
* @param {Object} options - Opzioni della chat
* @param {string} options.model - Nome del modello
* @param {Array} options.messages - Array di messaggi [{role: 'user'|'assistant', content: '...'}]
* @param {boolean} options.stream - Abilita streaming
* @param {number} options.temperature - Temperatura (0-2)
* @param {string} options.system - System prompt opzionale
* @returns {Promise<string|Response>}
*/
async chat(options) {
const {
model = this.defaultModel,
messages,
stream = false,
temperature = this.defaultTemperature,
system = null,
maxTokens = null,
topP = null,
topK = null,
} = options;
const payload = {
model,
messages,
stream,
options: {
temperature,
...(maxTokens && { num_predict: maxTokens }),
...(topP && { top_p: topP }),
...(topK && { top_k: topK }),
},
};
if (system) {
payload.system = system;
}
const response = await fetch(`${this.baseUrl}/api2/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
}
if (stream) return response;
const data = await response.json();
return data.message.content;
}
/**
* Generazione testo semplice (senza cronologia)
* @param {Object} options - Opzioni di generazione
* @param {string} options.model - Nome del modello
* @param {string} options.prompt - Prompt di input
* @param {boolean} options.stream - Abilita streaming
* @param {number} options.temperature - Temperatura (0-2)
* @param {string} options.system - System prompt opzionale
* @returns {Promise<string|Response>}
*/
async generate(options) {
const {
model = this.defaultModel,
prompt,
stream = false,
temperature = this.defaultTemperature,
system = null,
maxTokens = null,
} = options;
const payload = {
model,
prompt,
stream,
options: {
temperature,
...(maxTokens && { num_predict: maxTokens }),
},
};
if (system) {
payload.system = system;
}
const response = await fetch(`${this.baseUrl}/api2/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
}
if (stream) return response;
const data = await response.json();
return data.response;
}
/**
* Chat con streaming e callback
* @param {Object} options - Opzioni della chat
* @param {Function} onChunk - Callback per ogni chunk (chunk, fullContent)
* @param {Function} onComplete - Callback al completamento (fullContent)
* @returns {Promise<string>}
*/
async streamChat(options, onChunk, onComplete = null) {
const response = await this.chat({ ...options, stream: true });
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullContent = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter((line) => line.trim());
for (const line of lines) {
try {
const json = JSON.parse(line);
if (json.message?.content) {
fullContent += json.message.content;
if (onChunk) onChunk(json.message.content, fullContent);
}
} catch (e) {
// Ignora linee non JSON
}
}
}
} finally {
reader.releaseLock();
}
if (onComplete) onComplete(fullContent);
return fullContent;
}
/**
* Generazione con streaming e callback
* @param {Object} options - Opzioni di generazione
* @param {Function} onChunk - Callback per ogni chunk
* @returns {Promise<string>}
*/
async streamGenerate(options, onChunk, onComplete = null) {
const response = await this.generate({ ...options, stream: true });
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullContent = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter((line) => line.trim());
for (const line of lines) {
try {
const json = JSON.parse(line);
if (json.response) {
fullContent += json.response;
if (onChunk) onChunk(json.response, fullContent);
}
} catch (e) {
// Ignora linee non JSON
}
}
}
} finally {
reader.releaseLock();
}
if (onComplete) onComplete(fullContent);
return fullContent;
}
/**
* Lista modelli disponibili
* @returns {Promise<Array>}
*/
async listModels() {
const response = await fetch(`${this.baseUrl}/api2/models`);
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
const data = await response.json();
return data.models || [];
}
/**
* Informazioni su un modello
* @param {string} model - Nome del modello
* @returns {Promise<Object>}
*/
async showModel(model) {
const response = await fetch(`${this.baseUrl}/api2/show`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: model }),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
return response.json();
}
/**
* Genera embeddings
* @param {string} model - Nome del modello
* @param {string|Array} prompt - Testo o array di testi
* @returns {Promise<Array>}
*/
async embeddings(model, prompt) {
const response = await fetch(`${this.baseUrl}/api2/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, prompt }),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
const data = await response.json();
return data.embedding;
}
// ============================================
// METODI HELPER PER USI COMUNI
// ============================================
/**
* Genera testo creativo
*/
async generateText(prompt, options = {}) {
return this.generate({
prompt,
temperature: 0.8,
...options,
});
}
/**
* Genera codice
*/
async generateCode(prompt, language = 'javascript', options = {}) {
return this.generate({
prompt: `Scrivi codice ${language} per: ${prompt}\n\nRispondi solo con il codice, senza spiegazioni.`,
temperature: 0.3,
...options,
});
}
/**
* Traduci testo
*/
async translate(text, targetLang = 'english', options = {}) {
return this.generate({
prompt: `Traduci il seguente testo in ${targetLang}. Rispondi solo con la traduzione:\n\n${text}`,
temperature: 0.3,
...options,
});
}
/**
* Riassumi testo
*/
async summarize(text, options = {}) {
return this.generate({
prompt: `Riassumi il seguente testo in modo conciso:\n\n${text}`,
temperature: 0.5,
...options,
});
}
/**
* Rispondi a una domanda basata su un contesto
*/
async answerQuestion(question, context, options = {}) {
return this.generate({
prompt: `Contesto:\n${context}\n\nDomanda: ${question}\n\nRispondi basandoti solo sul contesto fornito.`,
temperature: 0.3,
...options,
});
}
/**
* Estrai informazioni strutturate (JSON)
*/
async extractJSON(text, schema, options = {}) {
const response = await this.generate({
prompt: `Estrai le informazioni dal seguente testo e restituisci un JSON valido con questa struttura: ${JSON.stringify(schema)}\n\nTesto:\n${text}\n\nRispondi SOLO con il JSON, senza altro testo.`,
temperature: 0.1,
...options,
});
try {
// Cerca di estrarre JSON dalla risposta
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return JSON.parse(response);
} catch (e) {
throw new Error('Failed to parse JSON response: ' + response);
}
}
/**
* Analisi del sentiment
*/
async analyzeSentiment(text, options = {}) {
const response = await this.generate({
prompt: `Analizza il sentiment del seguente testo e rispondi SOLO con un JSON nel formato {"sentiment": "positive"|"negative"|"neutral", "confidence": 0.0-1.0, "explanation": "breve spiegazione"}\n\nTesto: ${text}`,
temperature: 0.1,
...options,
});
try {
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return JSON.parse(response);
} catch (e) {
return { sentiment: 'unknown', confidence: 0, raw: response };
}
}
/**
* Correggi grammatica
*/
async correctGrammar(text, language = 'italiano', options = {}) {
return this.generate({
prompt: `Correggi gli errori grammaticali nel seguente testo in ${language}. Rispondi solo con il testo corretto:\n\n${text}`,
temperature: 0.2,
...options,
});
}
/**
* Genera lista/bullet points
*/
async generateList(topic, count = 5, options = {}) {
return this.generate({
prompt: `Genera una lista di ${count} elementi su: ${topic}\n\nFormatta come lista puntata.`,
temperature: 0.7,
...options,
});
}
}
// Export per ES6 modules
export default OllamaService;
// Export per CommonJS
if (typeof module !== 'undefined' && module.exports) {
module.exports = OllamaService;
}

View File

@@ -0,0 +1,178 @@
/**
* useOllama - Composable Vue 3 per usare Ollama nelle tue applicazioni Quasar
*
* Esempio d'uso:
*
* import { useOllama } from './useOllama';
*
* const { generate, chat, isLoading, error } = useOllama({
* baseUrl: 'http://localhost:11434',
* model: 'llama3.2'
* });
*
* const result = await generate('Scrivi una poesia');
*/
import { ref, reactive } from 'vue';
import OllamaService from './OllamaService.js';
export function useOllama(options = {}) {
const {
baseUrl = 'http://localhost:11434',
model = 'llama3.2',
temperature = 0.7,
} = options;
// State
const isLoading = ref(false);
const error = ref(null);
const streamingContent = ref('');
const models = ref([]);
// Service instance
const service = new OllamaService(baseUrl);
service.setDefaultModel(model);
/**
* Genera testo
*/
const generate = async (prompt, opts = {}) => {
isLoading.value = true;
error.value = null;
streamingContent.value = '';
try {
const result = await service.generate({
prompt,
temperature,
...opts,
});
return result;
} catch (e) {
error.value = e.message;
throw e;
} finally {
isLoading.value = false;
}
};
/**
* Genera testo con streaming
*/
const generateStream = async (prompt, opts = {}) => {
isLoading.value = true;
error.value = null;
streamingContent.value = '';
try {
const result = await service.streamGenerate(
{ prompt, temperature, ...opts },
(chunk, full) => {
streamingContent.value = full;
}
);
return result;
} catch (e) {
error.value = e.message;
throw e;
} finally {
isLoading.value = false;
}
};
/**
* Chat con messaggi
*/
const chat = async (messages, opts = {}) => {
isLoading.value = true;
error.value = null;
streamingContent.value = '';
try {
const result = await service.chat({
messages,
temperature,
...opts,
});
return result;
} catch (e) {
error.value = e.message;
throw e;
} finally {
isLoading.value = false;
}
};
/**
* Chat con streaming
*/
const chatStream = async (messages, opts = {}) => {
isLoading.value = true;
error.value = null;
streamingContent.value = '';
try {
const result = await service.streamChat(
{ messages, temperature, ...opts },
(chunk, full) => {
streamingContent.value = full;
}
);
return result;
} catch (e) {
error.value = e.message;
throw e;
} finally {
isLoading.value = false;
}
};
/**
* Carica modelli disponibili
*/
const loadModels = async () => {
try {
models.value = await service.listModels();
return models.value;
} catch (e) {
error.value = e.message;
return [];
}
};
// Helper methods
const generateText = (prompt, opts) => service.generateText(prompt, opts);
const generateCode = (prompt, lang, opts) => service.generateCode(prompt, lang, opts);
const translate = (text, lang, opts) => service.translate(text, lang, opts);
const summarize = (text, opts) => service.summarize(text, opts);
const extractJSON = (text, schema, opts) => service.extractJSON(text, schema, opts);
const analyzeSentiment = (text, opts) => service.analyzeSentiment(text, opts);
return {
// State
isLoading,
error,
streamingContent,
models,
// Core methods
generate,
generateStream,
chat,
chatStream,
loadModels,
// Helper methods
generateText,
generateCode,
translate,
summarize,
extractJSON,
analyzeSentiment,
// Service access
service,
};
}
export default useOllama;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff