agenda: C8 OK + Usar/Revogar pacote saldo + UI de contract + ajustes UX
Cenário 8 (Pacote SALDO — Otávio Souza Ferreira 12 × R$ 50)
- Testado e passou. DB: 1 rule, 0 events, 1 contract (saldo), 0 records.
Visual: 12 virtuais limpas no calendário.
UI de pacote (saldo + upfront)
- _ruleContractMap em useMelissaAgenda: bulk-load popula contract info
(id, style, totalSessions, sessionsUsed, packagePrice) por
recurrence_id. Query recurrence_rules.patient_id como fonte
autoritativa — cobre saldo sem materializadas (sem isso, ruleToPatient
via records vinha vazio pra saldo)
- normalize injeta `contract` no evento via ruleContractMap
- MelissaEventoPanel: nova linha colorida (violeta saldo, verde upfront)
com "Pacote saldo · N/M usadas" ou "Pacote · N/M realizadas"
- AgendaEventDialog: info card mt-4 com header+body+hint explicando
modelo, gateado por occFinancialLoading (spinner durante carga
pra evitar piscar entre Usar/Revogar)
Handlers Usar/Revogar atômicos
- onUsarSessao em MelissaLayout: materializa virtual (preserva
determined_commitment_id da regra) → status=realizado +
billing_contract_id → create_financial_record_for_session →
sessions_used++ → (se atingiu total) contract.status=completed
- onRevogarSessao: cancela record + sessions_used-- + reativa contract
se estava completed + status=agendado. Bloqueia se record paid
(precisa estorno formal pelo Financeiro)
- Ambos aceitam payload {eventRow, contract} do dialog OU fallback
pra eventoSelecionado do popover
- Botão "Usar" verde no popover (paymentState=none) substituído por
"Revogar" vermelho (paymentState=pending). Equivalente "Usar agora"/
"Revogar uso" no info card do dialog
Fix enum status_evento_agenda
- 'realizada' não existe no enum — DB exige 'realizado' (masculino).
Corrigido em todas as ocorrências do handler
Fix campo "Título" indevido em sessão
- Sessão sem determined_commitment_id → selectedCommitment=null →
isSessionEvent=false → mostra campo Título (que é só pra não-sessão)
- Fix: materialize do Usar inclui determined_commitment_id (insert
path); update path backfilla via query da rule se NULL; Revogar
também backfilla pra consistência
Fix "Gerar fatura" não cabe em saldo
- Botão "Gerar fatura" do popover hide quando há contractInfo. Em
saldo, gerar fatura solta criaria cobrança duplicada sem incrementar
sessions_used. Fluxo correto: "Usar"
Recorrências Aplicadas — UI
- Header stats coloridos: total **azul**, realizadas **verde**,
faltaram **amber**, canceladas **cinza**, remarcadas **violeta**
- Pills com badge sólido por status (emerald-600 realizado, amber-600
faltou, stone-500 cancelado, violet-600 remarcado)
Race condition no dialog
- AgendaEventDialog mostrava botões Usar/Revogar baseado em
occFinancialRecord async; durante ~500ms de load, botão errado
podia piscar. Fix: spinner "Verificando estado…" enquanto
occFinancialLoading=true; botões só renderizam após
- Popover não fixado (race window pequena, fechar/reabrir resolve)
3 decisões UX confirmadas antes de codar
- Editar serviço pago → NÃO (cobrança fiscal imutável)
- Alternar Particular/Convênio/Gratuito em série cobrada → NÃO
- Gerar fatura individual em pacote upfront → NÃO (duplicação)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,7 +147,7 @@ const props = defineProps({
|
||||
blockOverlapWarning: { type: Object, default: null }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated']);
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated', 'usar-sessao', 'revogar-sessao']);
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
@@ -596,6 +596,7 @@ const {
|
||||
occFinancialRecord,
|
||||
occFinancialLoading,
|
||||
sessionPaymentRecord,
|
||||
sessionContract,
|
||||
serieCountByStatus,
|
||||
pillDeleteMenuItems,
|
||||
loadSerieEvents,
|
||||
@@ -1009,6 +1010,40 @@ const paymentSummary = computed(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
// Info card do pacote (saldo/upfront) — quando a sessão pertence a uma
|
||||
// série com billing_contract ativo. Pra saldo é a única forma do user
|
||||
// entender o que tá acontecendo (nada aparece em /financeiro até
|
||||
// realizar). Pra upfront é redundante com o lock-edit mas ainda útil.
|
||||
const sessionContractInfo = computed(() => {
|
||||
const c = sessionContract.value;
|
||||
if (!c || c.type !== 'package') return null;
|
||||
const total = c.total_sessions || 0;
|
||||
const used = c.sessions_used || 0;
|
||||
const remaining = Math.max(0, total - used);
|
||||
const totalPrice = Number(c.package_price || 0);
|
||||
const perSession = total > 0 ? totalPrice / total : 0;
|
||||
const remainingValue = perSession * remaining;
|
||||
return {
|
||||
id: c.id,
|
||||
style: c.charging_style || 'upfront',
|
||||
total,
|
||||
used,
|
||||
remaining,
|
||||
totalPrice,
|
||||
perSession,
|
||||
remainingValue,
|
||||
// Forma "popover-shape" pra reusar no emit usar-sessao (handler em
|
||||
// MelissaLayout aceita ambos os fluxos: popover ou dialog)
|
||||
_normalized: {
|
||||
id: c.id,
|
||||
style: c.charging_style || 'upfront',
|
||||
totalSessions: total,
|
||||
sessionsUsed: used,
|
||||
packagePrice: totalPrice
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ── Preview "antes e depois" das ocorrencias afetadas ─────────────────
|
||||
// So vale no occurrenceMode quando o user escolhe escopo > somente_este.
|
||||
// Mostra abaixo do card "Aplicar alteracoes em" duas colunas: as datas
|
||||
@@ -2153,11 +2188,11 @@ onBeforeUnmount(() => {
|
||||
<i class="pi pi-refresh shrink-0" />
|
||||
<span>Recorrências Aplicadas</span>
|
||||
<div v-if="!serieLoading && serieEvents.length" class="serie-panel__stats">
|
||||
<span>{{ serieEvents.length }} sessões</span>
|
||||
<span v-if="serieCountByStatus.realizado"> · {{ serieCountByStatus.realizado }} realizadas</span>
|
||||
<span v-if="serieCountByStatus.faltou"> · {{ serieCountByStatus.faltou }} faltaram</span>
|
||||
<span v-if="serieCountByStatus.cancelado"> · {{ serieCountByStatus.cancelado }} canceladas</span>
|
||||
<span v-if="serieCountByStatus.remarcado"> · {{ serieCountByStatus.remarcado }} para remarcar</span>
|
||||
<span class="serie-stat serie-stat--total">{{ serieEvents.length }} sessões</span>
|
||||
<span v-if="serieCountByStatus.realizado" class="serie-stat serie-stat--realizado"> · {{ serieCountByStatus.realizado }} realizadas</span>
|
||||
<span v-if="serieCountByStatus.faltou" class="serie-stat serie-stat--faltou"> · {{ serieCountByStatus.faltou }} faltaram</span>
|
||||
<span v-if="serieCountByStatus.cancelado" class="serie-stat serie-stat--cancelado"> · {{ serieCountByStatus.cancelado }} canceladas</span>
|
||||
<span v-if="serieCountByStatus.remarcado" class="serie-stat serie-stat--remarcado"> · {{ serieCountByStatus.remarcado }} para remarcar</span>
|
||||
</div>
|
||||
<span v-if="serieLoading" class="ml-auto text-xs opacity-50">Carregando…</span>
|
||||
</div>
|
||||
@@ -2197,6 +2232,70 @@ onBeforeUnmount(() => {
|
||||
2026-05-12 — só aparece no 2º dialog empilhado de ocorrência. -->
|
||||
<div v-if="!occurrenceMode" class="composer-right">
|
||||
|
||||
<!-- Info card do pacote (saldo/upfront) — quando a sessão
|
||||
pertence a série com billing_contract ativo. Pra saldo
|
||||
é a única forma do user entender que existe um saldo
|
||||
(sem record em /financeiro até realizar). 2026-05-19 noite. -->
|
||||
<div v-if="sessionContractInfo && isSessionEvent" class="aed-contract-card mt-4" :class="`aed-contract-card--${sessionContractInfo.style}`">
|
||||
<div class="aed-contract-card__head">
|
||||
<i class="pi pi-box" />
|
||||
<span class="aed-contract-card__title">
|
||||
Pacote {{ sessionContractInfo.style === 'saldo' ? 'saldo' : 'fechado (upfront)' }}
|
||||
</span>
|
||||
<span class="aed-contract-card__count">{{ sessionContractInfo.used }} / {{ sessionContractInfo.total }} usadas</span>
|
||||
</div>
|
||||
<div class="aed-contract-card__body">
|
||||
<div class="aed-contract-card__row">
|
||||
<span class="aed-contract-card__label">Total contratado:</span>
|
||||
<span class="aed-contract-card__value">{{ fmtBRL(sessionContractInfo.totalPrice) }}</span>
|
||||
</div>
|
||||
<div class="aed-contract-card__row">
|
||||
<span class="aed-contract-card__label">Por sessão:</span>
|
||||
<span class="aed-contract-card__value">{{ fmtBRL(sessionContractInfo.perSession) }}</span>
|
||||
</div>
|
||||
<div v-if="sessionContractInfo.remaining > 0" class="aed-contract-card__row">
|
||||
<span class="aed-contract-card__label">Restam:</span>
|
||||
<span class="aed-contract-card__value">{{ sessionContractInfo.remaining }} sessão(ões) · {{ fmtBRL(sessionContractInfo.remainingValue) }}</span>
|
||||
</div>
|
||||
<!-- Spinner enquanto carrega occFinancialRecord pra
|
||||
evitar piscar entre "Usar" e "Revogar". -->
|
||||
<div v-if="occFinancialLoading" class="aed-contract-card__loading">
|
||||
<i class="pi pi-spinner pi-spin" />
|
||||
<span>Verificando estado…</span>
|
||||
</div>
|
||||
<!-- Revogar: sessão já consumida (record pending vinculado),
|
||||
paciente ainda não pagou. Atalho pra desfazer. -->
|
||||
<Button
|
||||
v-else-if="sessionContractInfo.style === 'saldo' && occFinancialRecord && occFinancialRecord.status === 'pending'"
|
||||
label="Revogar uso"
|
||||
icon="pi pi-undo"
|
||||
size="small"
|
||||
severity="danger"
|
||||
outlined
|
||||
class="rounded-full mt-2 aed-contract-card__usar"
|
||||
@click="emit('revogar-sessao', { eventRow, contract: sessionContractInfo._normalized })"
|
||||
/>
|
||||
<!-- Usar agora: sessão ainda não usada + tem saldo -->
|
||||
<Button
|
||||
v-else-if="sessionContractInfo.style === 'saldo' && sessionContractInfo.remaining > 0 && !occFinancialRecord"
|
||||
label="Usar agora"
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
class="rounded-full mt-2 aed-contract-card__usar"
|
||||
@click="emit('usar-sessao', { eventRow, contract: sessionContractInfo._normalized })"
|
||||
/>
|
||||
<div class="aed-contract-card__hint">
|
||||
<i class="pi pi-info-circle" />
|
||||
<template v-if="sessionContractInfo.style === 'saldo'">
|
||||
Modelo <b>saldo</b>: cada sessão gera cobrança ao ser marcada como <b>realizada</b>. Use <b>"Usar agora"</b> pra consumir 1 do saldo (marca realizada + gera fatura de {{ fmtBRL(sessionContractInfo.perSession) }}).
|
||||
</template>
|
||||
<template v-else>
|
||||
Modelo <b>upfront</b>: todas as sessões foram cobradas de uma vez no início do pacote.
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SESSÃO/HONORÁRIOS + FREQUÊNCIA (lado a lado em desktop) ──
|
||||
Quando ha serie (edicao de evento ja vinculado a regra),
|
||||
Frequencia some e Sessao/Honorarios ocupa a linha sozinho. -->
|
||||
@@ -3635,6 +3734,104 @@ onBeforeUnmount(() => {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Info card de pacote (saldo/upfront) — 2026-05-19 noite. Mostra status
|
||||
do contrato quando sessão pertence a série com billing_contract ativo.
|
||||
Saldo: tom violeta (contexto, modelo Cliniko). Upfront: tom verde
|
||||
(já cobrado, status positivo). */
|
||||
.aed-contract-card {
|
||||
border-radius: 10px;
|
||||
border: 1px solid;
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.aed-contract-card--saldo {
|
||||
background: color-mix(in srgb, rgb(124, 58, 237) 6%, var(--surface-card));
|
||||
border-color: color-mix(in srgb, rgb(124, 58, 237) 30%, transparent);
|
||||
}
|
||||
.aed-contract-card--upfront {
|
||||
background: color-mix(in srgb, rgb(16, 185, 129) 6%, var(--surface-card));
|
||||
border-color: color-mix(in srgb, rgb(16, 185, 129) 30%, transparent);
|
||||
}
|
||||
.aed-contract-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--surface-border), transparent 30%);
|
||||
font-weight: 600;
|
||||
}
|
||||
.aed-contract-card--saldo .aed-contract-card__head > i {
|
||||
color: rgb(124, 58, 237);
|
||||
}
|
||||
.aed-contract-card--upfront .aed-contract-card__head > i {
|
||||
color: rgb(16, 185, 129);
|
||||
}
|
||||
.aed-contract-card__title {
|
||||
flex: 1;
|
||||
}
|
||||
.aed-contract-card__count {
|
||||
font-size: 0.74rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.aed-contract-card__body {
|
||||
padding: 0.7rem 0.85rem 0.85rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.aed-contract-card__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.aed-contract-card__label {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.aed-contract-card__value {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.aed-contract-card__hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px dashed color-mix(in srgb, var(--surface-border), transparent 30%);
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.aed-contract-card__hint > i {
|
||||
margin-top: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.aed-contract-card--saldo .aed-contract-card__hint > i {
|
||||
color: rgb(124, 58, 237);
|
||||
}
|
||||
.aed-contract-card--upfront .aed-contract-card__hint > i {
|
||||
color: rgb(16, 185, 129);
|
||||
}
|
||||
.aed-contract-card__hint b {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.aed-contract-card__loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.aed-contract-card__loading > i {
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
|
||||
/* Padding interno do card "Campos Extras (compromisso)" — mesmo
|
||||
tratamento do aed-pay-body. Sem isso os inputs ficam grudados nas
|
||||
bordas. */
|
||||
@@ -5195,12 +5392,42 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
opacity: 0.75;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
/* Contadores coloridos por estado no header de Recorrências Aplicadas */
|
||||
.serie-stat--total {
|
||||
color: rgb(37, 99, 235); /* blue-600 */
|
||||
}
|
||||
.serie-stat--realizado {
|
||||
color: rgb(5, 150, 105); /* emerald-600 */
|
||||
}
|
||||
.serie-stat--faltou {
|
||||
color: rgb(217, 119, 6); /* amber-600 */
|
||||
}
|
||||
.serie-stat--cancelado {
|
||||
color: rgb(120, 113, 108); /* stone-500 */
|
||||
}
|
||||
.serie-stat--remarcado {
|
||||
color: rgb(124, 58, 237); /* violet-600 */
|
||||
}
|
||||
html.app-dark .serie-stat--total {
|
||||
color: rgb(96, 165, 250); /* blue-400 */
|
||||
}
|
||||
html.app-dark .serie-stat--realizado {
|
||||
color: rgb(52, 211, 153); /* emerald-400 */
|
||||
}
|
||||
html.app-dark .serie-stat--faltou {
|
||||
color: rgb(251, 191, 36); /* amber-400 */
|
||||
}
|
||||
html.app-dark .serie-stat--cancelado {
|
||||
color: rgb(168, 162, 158); /* stone-400 */
|
||||
}
|
||||
html.app-dark .serie-stat--remarcado {
|
||||
color: rgb(167, 139, 250); /* violet-400 */
|
||||
}
|
||||
.serie-panel__empty {
|
||||
padding: 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
@@ -5310,6 +5537,47 @@ onBeforeUnmount(() => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Badge sólido por status quando relevante (realizado/faltou/cancelado/
|
||||
remarcado). Agendado fica neutro (default acima). */
|
||||
.serie-pill--realizado .serie-pill__status-badge,
|
||||
.serie-pill--realizada .serie-pill__status-badge {
|
||||
flex: 0 0 auto;
|
||||
color: white;
|
||||
background: rgb(5, 150, 105); /* emerald-600 */
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.serie-pill--faltou .serie-pill__status-badge {
|
||||
flex: 0 0 auto;
|
||||
color: white;
|
||||
background: rgb(217, 119, 6); /* amber-600 */
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.serie-pill--cancelado .serie-pill__status-badge,
|
||||
.serie-pill--cancelada .serie-pill__status-badge {
|
||||
flex: 0 0 auto;
|
||||
color: white;
|
||||
background: rgb(120, 113, 108); /* stone-500 */
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.serie-pill--remarcado .serie-pill__status-badge,
|
||||
.serie-pill--remarcada .serie-pill__status-badge {
|
||||
flex: 0 0 auto;
|
||||
color: white;
|
||||
background: rgb(124, 58, 237); /* violet-600 */
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.serie-pill__cur-badge {
|
||||
font-size: 0.58rem;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -112,6 +112,14 @@ export function useAgendaEventLifecycle({
|
||||
// continua via occFinancialRecord (território da Fase 6/C13).
|
||||
const sessionPaymentRecord = ref(null);
|
||||
|
||||
// sessionContract (2026-05-19 noite): billing_contract ativo do paciente
|
||||
// quando a sessão pertence a uma série com pacote (upfront ou saldo).
|
||||
// Usado pra exibir info card no dialog explicando o pacote (qtd
|
||||
// sessões usadas/restantes, valor, comportamento). Pra saldo, é a
|
||||
// única forma do user entender o que tá acontecendo (nada aparece
|
||||
// no /financeiro até realizar sessão).
|
||||
const sessionContract = ref(null);
|
||||
|
||||
// ── computeds locais ───────────────────────────────────────
|
||||
const serieCountByStatus = computed(() => {
|
||||
const counts = {};
|
||||
@@ -323,6 +331,33 @@ export function useAgendaEventLifecycle({
|
||||
}
|
||||
}
|
||||
|
||||
// loadSessionContract (2026-05-19 noite): busca billing_contract ativo
|
||||
// do paciente quando o evento pertence a uma série com pacote. Usado
|
||||
// pra info card no dialog explicando o pacote saldo/upfront.
|
||||
async function loadSessionContract() {
|
||||
sessionContract.value = null;
|
||||
const patientId = props.eventRow?.paciente_id || props.eventRow?.patient_id;
|
||||
const ruleId = props.eventRow?.recurrence_id;
|
||||
// Só faz sentido pra sessão de série
|
||||
if (!patientId || !ruleId) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('billing_contracts')
|
||||
.select('id, type, total_sessions, sessions_used, package_price, charging_style, status, active_from')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'package')
|
||||
.eq('status', 'active')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
sessionContract.value = data ?? null;
|
||||
} catch (e) {
|
||||
console.warn('[session-contract] erro ao carregar:', e?.message);
|
||||
sessionContract.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onPillEditClick(ev) {
|
||||
emit('editSeriesOccurrence', {
|
||||
id: ev.id,
|
||||
@@ -508,6 +543,7 @@ export function useAgendaEventLifecycle({
|
||||
// sessionPaymentRecord: carrega em qualquer edit (Melissa
|
||||
// tambem) pra alimentar a linha "Cobrança" do Resumo lateral.
|
||||
loadSessionPaymentRecord();
|
||||
loadSessionContract();
|
||||
|
||||
// occurrenceMode: editando UMA ocorrencia de serie ja existente —
|
||||
// tipo de compromisso ja foi escolhido (paciente + sessao). Pular
|
||||
@@ -628,6 +664,8 @@ export function useAgendaEventLifecycle({
|
||||
loadSerieEvents,
|
||||
loadOccFinancialRecord,
|
||||
loadSessionPaymentRecord,
|
||||
sessionContract,
|
||||
loadSessionContract,
|
||||
onPillEditClick,
|
||||
onPillStatusChange,
|
||||
onPillDeleteClick,
|
||||
|
||||
Reference in New Issue
Block a user