feat(3d-viewer): enriquecer identificacao de pecas WRL com marca principal e perfil oficial
This commit is contained in:
@@ -18,6 +18,8 @@ interface ProductionPieceStatus {
|
||||
currentProcessName?: string;
|
||||
processColor?: string;
|
||||
processOrdem?: number;
|
||||
perfilPrincipal?: string;
|
||||
descricao?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +69,42 @@ function findProductionDataForPiece(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para formatar a Marca Principal da Peça no padrão de Engenharia (OF-Fase-Marca, ex: B138-2-1 / B135-2-8)
|
||||
* e obter o Perfil/Bitola oficial a partir do banco de dados ou do modelo 3D.
|
||||
*/
|
||||
function resolvePieceEngineeringDetails(
|
||||
rawPieceMark: string,
|
||||
rawSection: string | undefined,
|
||||
phase: string | undefined,
|
||||
singlePartMark: string | undefined,
|
||||
prod: ProductionPieceStatus | undefined,
|
||||
selectedOF: string | undefined
|
||||
): {
|
||||
displayMark: string;
|
||||
displaySection: string;
|
||||
} {
|
||||
const baseMark = prod?.marca || rawPieceMark || 'Peça';
|
||||
let displayMark = baseMark;
|
||||
|
||||
// Se tivermos OF e Fase e a marca for pura (ex: "1" ou "8"), formata como "B138-2-1" idêntico ao Print 2 "B135-2-8"
|
||||
if (selectedOF && phase && !baseMark.toUpperCase().startsWith(selectedOF.toUpperCase())) {
|
||||
displayMark = `${selectedOF}-${phase}-${baseMark}`;
|
||||
}
|
||||
|
||||
// Perfil / Bitola oficial: prioriza banco de produção (TS_ERP.pecas.perfil_principal), seguido de section limpo do modelo
|
||||
let displaySection = prod?.perfilPrincipal || '';
|
||||
if (!displaySection || displaySection === '1001' || displaySection === 'Perfil') {
|
||||
if (rawSection && rawSection !== '1001' && rawSection !== 'Perfil') {
|
||||
displaySection = rawSection;
|
||||
} else {
|
||||
displaySection = prod?.descricao || 'Perfil';
|
||||
}
|
||||
}
|
||||
|
||||
return { displayMark, displaySection };
|
||||
}
|
||||
|
||||
interface ModelViewer3DProps {
|
||||
modelData: LoadedIFCResult | null;
|
||||
productionData?: Map<string, ProductionPieceStatus>;
|
||||
@@ -940,17 +978,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const pmark = hitMesh.userData.pieceMark;
|
||||
|
||||
if (modelData && expressID !== undefined) {
|
||||
const piece = modelData.pieceByExpressID.get(expressID) || {
|
||||
const rawPiece = modelData.pieceByExpressID.get(expressID);
|
||||
const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || '';
|
||||
const phase = hitMesh.userData.phase || rawPiece?.phase;
|
||||
const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark;
|
||||
|
||||
const prod = findProductionDataForPiece(productionData, {
|
||||
pieceMark: rawMark,
|
||||
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
|
||||
phase,
|
||||
}, selectedOF);
|
||||
|
||||
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
|
||||
rawMark,
|
||||
hitMesh.userData.section || rawPiece?.section,
|
||||
phase,
|
||||
singlePartMark,
|
||||
prod,
|
||||
selectedOF
|
||||
);
|
||||
|
||||
const piece: PieceInfo = {
|
||||
expressID,
|
||||
guid: hitMesh.userData.guid || '',
|
||||
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça',
|
||||
type: hitMesh.userData.type || 'PIECE',
|
||||
pieceMark: pmark,
|
||||
cleanMark: hitMesh.userData.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark,
|
||||
section: hitMesh.userData.section || hitMesh.userData.name,
|
||||
phase: hitMesh.userData.phase,
|
||||
guid: hitMesh.userData.guid || rawPiece?.guid || '',
|
||||
name: hitMesh.userData.name || rawPiece?.name || 'Peça',
|
||||
type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
|
||||
pieceMark: displayMark,
|
||||
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
|
||||
section: displaySection,
|
||||
phase,
|
||||
singlePartMark,
|
||||
};
|
||||
setHoveredPiece(piece);
|
||||
setHoverPosition({ x: clientX, y: clientY });
|
||||
@@ -1040,18 +1101,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
const pmark = hitMesh.userData.pieceMark;
|
||||
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || {
|
||||
const rawPiece = modelData?.pieceByExpressID.get(targetIfcId);
|
||||
const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || '';
|
||||
const phase = hitMesh.userData.phase || rawPiece?.phase;
|
||||
const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark;
|
||||
|
||||
const prod = findProductionDataForPiece(productionData, {
|
||||
pieceMark: rawMark,
|
||||
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
|
||||
phase,
|
||||
}, selectedOF);
|
||||
|
||||
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
|
||||
rawMark,
|
||||
hitMesh.userData.section || rawPiece?.section,
|
||||
phase,
|
||||
singlePartMark,
|
||||
prod,
|
||||
selectedOF
|
||||
);
|
||||
|
||||
const piece: PieceInfo = {
|
||||
expressID: targetIfcId,
|
||||
guid: hitMesh.userData.guid || '',
|
||||
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça',
|
||||
type: hitMesh.userData.type || 'PIECE',
|
||||
pieceMark: pmark,
|
||||
cleanMark: hitMesh.userData.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark,
|
||||
section: hitMesh.userData.section,
|
||||
phase: hitMesh.userData.phase,
|
||||
guid: hitMesh.userData.guid || rawPiece?.guid || '',
|
||||
name: hitMesh.userData.name || rawPiece?.name || 'Peça',
|
||||
type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
|
||||
pieceMark: displayMark,
|
||||
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
|
||||
section: displaySection,
|
||||
phase,
|
||||
singlePartMark,
|
||||
};
|
||||
|
||||
nextMap.set(targetIfcId, {
|
||||
@@ -1165,17 +1248,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
m.material = magentaMaterialRef.current;
|
||||
});
|
||||
|
||||
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(elId)) || {
|
||||
const rawPiece = modelData?.pieceByExpressID.get(elId);
|
||||
const rawMark = sampleMesh.userData.pieceMark || rawPiece?.pieceMark || '';
|
||||
const phase = sampleMesh.userData.phase || rawPiece?.phase || targetPhase;
|
||||
const singlePartMark = sampleMesh.userData.singlePartMark || rawPiece?.singlePartMark;
|
||||
|
||||
const prod = findProductionDataForPiece(productionData, {
|
||||
pieceMark: rawMark,
|
||||
cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark,
|
||||
assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark,
|
||||
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
|
||||
phase,
|
||||
}, selectedOF);
|
||||
|
||||
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
|
||||
rawMark,
|
||||
sampleMesh.userData.section || rawPiece?.section,
|
||||
phase,
|
||||
singlePartMark,
|
||||
prod,
|
||||
selectedOF
|
||||
);
|
||||
|
||||
const piece: PieceInfo = {
|
||||
expressID: elId,
|
||||
guid: sampleMesh.userData.guid || '',
|
||||
name: sampleMesh.userData.name || sampleMesh.userData.section || 'Peça',
|
||||
type: sampleMesh.userData.type || 'PIECE',
|
||||
pieceMark: sampleMesh.userData.pieceMark,
|
||||
cleanMark: sampleMesh.userData.cleanMark,
|
||||
assemblyMark: sampleMesh.userData.assemblyMark,
|
||||
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark,
|
||||
section: sampleMesh.userData.section,
|
||||
phase: sampleMesh.userData.phase || targetPhase,
|
||||
guid: sampleMesh.userData.guid || rawPiece?.guid || '',
|
||||
name: sampleMesh.userData.name || rawPiece?.name || 'Peça',
|
||||
type: sampleMesh.userData.type || rawPiece?.type || 'PIECE',
|
||||
pieceMark: displayMark,
|
||||
cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark,
|
||||
assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark,
|
||||
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
|
||||
section: displaySection,
|
||||
phase,
|
||||
singlePartMark,
|
||||
};
|
||||
|
||||
nextMap.set(elId, {
|
||||
|
||||
@@ -62,6 +62,15 @@ export const PieceHoverTooltip: React.FC<PieceHoverTooltipProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{piece.singlePartMark && piece.singlePartMark !== piece.pieceMark && (
|
||||
<div className="flex items-center justify-between text-slate-300">
|
||||
<span className="text-slate-400">Sub-peça / Corte:</span>
|
||||
<span className="font-semibold text-slate-200 font-mono bg-slate-950/60 px-2 py-0.5 rounded border border-slate-800">
|
||||
Pos. {piece.singlePartMark}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{piece.childrenIDs && piece.childrenIDs.length > 0 && (
|
||||
<div className="flex items-center justify-between text-slate-300">
|
||||
<span className="text-slate-400">Componentes:</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import { VRMLLoader } from 'three-stdlib';
|
||||
import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark } from '@/lib/ifc/ifcLoaderService';
|
||||
import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark, isSinglePartCutNumber } from '@/lib/ifc/ifcLoaderService';
|
||||
|
||||
/**
|
||||
* Função utilitária para descompactar arquivos gzip (como .wrz ou .wrl comprimidos) no navegador.
|
||||
@@ -57,6 +57,9 @@ function extractPhaseFromAncestors(ancestors: string[]): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad.
|
||||
*/
|
||||
/**
|
||||
* Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad.
|
||||
*/
|
||||
@@ -65,10 +68,15 @@ function parseWrlNodeName(
|
||||
inheritedPhase?: string,
|
||||
ancestorAssembly?: string
|
||||
): {
|
||||
mainMark: string;
|
||||
cleanMainMark: string;
|
||||
pieceMark: string;
|
||||
cleanMark: string;
|
||||
assemblyMark?: string;
|
||||
cleanAssemblyMark?: string;
|
||||
singlePartMark?: string;
|
||||
profile?: string;
|
||||
section?: string;
|
||||
phase?: string;
|
||||
detectedType: string;
|
||||
} {
|
||||
@@ -79,20 +87,22 @@ function parseWrlNodeName(
|
||||
|
||||
let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined;
|
||||
let pieceMark = name;
|
||||
let singlePartMark: string | undefined = undefined;
|
||||
let profile = '';
|
||||
|
||||
const parts = name.split('_');
|
||||
|
||||
// CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "121_1127_TUBO44x45X3_121293")
|
||||
// CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "1_1001_W360X44_0", "121_1127_TUBO44x45X3_121293")
|
||||
if (parts.length >= 3 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
|
||||
assemblyMark = parts[0];
|
||||
pieceMark = parts[1];
|
||||
profile = parts[2];
|
||||
singlePartMark = parts[1];
|
||||
let rest = parts.slice(2).join('_');
|
||||
rest = rest.replace(/_\d{5,}$/, '');
|
||||
profile = rest;
|
||||
}
|
||||
// CASO 2: Tekla padrão [MarcaPeca]_[Perfil] (ex: "5_W360X44_0", "6_W360X32_9", "1_W360X44_0", "6_VIGA")
|
||||
else if (parts.length >= 2 && /^\d+$/.test(parts[0])) {
|
||||
// Se o primeiro segmento é numérico e o segundo não é outro número (é perfil ou descrição),
|
||||
// a marca real da peça é o primeiro segmento (ex: "5", "6", "1")!
|
||||
pieceMark = parts[0];
|
||||
assemblyMark = parts[0];
|
||||
profile = parts.slice(1).join('_');
|
||||
@@ -100,7 +110,7 @@ function parseWrlNodeName(
|
||||
// CASO 3: Prefixo de conjunto alfanumérico (ex: "V101_1_CHAPA", "P1_2_CANTON")
|
||||
else if (parts.length >= 2 && /^[A-Za-z]+\d+$/i.test(parts[0])) {
|
||||
assemblyMark = parts[0];
|
||||
pieceMark = parts[0]; // ou parts[1]
|
||||
pieceMark = parts[0];
|
||||
profile = parts.slice(1).join('_');
|
||||
}
|
||||
// CASO 4: Parafusos / Fixadores
|
||||
@@ -113,18 +123,32 @@ function parseWrlNodeName(
|
||||
pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, '');
|
||||
}
|
||||
|
||||
// Normalização do Perfil / Bitola (ex: W360X44_0 -> W360X44.0, W360X32_9 -> W360X32.9)
|
||||
if (profile) {
|
||||
profile = profile.replace(/_(\d+)$/, '.$1').replace(/_/g, ' ').trim();
|
||||
}
|
||||
|
||||
let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase();
|
||||
if (!cleanMark) cleanMark = pieceMark.toUpperCase();
|
||||
|
||||
const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark;
|
||||
|
||||
// Em estruturas metálicas, a marca da peça apontada na produção é a marca do conjunto (Assembly Mark).
|
||||
// Se a sub-peça for um número de corte (>= 1000) e houver conjunto pai, a marca principal é o conjunto!
|
||||
let mainMark = pieceMark;
|
||||
let cleanMainMark = cleanMark;
|
||||
if (assemblyMark && (isSinglePartCutNumber(pieceMark) || !mainMark || mainMark === 'indefinido')) {
|
||||
mainMark = assemblyMark;
|
||||
cleanMainMark = cleanAssemblyMark;
|
||||
}
|
||||
|
||||
// Detecção de tipo de elemento estrutural
|
||||
let detectedType = 'ELEMENT';
|
||||
const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark}`.toUpperCase();
|
||||
const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark} ${mainMark}`.toUpperCase();
|
||||
|
||||
if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMark.startsWith('V')) {
|
||||
if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMainMark.startsWith('V')) {
|
||||
detectedType = 'BEAM';
|
||||
} else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMark.startsWith('P')) {
|
||||
} else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMainMark.startsWith('P')) {
|
||||
detectedType = 'COLUMN';
|
||||
} else if (searchStr.includes('CHAPA') || searchStr.includes('PLATE') || cleanMark.startsWith('PL') || cleanMark.startsWith('CH')) {
|
||||
detectedType = 'PLATE';
|
||||
@@ -145,10 +169,15 @@ function parseWrlNodeName(
|
||||
}
|
||||
|
||||
return {
|
||||
mainMark: mainMark.toUpperCase(),
|
||||
cleanMainMark: cleanMainMark.toUpperCase(),
|
||||
pieceMark: pieceMark.toUpperCase(),
|
||||
cleanMark,
|
||||
assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined,
|
||||
cleanAssemblyMark,
|
||||
singlePartMark,
|
||||
profile: profile || undefined,
|
||||
section: profile || undefined,
|
||||
phase: inheritedPhase,
|
||||
detectedType,
|
||||
};
|
||||
@@ -503,19 +532,22 @@ export async function loadAndAuditWRL(
|
||||
}
|
||||
mesh.material = material;
|
||||
|
||||
const pieceSection = parsed.cleanMark || parsed.pieceMark || 'Perfil';
|
||||
const pieceSection = parsed.profile || parsed.section || parsed.cleanMainMark || 'Perfil';
|
||||
const mainMark = parsed.mainMark || parsed.cleanMainMark || parsed.pieceMark;
|
||||
const cleanMain = parsed.cleanMainMark || parsed.cleanMark;
|
||||
|
||||
const pieceInfo: PieceInfo = {
|
||||
expressID,
|
||||
guid,
|
||||
name: rawNodeName,
|
||||
type: parsed.detectedType,
|
||||
pieceMark: parsed.pieceMark,
|
||||
cleanMark: parsed.cleanMark,
|
||||
assemblyMark: parsed.assemblyMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark,
|
||||
pieceMark: mainMark,
|
||||
cleanMark: cleanMain,
|
||||
assemblyMark: parsed.assemblyMark || mainMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain,
|
||||
section: pieceSection,
|
||||
phase: parsed.phase,
|
||||
singlePartMark: parsed.singlePartMark,
|
||||
mesh,
|
||||
};
|
||||
|
||||
@@ -524,10 +556,11 @@ export async function loadAndAuditWRL(
|
||||
ifcId: expressID, // Identificador primordial compatível com Raycaster, Hover e Seleção do visualizador
|
||||
expressID,
|
||||
guid,
|
||||
pieceMark: parsed.pieceMark,
|
||||
cleanMark: parsed.cleanMark,
|
||||
assemblyMark: parsed.assemblyMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark,
|
||||
pieceMark: mainMark, // Peça principal da produção (ex: 1, 8, V101)
|
||||
cleanMark: cleanMain,
|
||||
singlePartMark: parsed.singlePartMark, // Componente de corte (ex: 1001)
|
||||
assemblyMark: parsed.assemblyMark || mainMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain,
|
||||
section: pieceSection,
|
||||
phase: parsed.phase,
|
||||
type: parsed.detectedType,
|
||||
@@ -538,17 +571,12 @@ export async function loadAndAuditWRL(
|
||||
|
||||
pieceByExpressID.set(expressID, pieceInfo);
|
||||
|
||||
// Agrupa peças por marca
|
||||
const markKey = parsed.cleanMark || parsed.pieceMark;
|
||||
if (markKey) {
|
||||
const list = piecesByMark.get(markKey) || [];
|
||||
// Agrupa peças por marca de conjunto e por marca de componente
|
||||
const markKeys = [mainMark, cleanMain, parsed.singlePartMark, parsed.pieceMark, parsed.cleanMark].filter(Boolean) as string[];
|
||||
for (const mKey of Array.from(new Set(markKeys))) {
|
||||
const list = piecesByMark.get(mKey) || [];
|
||||
list.push(pieceInfo);
|
||||
piecesByMark.set(markKey, list);
|
||||
}
|
||||
if (parsed.cleanAssemblyMark && parsed.cleanAssemblyMark !== markKey) {
|
||||
const listAss = piecesByMark.get(parsed.cleanAssemblyMark) || [];
|
||||
listAss.push(pieceInfo);
|
||||
piecesByMark.set(parsed.cleanAssemblyMark, listAss);
|
||||
piecesByMark.set(mKey, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface PieceInfo {
|
||||
parentAssemblyID?: number;
|
||||
phase?: string;
|
||||
ofNumber?: string;
|
||||
singlePartMark?: string;
|
||||
section?: string;
|
||||
span?: number;
|
||||
material?: string;
|
||||
|
||||
@@ -148,6 +148,8 @@ export default function Visualizador3D() {
|
||||
marca: marcaStr,
|
||||
fase,
|
||||
ofNumber: selectedOF,
|
||||
descricao: p.descricao || '',
|
||||
perfilPrincipal: p.perfil_principal || p.descricao || '',
|
||||
totalQtd: Number(p.quantidade || 1),
|
||||
pesoTotal: Number(p.peso_total || 0),
|
||||
pesoUnitario: Number(p.peso_unitario || 0),
|
||||
|
||||
Reference in New Issue
Block a user