feat: [corporate-card] 카드분리 기능 추가
- 결제 내역 수정 모달에 카드분리 버튼 추가 - 카드별 배분금액 직접 입력 UI - 균등 배분 / 비율 배분 / 해제 버튼 - 배분 합계 검증 (일치해야 저장 가능) - card_splits 데이터 JSON 저장 (기존 items 확장) - cardDeductions 로직: card_splits 우선 적용, 없으면 기존 비율 배분
This commit is contained in:
@@ -278,6 +278,8 @@ public function updatePrepayment(Request $request): JsonResponse
|
|||||||
'items.*.date' => 'required|date',
|
'items.*.date' => 'required|date',
|
||||||
'items.*.amount' => 'required|integer|min:0',
|
'items.*.amount' => 'required|integer|min:0',
|
||||||
'items.*.description' => 'nullable|string|max:200',
|
'items.*.description' => 'nullable|string|max:200',
|
||||||
|
'items.*.card_splits' => 'nullable|array',
|
||||||
|
'items.*.card_splits.*' => 'integer|min:0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenantId = session('selected_tenant_id', 1);
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
|||||||
@@ -117,12 +117,16 @@ function CorporateCardsManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 카드분리 열림 상태 (항목 인덱스 또는 null)
|
||||||
|
const [splitOpenIndex, setSplitOpenIndex] = useState(null);
|
||||||
|
|
||||||
// 결제 항목 추가
|
// 결제 항목 추가
|
||||||
const addPrepaymentItem = () => {
|
const addPrepaymentItem = () => {
|
||||||
setPrepaymentItems(prev => [...prev, {
|
setPrepaymentItems(prev => [...prev, {
|
||||||
date: new Date().toISOString().slice(0, 10),
|
date: new Date().toISOString().slice(0, 10),
|
||||||
amount: '',
|
amount: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
card_splits: null,
|
||||||
}]);
|
}]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,8 +143,92 @@ function CorporateCardsManagement() {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 카드분리 - 배분금액 수정
|
||||||
|
const updateCardSplit = (itemIndex, cardNum, value) => {
|
||||||
|
setPrepaymentItems(prev => prev.map((item, i) => {
|
||||||
|
if (i !== itemIndex) return item;
|
||||||
|
const splits = { ...(item.card_splits || {}) };
|
||||||
|
const parsed = parseInt(parseInputCurrency(value)) || 0;
|
||||||
|
splits[cardNum] = parsed;
|
||||||
|
return { ...item, card_splits: splits };
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카드분리 - 균등 배분
|
||||||
|
const distributeEvenly = (itemIndex) => {
|
||||||
|
const item = prepaymentItems[itemIndex];
|
||||||
|
const total = parseInt(parseInputCurrency(item.amount)) || 0;
|
||||||
|
if (total <= 0) return;
|
||||||
|
const activeList = cards.filter(c => c.status === 'active');
|
||||||
|
if (activeList.length === 0) return;
|
||||||
|
const base = Math.floor(total / activeList.length);
|
||||||
|
const remainder = total - base * activeList.length;
|
||||||
|
const splits = {};
|
||||||
|
activeList.forEach((c, idx) => {
|
||||||
|
const key = c.cardNumber.replace(/-/g, '');
|
||||||
|
splits[key] = base + (idx < remainder ? 1 : 0);
|
||||||
|
});
|
||||||
|
setPrepaymentItems(prev => prev.map((it, i) => i === itemIndex ? { ...it, card_splits: splits } : it));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카드분리 - 사용비율 배분
|
||||||
|
const distributeByRatio = (itemIndex) => {
|
||||||
|
const item = prepaymentItems[itemIndex];
|
||||||
|
const total = parseInt(parseInputCurrency(item.amount)) || 0;
|
||||||
|
if (total <= 0) return;
|
||||||
|
const activeList = cards.filter(c => c.status === 'active').map(c => ({
|
||||||
|
key: c.cardNumber.replace(/-/g, ''),
|
||||||
|
raw: getRawCardUsage(c.cardNumber),
|
||||||
|
})).filter(c => c.raw > 0);
|
||||||
|
if (activeList.length === 0) return distributeEvenly(itemIndex);
|
||||||
|
const totalRaw = activeList.reduce((s, c) => s + c.raw, 0);
|
||||||
|
if (totalRaw <= 0) return distributeEvenly(itemIndex);
|
||||||
|
const splits = {};
|
||||||
|
let distributed = 0;
|
||||||
|
activeList.forEach((c, idx) => {
|
||||||
|
if (idx === activeList.length - 1) {
|
||||||
|
splits[c.key] = total - distributed;
|
||||||
|
} else {
|
||||||
|
const amt = Math.round(total * (c.raw / totalRaw));
|
||||||
|
splits[c.key] = amt;
|
||||||
|
distributed += amt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 사용금액 0인 카드는 0으로
|
||||||
|
cards.filter(c => c.status === 'active').forEach(c => {
|
||||||
|
const key = c.cardNumber.replace(/-/g, '');
|
||||||
|
if (!(key in splits)) splits[key] = 0;
|
||||||
|
});
|
||||||
|
setPrepaymentItems(prev => prev.map((it, i) => i === itemIndex ? { ...it, card_splits: splits } : it));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카드분리 - 초기화
|
||||||
|
const resetCardSplit = (itemIndex) => {
|
||||||
|
setPrepaymentItems(prev => prev.map((it, i) => i === itemIndex ? { ...it, card_splits: null } : it));
|
||||||
|
setSplitOpenIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카드분리 합계
|
||||||
|
const getCardSplitTotal = (splits) => {
|
||||||
|
if (!splits) return 0;
|
||||||
|
return Object.values(splits).reduce((s, v) => s + (v || 0), 0);
|
||||||
|
};
|
||||||
|
|
||||||
// 결제 내역 저장
|
// 결제 내역 저장
|
||||||
const handleSavePrepayment = async () => {
|
const handleSavePrepayment = async () => {
|
||||||
|
// 카드분리 검증
|
||||||
|
for (let i = 0; i < prepaymentItems.length; i++) {
|
||||||
|
const item = prepaymentItems[i];
|
||||||
|
const itemAmount = parseInt(parseInputCurrency(item.amount)) || 0;
|
||||||
|
if (item.card_splits && itemAmount > 0) {
|
||||||
|
const splitTotal = getCardSplitTotal(item.card_splits);
|
||||||
|
if (splitTotal !== itemAmount) {
|
||||||
|
alert(`항목 ${i + 1}의 카드분리 배분 합계(${formatCurrency(splitTotal)}원)가 결제 금액(${formatCurrency(itemAmount)}원)과 일치하지 않습니다.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const items = prepaymentItems
|
const items = prepaymentItems
|
||||||
.filter(item => item.amount && parseInt(parseInputCurrency(item.amount)) > 0)
|
.filter(item => item.amount && parseInt(parseInputCurrency(item.amount)) > 0)
|
||||||
@@ -148,6 +236,7 @@ function CorporateCardsManagement() {
|
|||||||
date: item.date,
|
date: item.date,
|
||||||
amount: parseInt(parseInputCurrency(item.amount)),
|
amount: parseInt(parseInputCurrency(item.amount)),
|
||||||
description: item.description,
|
description: item.description,
|
||||||
|
card_splits: item.card_splits || null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await fetch('/finance/corporate-cards/prepayment', {
|
const response = await fetch('/finance/corporate-cards/prepayment', {
|
||||||
@@ -183,14 +272,17 @@ function CorporateCardsManagement() {
|
|||||||
date: item.date,
|
date: item.date,
|
||||||
amount: String(item.amount),
|
amount: String(item.amount),
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
|
card_splits: item.card_splits || null,
|
||||||
})));
|
})));
|
||||||
} else {
|
} else {
|
||||||
setPrepaymentItems([{
|
setPrepaymentItems([{
|
||||||
date: new Date().toISOString().slice(0, 10),
|
date: new Date().toISOString().slice(0, 10),
|
||||||
amount: '',
|
amount: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
card_splits: null,
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
setSplitOpenIndex(null);
|
||||||
setShowPrepaymentModal(true);
|
setShowPrepaymentModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -403,58 +495,82 @@ function CorporateCardsManagement() {
|
|||||||
return summaryData.cardUsages[normalized] || 0;
|
return summaryData.cardUsages[normalized] || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 선결제 스마트 배분: 1단계 한도초과 우선 차감 → 2단계 잔여 비율 배분
|
// 선결제 카드별 차감액 계산: card_splits 우선, 없으면 비율 배분
|
||||||
const cardDeductions = (() => {
|
const cardDeductions = (() => {
|
||||||
const totalRaw = summaryData?.billingUsage || 0;
|
const totalRaw = summaryData?.billingUsage || 0;
|
||||||
const prepaid = summaryData?.prepaidAmount || 0;
|
const prepaid = summaryData?.prepaidAmount || 0;
|
||||||
if (totalRaw <= 0 || prepaid <= 0) return {};
|
if (totalRaw <= 0 || prepaid <= 0) return {};
|
||||||
|
|
||||||
const activeCards = cards
|
const prepaidItems = summaryData?.prepaidItems || [];
|
||||||
.filter(c => c.status === 'active')
|
|
||||||
.map(c => ({
|
|
||||||
key: c.cardNumber.replace(/-/g, ''),
|
|
||||||
raw: getRawCardUsage(c.cardNumber),
|
|
||||||
limit: c.cardType === 'credit' ? c.creditLimit : 0,
|
|
||||||
}))
|
|
||||||
.filter(c => c.raw > 0);
|
|
||||||
|
|
||||||
if (activeCards.length === 0) return {};
|
|
||||||
|
|
||||||
let remaining = Math.min(prepaid, totalRaw);
|
|
||||||
const result = {};
|
const result = {};
|
||||||
activeCards.forEach(c => result[c.key] = 0);
|
|
||||||
|
|
||||||
// 1단계: 한도 초과 카드에 초과분만큼 우선 차감
|
// 1. card_splits가 있는 항목: 사용자 지정 배분 적용
|
||||||
for (const card of activeCards) {
|
let splitTotal = 0;
|
||||||
if (remaining <= 0) break;
|
for (const item of prepaidItems) {
|
||||||
if (card.limit > 0 && card.raw > card.limit) {
|
if (item.card_splits && Object.keys(item.card_splits).length > 0) {
|
||||||
const excess = card.raw - card.limit;
|
for (const [cardNum, amount] of Object.entries(item.card_splits)) {
|
||||||
const deduct = Math.min(excess, remaining);
|
result[cardNum] = (result[cardNum] || 0) + (amount || 0);
|
||||||
result[card.key] += deduct;
|
splitTotal += (amount || 0);
|
||||||
remaining -= deduct;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2단계: 잔여 금액을 현재 사용액 비율로 배분
|
// 2. card_splits가 없는 항목의 금액: 기존 비율 배분
|
||||||
if (remaining > 0) {
|
const unsplitAmount = prepaidItems
|
||||||
const withCurrent = activeCards
|
.filter(item => !item.card_splits || Object.keys(item.card_splits).length === 0)
|
||||||
.map(c => ({ ...c, current: c.raw - result[c.key] }))
|
.reduce((sum, item) => sum + (item.amount || 0), 0);
|
||||||
.filter(c => c.current > 0);
|
|
||||||
const totalCurrent = withCurrent.reduce((sum, c) => sum + c.current, 0);
|
|
||||||
|
|
||||||
if (totalCurrent > 0) {
|
if (unsplitAmount > 0) {
|
||||||
let distributed = 0;
|
const activeCards = cards
|
||||||
for (let i = 0; i < withCurrent.length; i++) {
|
.filter(c => c.status === 'active')
|
||||||
const card = withCurrent[i];
|
.map(c => ({
|
||||||
let deduct;
|
key: c.cardNumber.replace(/-/g, ''),
|
||||||
if (i === withCurrent.length - 1) {
|
raw: getRawCardUsage(c.cardNumber),
|
||||||
deduct = remaining - distributed;
|
limit: c.cardType === 'credit' ? c.creditLimit : 0,
|
||||||
} else {
|
}))
|
||||||
deduct = Math.round(remaining * (card.current / totalCurrent));
|
.filter(c => c.raw > 0);
|
||||||
|
|
||||||
|
if (activeCards.length > 0) {
|
||||||
|
let remaining = Math.min(unsplitAmount, totalRaw - splitTotal);
|
||||||
|
if (remaining > 0) {
|
||||||
|
activeCards.forEach(c => { if (!result[c.key]) result[c.key] = 0; });
|
||||||
|
|
||||||
|
// 1단계: 한도 초과 카드에 초과분만큼 우선 차감
|
||||||
|
for (const card of activeCards) {
|
||||||
|
if (remaining <= 0) break;
|
||||||
|
const currentDeduction = result[card.key] || 0;
|
||||||
|
const effectiveRaw = card.raw - currentDeduction;
|
||||||
|
if (card.limit > 0 && effectiveRaw > card.limit) {
|
||||||
|
const excess = effectiveRaw - card.limit;
|
||||||
|
const deduct = Math.min(excess, remaining);
|
||||||
|
result[card.key] += deduct;
|
||||||
|
remaining -= deduct;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2단계: 잔여 금액을 현재 사용액 비율로 배분
|
||||||
|
if (remaining > 0) {
|
||||||
|
const withCurrent = activeCards
|
||||||
|
.map(c => ({ ...c, current: c.raw - (result[c.key] || 0) }))
|
||||||
|
.filter(c => c.current > 0);
|
||||||
|
const totalCurrent = withCurrent.reduce((sum, c) => sum + c.current, 0);
|
||||||
|
|
||||||
|
if (totalCurrent > 0) {
|
||||||
|
let distributed = 0;
|
||||||
|
for (let i = 0; i < withCurrent.length; i++) {
|
||||||
|
const card = withCurrent[i];
|
||||||
|
let deduct;
|
||||||
|
if (i === withCurrent.length - 1) {
|
||||||
|
deduct = remaining - distributed;
|
||||||
|
} else {
|
||||||
|
deduct = Math.round(remaining * (card.current / totalCurrent));
|
||||||
|
}
|
||||||
|
deduct = Math.min(deduct, card.current);
|
||||||
|
result[card.key] += deduct;
|
||||||
|
distributed += deduct;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deduct = Math.min(deduct, card.current);
|
|
||||||
result[card.key] += deduct;
|
|
||||||
distributed += deduct;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1026,6 +1142,144 @@ className="mt-3 flex items-center gap-1 text-sm text-amber-600 hover:text-amber-
|
|||||||
<span className="text-lg font-bold text-amber-600">{formatCurrency(prepaymentTotal)}원</span>
|
<span className="text-lg font-bold text-amber-600">{formatCurrency(prepaymentTotal)}원</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 카드분리 영역 */}
|
||||||
|
{prepaymentItems.map((item, index) => {
|
||||||
|
const itemAmount = parseInt(parseInputCurrency(item.amount)) || 0;
|
||||||
|
if (itemAmount <= 0) return null;
|
||||||
|
const isOpen = splitOpenIndex === index;
|
||||||
|
const hasSplit = item.card_splits && Object.keys(item.card_splits).length > 0;
|
||||||
|
const splitSum = getCardSplitTotal(item.card_splits);
|
||||||
|
const activeList = cards.filter(c => c.status === 'active');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`split-${index}`} className="mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setSplitOpenIndex(null);
|
||||||
|
} else {
|
||||||
|
// 카드분리 열 때 splits 초기화 (없으면)
|
||||||
|
if (!item.card_splits) {
|
||||||
|
const splits = {};
|
||||||
|
activeList.forEach(c => {
|
||||||
|
splits[c.cardNumber.replace(/-/g, '')] = 0;
|
||||||
|
});
|
||||||
|
updatePrepaymentItem(index, 'card_splits', splits);
|
||||||
|
}
|
||||||
|
setSplitOpenIndex(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center justify-between px-4 py-2.5 rounded-lg border text-sm font-medium transition-colors ${
|
||||||
|
hasSplit
|
||||||
|
? 'bg-violet-50 border-violet-300 text-violet-700 hover:bg-violet-100'
|
||||||
|
: 'bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{prepaymentItems.length > 1 ? `항목 ${index + 1} ` : ''}카드분리
|
||||||
|
{hasSplit && <span className="ml-1 text-xs">({Object.values(item.card_splits).filter(v => v > 0).length}장 배분)</span>}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">
|
||||||
|
{hasSplit ? `${formatCurrency(splitSum)}원 / ${formatCurrency(itemAmount)}원` : '미설정'}
|
||||||
|
{isOpen ? ' ▲' : ' ▼'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="mt-2 border border-violet-200 rounded-lg bg-violet-50/50 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
배분할 금액: <strong className="text-violet-700">{formatCurrency(itemAmount)}원</strong>
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button onClick={() => distributeEvenly(index)}
|
||||||
|
className="px-2 py-1 text-xs bg-white border border-gray-200 text-gray-600 rounded hover:bg-gray-50">
|
||||||
|
균등 배분
|
||||||
|
</button>
|
||||||
|
<button onClick={() => distributeByRatio(index)}
|
||||||
|
className="px-2 py-1 text-xs bg-white border border-gray-200 text-gray-600 rounded hover:bg-gray-50">
|
||||||
|
비율 배분
|
||||||
|
</button>
|
||||||
|
<button onClick={() => resetCardSplit(index)}
|
||||||
|
className="px-2 py-1 text-xs bg-white border border-red-200 text-red-500 rounded hover:bg-red-50">
|
||||||
|
해제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카드 목록 헤더 */}
|
||||||
|
<div className="flex gap-2 mb-1 text-xs font-medium text-gray-500 px-1">
|
||||||
|
<div style={{flex: '1 1 0'}}>카드</div>
|
||||||
|
<div style={{width: '90px', flexShrink: 0, textAlign: 'right'}}>기초한도</div>
|
||||||
|
<div style={{width: '90px', flexShrink: 0, textAlign: 'right'}}>바로빌 사용</div>
|
||||||
|
<div style={{width: '120px', flexShrink: 0, textAlign: 'right'}}>배분금액</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카드 목록 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{activeList.map(card => {
|
||||||
|
const cardKey = card.cardNumber.replace(/-/g, '');
|
||||||
|
const splitVal = (item.card_splits && item.card_splits[cardKey]) || 0;
|
||||||
|
const rawUsage = getRawCardUsage(card.cardNumber);
|
||||||
|
const last4 = card.cardNumber.slice(-4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={cardKey} className="flex gap-2 items-center bg-white rounded-lg px-2 py-1.5 border border-gray-100">
|
||||||
|
<div style={{flex: '1 1 0', minWidth: 0}}>
|
||||||
|
<span className="text-sm text-gray-800 truncate block">
|
||||||
|
{card.cardCompany} {card.cardName}
|
||||||
|
<span className="text-gray-400 ml-1 font-mono text-xs">{last4}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{width: '90px', flexShrink: 0, textAlign: 'right'}}>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{card.cardType === 'credit' && card.creditLimit > 0 ? formatCurrency(card.creditLimit) : '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{width: '90px', flexShrink: 0, textAlign: 'right'}}>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{rawUsage > 0 ? formatCurrency(rawUsage) : '0'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{width: '120px', flexShrink: 0}}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formatInputCurrency(splitVal)}
|
||||||
|
onChange={(e) => updateCardSplit(index, cardKey, e.target.value)}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-full px-2 py-1 border border-gray-300 rounded text-sm text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 배분 합계 */}
|
||||||
|
<div className={`mt-3 pt-2 border-t flex items-center justify-between text-sm ${
|
||||||
|
splitSum === itemAmount ? 'border-green-200' : 'border-red-200'
|
||||||
|
}`}>
|
||||||
|
<span className="font-medium text-gray-700">배분 합계</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className={`font-bold ${splitSum === itemAmount ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{formatCurrency(splitSum)}원
|
||||||
|
</span>
|
||||||
|
{splitSum !== itemAmount && (
|
||||||
|
<span className="text-xs text-red-500 ml-2">
|
||||||
|
(미배분: {formatCurrency(itemAmount - splitSum)}원)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{splitSum === itemAmount && (
|
||||||
|
<span className="text-xs text-green-500 ml-2">일치</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* 버튼 */}
|
{/* 버튼 */}
|
||||||
<div className="flex gap-3 mt-5">
|
<div className="flex gap-3 mt-5">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user