fix: corregir parseBrandbook para deserializar strings de documents, restaurar paneles y sincronización bidireccional total entre Zona de Marca y Creatividades Ads
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
|
||||
import { isAgencyRole, supabaseUrl, userRole } from '@/lib/agency';
|
||||
import { createClient as createAdminClient } from '@supabase/supabase-js';
|
||||
import {
|
||||
BRANDBOOK_DOC_TITLE,
|
||||
@@ -29,12 +29,16 @@ async function loadDoc(tenantId: string): Promise<{
|
||||
id: string | null;
|
||||
book: Brandbook | null;
|
||||
}> {
|
||||
const r = await restGet<{ id: string; content: unknown }[]>(
|
||||
`/rest/v1/documents?tenant_id=eq.${encodeURIComponent(
|
||||
tenantId
|
||||
)}&type=eq.${BRANDBOOK_DOC_TYPE}&select=id,content&order=updated_at.desc&limit=1`
|
||||
);
|
||||
const row = r.data?.[0];
|
||||
const admin = getAdmin();
|
||||
const { data: row } = await admin
|
||||
.from('documents')
|
||||
.select('id, content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', BRANDBOOK_DOC_TYPE)
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (!row) return { id: null, book: null };
|
||||
return { id: row.id, book: parseBrandbook(row.content) };
|
||||
}
|
||||
@@ -48,8 +52,11 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const isAgency = isAgencyRole(userRole(user));
|
||||
const queryTenantId = req.nextUrl.searchParams.get('tenant_id') || req.nextUrl.searchParams.get('tenantId');
|
||||
const role = userRole(user);
|
||||
const isAgency = isAgencyRole(role);
|
||||
const queryTenantId =
|
||||
req.nextUrl.searchParams.get('tenant_id') ||
|
||||
req.nextUrl.searchParams.get('tenantId');
|
||||
|
||||
let tenantId: string | null = null;
|
||||
let label = 'Cliente';
|
||||
@@ -88,7 +95,56 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
const { id, book } = await loadDoc(tenantId);
|
||||
const current = book || emptyBrandbook(label);
|
||||
let current = book || emptyBrandbook(label);
|
||||
|
||||
// Sincronizar / fusionar con tenants.creative_style para que absolutamente NADA se pierda
|
||||
const admin = getAdmin();
|
||||
const { data: tenantRow } = await admin
|
||||
.from('tenants')
|
||||
.select('creative_style')
|
||||
.eq('id', tenantId)
|
||||
.maybeSingle();
|
||||
|
||||
if (tenantRow?.creative_style && typeof tenantRow.creative_style === 'object') {
|
||||
const cs = tenantRow.creative_style as any;
|
||||
const idObj = cs.identity || {};
|
||||
const audObj = cs.audiences || {};
|
||||
const argObj = cs.arguments || {};
|
||||
const proObj = cs.prohibitions || {};
|
||||
const voiObj = cs.voice || {};
|
||||
const visObj = cs.visual || {};
|
||||
|
||||
current = {
|
||||
...current,
|
||||
website: current.website || idObj.website || '',
|
||||
b2b_offering: current.b2b_offering || idObj.b2b_offering || '',
|
||||
b2c_offering: current.b2c_offering || idObj.b2c_offering || '',
|
||||
usp_differentiators: current.usp_differentiators || idObj.usp_differentiators || '',
|
||||
current_priorities: current.current_priorities || idObj.current_priorities || '',
|
||||
price_and_offer_framing: current.price_and_offer_framing || idObj.price_and_offer_framing || '',
|
||||
elevator_pitch: current.elevator_pitch || idObj.summary || '',
|
||||
tone: current.tone || voiObj.tone || '',
|
||||
copy_structure_rules: current.copy_structure_rules || voiObj.copy_structure_rules || '',
|
||||
english_terms_policy: current.english_terms_policy || voiObj.english_terms_policy || '',
|
||||
power_words: current.power_words?.length ? current.power_words : (voiObj.power_words || []),
|
||||
allowed_ctas: current.allowed_ctas?.length ? current.allowed_ctas : (voiObj.allowed_ctas || []),
|
||||
surface_rules: current.surface_rules || visObj.surface_rules || '',
|
||||
image_text_rules: current.image_text_rules || visObj.image_text_rules || '',
|
||||
typography_headline: current.typography_headline || visObj.typography_headline || '',
|
||||
typography_body: current.typography_body || visObj.typography_body || '',
|
||||
b2b_personas: current.b2b_personas?.length ? current.b2b_personas : (audObj.b2b_personas || []),
|
||||
b2c_personas: current.b2c_personas?.length ? current.b2c_personas : (audObj.b2c_personas || []),
|
||||
customer_frustrations: current.customer_frustrations || audObj.customer_frustrations || '',
|
||||
desired_transformation: current.desired_transformation || audObj.desired_transformation || '',
|
||||
objections: current.objections?.length ? current.objections : (argObj.objections || []),
|
||||
winning_hooks: current.winning_hooks?.length ? current.winning_hooks : (argObj.winning_hooks || []),
|
||||
preferred_angles: current.preferred_angles?.length ? current.preferred_angles : (argObj.preferred_angles || []),
|
||||
emoji_policy: current.emoji_policy || proObj.emoji_policy || '',
|
||||
testimonials_policy: current.testimonials_policy || proObj.testimonials_policy || '',
|
||||
competitors_never_mention: current.competitors_never_mention?.length ? current.competitors_never_mention : (proObj.competitors_never_mention || []),
|
||||
};
|
||||
}
|
||||
|
||||
const completeness = brandbookCompleteness(current);
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -91,8 +91,36 @@ export async function GET(req: NextRequest) {
|
||||
brandbook = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!brandbook) {
|
||||
brandbook = seedBrandbook(tenant.slug || 'cliente', tenant.name || 'Cliente');
|
||||
} else {
|
||||
if (brandbook.website) style.identity.website = brandbook.website;
|
||||
if (brandbook.b2b_offering) style.identity.b2b_offering = brandbook.b2b_offering;
|
||||
if (brandbook.b2c_offering) style.identity.b2c_offering = brandbook.b2c_offering;
|
||||
if (brandbook.usp_differentiators) style.identity.usp_differentiators = brandbook.usp_differentiators;
|
||||
if (brandbook.current_priorities) style.identity.current_priorities = brandbook.current_priorities;
|
||||
if (brandbook.price_and_offer_framing) style.identity.price_and_offer_framing = brandbook.price_and_offer_framing;
|
||||
if (brandbook.elevator_pitch) style.identity.summary = brandbook.elevator_pitch;
|
||||
if (brandbook.tone) style.voice.tone = brandbook.tone;
|
||||
if (brandbook.copy_structure_rules) style.voice.copy_structure_rules = brandbook.copy_structure_rules;
|
||||
if (brandbook.english_terms_policy) style.voice.english_terms_policy = brandbook.english_terms_policy;
|
||||
if (brandbook.power_words?.length) style.voice.power_words = brandbook.power_words;
|
||||
if (brandbook.allowed_ctas?.length) style.voice.allowed_ctas = brandbook.allowed_ctas;
|
||||
if (brandbook.surface_rules) style.visual.surface_rules = brandbook.surface_rules;
|
||||
if (brandbook.image_text_rules) style.visual.image_text_rules = brandbook.image_text_rules;
|
||||
if (brandbook.typography_headline) style.visual.typography_headline = brandbook.typography_headline;
|
||||
if (brandbook.typography_body) style.visual.typography_body = brandbook.typography_body;
|
||||
if (brandbook.b2b_personas?.length) style.audiences.b2b_personas = brandbook.b2b_personas as any;
|
||||
if (brandbook.b2c_personas?.length) style.audiences.b2c_personas = brandbook.b2c_personas as any;
|
||||
if (brandbook.customer_frustrations) style.audiences.customer_frustrations = brandbook.customer_frustrations;
|
||||
if (brandbook.desired_transformation) style.audiences.desired_transformation = brandbook.desired_transformation;
|
||||
if (brandbook.objections?.length) style.arguments.objections = brandbook.objections as any;
|
||||
if (brandbook.winning_hooks?.length) style.arguments.winning_hooks = brandbook.winning_hooks;
|
||||
if (brandbook.preferred_angles?.length) style.arguments.preferred_angles = brandbook.preferred_angles;
|
||||
if (brandbook.emoji_policy) style.prohibitions.emoji_policy = brandbook.emoji_policy;
|
||||
if (brandbook.testimonials_policy) style.prohibitions.testimonials_policy = brandbook.testimonials_policy;
|
||||
if (brandbook.competitors_never_mention?.length) style.prohibitions.competitors_never_mention = brandbook.competitors_never_mention;
|
||||
}
|
||||
|
||||
const compiledMarkdown = compileClientSkillFromBrandbook(
|
||||
@@ -241,10 +269,10 @@ export async function PUT(req: NextRequest) {
|
||||
return NextResponse.json({ error: updErr.message }, { status: 500 });
|
||||
}
|
||||
|
||||
// Cargar brandbook de documents para compilar skill actualizado y mantener sincronización
|
||||
// Cargar brandbook de documents para compilar skill actualizado
|
||||
const { data: brandDoc } = await admin
|
||||
.from('documents')
|
||||
.select('id, content')
|
||||
.select('content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', 'brandbook')
|
||||
.order('updated_at', { ascending: false })
|
||||
@@ -267,62 +295,61 @@ export async function PUT(req: NextRequest) {
|
||||
brandbook = seedBrandbook(existing.slug || 'cliente', existing.name || 'Cliente');
|
||||
}
|
||||
|
||||
// Sincronizar campos de normalizedStyle hacia brandbook
|
||||
if (normalizedStyle.identity) {
|
||||
if (normalizedStyle.identity.summary) brandbook.elevator_pitch = normalizedStyle.identity.summary;
|
||||
if (normalizedStyle.identity.website) brandbook.website = normalizedStyle.identity.website;
|
||||
if (normalizedStyle.identity.b2b_offering) brandbook.b2b_offering = normalizedStyle.identity.b2b_offering;
|
||||
if (normalizedStyle.identity.b2c_offering) brandbook.b2c_offering = normalizedStyle.identity.b2c_offering;
|
||||
if (normalizedStyle.identity.usp_differentiators) brandbook.usp_differentiators = normalizedStyle.identity.usp_differentiators;
|
||||
if (normalizedStyle.identity.current_priorities) brandbook.current_priorities = normalizedStyle.identity.current_priorities;
|
||||
if (normalizedStyle.identity.price_and_offer_framing) brandbook.price_and_offer_framing = normalizedStyle.identity.price_and_offer_framing;
|
||||
}
|
||||
if (normalizedStyle.voice) {
|
||||
if (normalizedStyle.voice.tone) brandbook.tone = normalizedStyle.voice.tone;
|
||||
if (normalizedStyle.voice.copy_structure_rules) brandbook.copy_structure_rules = normalizedStyle.voice.copy_structure_rules;
|
||||
if (normalizedStyle.voice.english_terms_policy) brandbook.english_terms_policy = normalizedStyle.voice.english_terms_policy;
|
||||
if (normalizedStyle.voice.power_words?.length) brandbook.power_words = normalizedStyle.voice.power_words;
|
||||
if (normalizedStyle.voice.allowed_ctas?.length) brandbook.allowed_ctas = normalizedStyle.voice.allowed_ctas;
|
||||
}
|
||||
if (normalizedStyle.visual) {
|
||||
if (normalizedStyle.visual.surface_rules) brandbook.surface_rules = normalizedStyle.visual.surface_rules;
|
||||
if (normalizedStyle.visual.image_text_rules) brandbook.image_text_rules = normalizedStyle.visual.image_text_rules;
|
||||
if (normalizedStyle.visual.typography_headline) brandbook.typography_headline = normalizedStyle.visual.typography_headline;
|
||||
if (normalizedStyle.visual.typography_body) brandbook.typography_body = normalizedStyle.visual.typography_body;
|
||||
}
|
||||
if (normalizedStyle.audiences) {
|
||||
if (normalizedStyle.audiences.b2b_personas?.length) brandbook.b2b_personas = normalizedStyle.audiences.b2b_personas;
|
||||
if (normalizedStyle.audiences.b2c_personas?.length) brandbook.b2c_personas = normalizedStyle.audiences.b2c_personas;
|
||||
if (normalizedStyle.audiences.customer_frustrations) brandbook.customer_frustrations = normalizedStyle.audiences.customer_frustrations;
|
||||
if (normalizedStyle.audiences.desired_transformation) brandbook.desired_transformation = normalizedStyle.audiences.desired_transformation;
|
||||
}
|
||||
if (normalizedStyle.arguments) {
|
||||
if (normalizedStyle.arguments.objections?.length) brandbook.objections = normalizedStyle.arguments.objections;
|
||||
if (normalizedStyle.arguments.winning_hooks?.length) brandbook.winning_hooks = normalizedStyle.arguments.winning_hooks;
|
||||
if (normalizedStyle.arguments.preferred_angles?.length) brandbook.preferred_angles = normalizedStyle.arguments.preferred_angles;
|
||||
}
|
||||
if (normalizedStyle.prohibitions) {
|
||||
if (normalizedStyle.prohibitions.emoji_policy) brandbook.emoji_policy = normalizedStyle.prohibitions.emoji_policy;
|
||||
if (normalizedStyle.prohibitions.testimonials_policy) brandbook.testimonials_policy = normalizedStyle.prohibitions.testimonials_policy;
|
||||
if (normalizedStyle.prohibitions.competitors_never_mention?.length) brandbook.competitors_never_mention = normalizedStyle.prohibitions.competitors_never_mention;
|
||||
}
|
||||
if (normalizedStyle.image_ai?.photo_style) {
|
||||
brandbook.photography_style = normalizedStyle.image_ai.photo_style;
|
||||
}
|
||||
brandbook.updated_at = new Date().toISOString();
|
||||
// Sincronizar simultáneamente con documents (brandbook) para que Zona de Marca esté siempre al día
|
||||
const updatedBrandbook: Brandbook = {
|
||||
...brandbook,
|
||||
elevator_pitch: normalizedStyle.identity.summary || brandbook.elevator_pitch,
|
||||
website: normalizedStyle.identity.website || brandbook.website,
|
||||
b2b_offering: normalizedStyle.identity.b2b_offering || brandbook.b2b_offering,
|
||||
b2c_offering: normalizedStyle.identity.b2c_offering || brandbook.b2c_offering,
|
||||
usp_differentiators: normalizedStyle.identity.usp_differentiators || brandbook.usp_differentiators,
|
||||
current_priorities: normalizedStyle.identity.current_priorities || brandbook.current_priorities,
|
||||
price_and_offer_framing: normalizedStyle.identity.price_and_offer_framing || brandbook.price_and_offer_framing,
|
||||
tone: normalizedStyle.voice.tone || brandbook.tone,
|
||||
copy_structure_rules: normalizedStyle.voice.copy_structure_rules || brandbook.copy_structure_rules,
|
||||
english_terms_policy: normalizedStyle.voice.english_terms_policy || brandbook.english_terms_policy,
|
||||
power_words: normalizedStyle.voice.power_words?.length ? normalizedStyle.voice.power_words : brandbook.power_words,
|
||||
allowed_ctas: normalizedStyle.voice.allowed_ctas?.length ? normalizedStyle.voice.allowed_ctas : brandbook.allowed_ctas,
|
||||
surface_rules: normalizedStyle.visual.surface_rules || brandbook.surface_rules,
|
||||
image_text_rules: normalizedStyle.visual.image_text_rules || brandbook.image_text_rules,
|
||||
typography_headline: normalizedStyle.visual.typography_headline || brandbook.typography_headline,
|
||||
typography_body: normalizedStyle.visual.typography_body || brandbook.typography_body,
|
||||
b2b_personas: normalizedStyle.audiences.b2b_personas?.length ? (normalizedStyle.audiences.b2b_personas as any) : brandbook.b2b_personas,
|
||||
b2c_personas: normalizedStyle.audiences.b2c_personas?.length ? (normalizedStyle.audiences.b2c_personas as any) : brandbook.b2c_personas,
|
||||
customer_frustrations: normalizedStyle.audiences.customer_frustrations || brandbook.customer_frustrations,
|
||||
desired_transformation: normalizedStyle.audiences.desired_transformation || brandbook.desired_transformation,
|
||||
objections: normalizedStyle.arguments.objections?.length ? (normalizedStyle.arguments.objections as any) : brandbook.objections,
|
||||
winning_hooks: normalizedStyle.arguments.winning_hooks?.length ? normalizedStyle.arguments.winning_hooks : brandbook.winning_hooks,
|
||||
preferred_angles: normalizedStyle.arguments.preferred_angles?.length ? normalizedStyle.arguments.preferred_angles : brandbook.preferred_angles,
|
||||
emoji_policy: normalizedStyle.prohibitions.emoji_policy || brandbook.emoji_policy,
|
||||
testimonials_policy: normalizedStyle.prohibitions.testimonials_policy || brandbook.testimonials_policy,
|
||||
competitors_never_mention: normalizedStyle.prohibitions.competitors_never_mention?.length ? normalizedStyle.prohibitions.competitors_never_mention : brandbook.competitors_never_mention,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (brandDoc?.id) {
|
||||
if (brandDoc) {
|
||||
await admin
|
||||
.from('documents')
|
||||
.update({
|
||||
content: brandbook,
|
||||
updated_at: brandbook.updated_at,
|
||||
content: updatedBrandbook,
|
||||
updated_at: updatedBrandbook.updated_at,
|
||||
})
|
||||
.eq('id', brandDoc.id);
|
||||
.eq('id', (brandDoc as any).id);
|
||||
} else {
|
||||
await admin
|
||||
.from('documents')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
type: 'brandbook',
|
||||
title: 'Brandbook',
|
||||
status: 'approved',
|
||||
content: updatedBrandbook,
|
||||
updated_at: updatedBrandbook.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
const compiledMarkdown = compileClientSkillFromBrandbook(
|
||||
brandbook,
|
||||
updatedBrandbook,
|
||||
{
|
||||
image_ai: normalizedStyle.image_ai,
|
||||
campaign_rules: normalizedStyle.campaign_rules,
|
||||
|
||||
@@ -170,7 +170,7 @@ export default function BrandZone({
|
||||
setScore(j.completeness?.score ?? 0);
|
||||
setMissing(j.completeness?.missing || []);
|
||||
setExists(true);
|
||||
setOkMsg('Brand kit guardado correctamente y sincronizado');
|
||||
setOkMsg('Brand kit guardado y sincronizado');
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : 'Error al guardar');
|
||||
} finally {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
ExternalLink,
|
||||
Building2,
|
||||
Users,
|
||||
MessageSquare,
|
||||
@@ -262,30 +261,6 @@ export default function ClientCreativeStylePanel({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* BANNER DE SINCRONIZACIÓN CON ZONA DE MARCA */}
|
||||
<div className="rounded-xl border border-brand-200 bg-brand-50/70 p-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 shadow-xs">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-brand-600 p-2 text-white shrink-0 mt-0.5">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-gray-900 flex items-center gap-2">
|
||||
Estilo Creativo & ADN Centralizado
|
||||
<span className="rounded-full bg-emerald-100 text-emerald-800 text-[10px] font-semibold px-2 py-0.5">Sincronizado</span>
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
Todo lo que configures aquí (Identidad, Audiencias, Voz, Argumentos, Visual y Operativa) se sincroniza automáticamente con la <strong>Zona de Marca</strong> y alimenta a Claude en tiempo real.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/dashboard/marca"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-brand-600 px-3.5 py-2 text-xs font-semibold text-white hover:bg-brand-700 shrink-0 shadow-xs transition-colors"
|
||||
>
|
||||
Ir a Zona de Marca
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
{/* Barra superior de estado y guardado */}
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between rounded-2xl border border-brand-200 bg-brand-50/50 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -297,7 +272,7 @@ export default function ClientCreativeStylePanel({
|
||||
Estilo Creativo & Brief Maestro · {clientName}
|
||||
</h2>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
Todo lo que configures aquí alimenta en tiempo real a <strong>Claude (WF3)</strong> para el copy y estrategia, a <strong>Freepik Mystic</strong> para la escena fotográfica y a <strong>Canva (WF5)</strong> para el montaje automático.
|
||||
Todo lo que configures aquí alimenta en tiempo real a <strong>Claude (WF3)</strong> para el copy y estrategia, a <strong>Freepik Mystic</strong> para la escena fotográfica y a <strong>Canva (WF5)</strong> para el montaje automático. Sincronizado con la <a href="/dashboard/marca" className="font-semibold text-brand-700 underline">Zona de Marca</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -983,8 +983,19 @@ export function seedBrandbook(slug: string, label: string): Brandbook {
|
||||
}
|
||||
|
||||
export function parseBrandbook(raw: unknown): Brandbook | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const o = raw as Record<string, unknown>;
|
||||
if (!raw) return null;
|
||||
let o: any = raw;
|
||||
if (typeof raw === 'string') {
|
||||
try {
|
||||
o = JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!o || typeof o !== 'object') return null;
|
||||
if (o.content && (typeof o.content === 'object' || typeof o.content === 'string')) {
|
||||
return parseBrandbook(o.content);
|
||||
}
|
||||
const ver = Number(o.version);
|
||||
if (
|
||||
ver !== 1 &&
|
||||
@@ -994,11 +1005,13 @@ export function parseBrandbook(raw: unknown): Brandbook | null {
|
||||
!('tone' in o) &&
|
||||
!('pillars_rrss' in o) &&
|
||||
!('mission' in o) &&
|
||||
!('kit_assets' in o)
|
||||
!('kit_assets' in o) &&
|
||||
!('b2b_offering' in o) &&
|
||||
!('identity' in o)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const partial = raw as Partial<Brandbook>;
|
||||
const partial = o as Partial<Brandbook>;
|
||||
const empty = emptyBrandbook(String(partial.brand_name || ''));
|
||||
const colors = migrateColors(partial, empty);
|
||||
const fonts_kit = migrateFonts(partial, empty);
|
||||
|
||||
Reference in New Issue
Block a user