Initial commit MKD fixes
This commit is contained in:
520
components/FinanceModule.tsx
Executable file
520
components/FinanceModule.tsx
Executable file
@@ -0,0 +1,520 @@
|
||||
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { MOCK_BUILDINGS } from '../constants';
|
||||
import { Invoice, InvoiceStatus, PaymentInvoice, PaymentCalendarEntry } from '../types';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { apiClient, authFetch } from '../services/apiClient';
|
||||
|
||||
function getCalendarPeriod(interval: 'week' | 'month'): { dateFrom: string; dateTo: string } {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
if (interval === 'week') {
|
||||
const day = now.getDay();
|
||||
const mondayOffset = day === 0 ? -6 : 1 - day;
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + mondayOffset);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
return {
|
||||
dateFrom: monday.toISOString().split('T')[0],
|
||||
dateTo: sunday.toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
const first = new Date(year, month, 1);
|
||||
const last = new Date(year, month + 1, 0);
|
||||
return {
|
||||
dateFrom: first.toISOString().split('T')[0],
|
||||
dateTo: last.toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
|
||||
// Imported modular components
|
||||
import { FinanceSummary } from './finance/FinanceSummary';
|
||||
import { InvoiceRegistry } from './finance/InvoiceRegistry';
|
||||
import { PaymentCalendar } from './finance/PaymentCalendar';
|
||||
import { FinanceReports } from './finance/FinanceReports';
|
||||
import { ReportUploader } from './finance/ReportUploader';
|
||||
import { ReportProcessing } from './finance/ReportProcessing';
|
||||
import { BuildingFinancialSummary } from './finance/BuildingFinancialSummary';
|
||||
import { ReportsGrid } from './finance/ReportsGrid';
|
||||
import { ReportDetailView } from './finance/ReportDetailView';
|
||||
import { DebtorReportDetailView } from './finance/DebtorReportDetailView';
|
||||
import { ReportTypesGrid } from './finance/ReportTypesGrid';
|
||||
import { FinancialReport } from '../types';
|
||||
import { PaymentInvoiceList } from './finance/PaymentInvoiceList';
|
||||
import { PaymentInvoiceDetail } from './finance/PaymentInvoiceDetail';
|
||||
import { PaymentInvoiceForm } from './finance/PaymentInvoiceForm';
|
||||
import { allowedSubsForSection } from '../constants/permissions';
|
||||
import { useSubPermission } from '../contexts/PermissionsContext';
|
||||
import type { User } from '../types';
|
||||
|
||||
type FinanceTab = 'summary' | 'invoices' | 'calendar' | 'reports';
|
||||
type ReportsView = 'types' | 'upload' | 'processing' | 'detail' | 'list';
|
||||
type PaymentInvoicesView = 'list' | 'detail' | 'form';
|
||||
|
||||
const FINANCE_TABS: FinanceTab[] = ['summary', 'invoices', 'calendar', 'reports'];
|
||||
const SUBTAB_KEY = 'mkd_subTab_finance';
|
||||
|
||||
export type FinanceInvoicePrefill = {
|
||||
purposeType: 'event';
|
||||
purposeEventId?: string;
|
||||
purposeDescription?: string;
|
||||
totalAmount?: number;
|
||||
} | null;
|
||||
|
||||
interface FinanceModuleProps {
|
||||
currentUser?: User | null;
|
||||
externalInvoiceId?: number | null;
|
||||
externalInvoicePrefill?: FinanceInvoicePrefill;
|
||||
onInvoiceHandled?: () => void;
|
||||
/** Детальные права: null/пусто = все вкладки по роли */
|
||||
allowedPermissions?: string[] | null;
|
||||
}
|
||||
|
||||
export const FinanceModule: React.FC<FinanceModuleProps> = ({ currentUser, externalInvoiceId, externalInvoicePrefill, onInvoiceHandled, allowedPermissions }) => {
|
||||
const visibleTabs = useMemo(() => {
|
||||
const allowed = allowedSubsForSection(allowedPermissions ?? [], 'finance');
|
||||
if (allowed === 'all') return FINANCE_TABS;
|
||||
return FINANCE_TABS.filter((t) => allowed.includes(t));
|
||||
}, [allowedPermissions]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<FinanceTab>(() => {
|
||||
const s = localStorage.getItem(SUBTAB_KEY);
|
||||
const tab = (s && FINANCE_TABS.includes(s as FinanceTab)) ? s as FinanceTab : 'summary';
|
||||
return tab;
|
||||
});
|
||||
|
||||
const { canEdit: canEditCurrentTab, scopeOwn: scopeOwnCurrentTab } = useSubPermission('finance', activeTab);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleTabs.length > 0 && !visibleTabs.includes(activeTab)) {
|
||||
setActiveTab(visibleTabs[0]);
|
||||
}
|
||||
}, [visibleTabs, activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleTabs.includes(activeTab)) localStorage.setItem(SUBTAB_KEY, activeTab);
|
||||
}, [activeTab, visibleTabs]);
|
||||
const [reportsView, setReportsView] = useState<ReportsView>('types');
|
||||
const [currentJobId, setCurrentJobId] = useState<string | null>(null);
|
||||
const [currentReportId, setCurrentReportId] = useState<number | null>(null);
|
||||
const [selectedReport, setSelectedReport] = useState<FinancialReport | null>(null);
|
||||
const [selectedBuildingId, setSelectedBuildingId] = useState<string | null>(null);
|
||||
const [selectedReportType, setSelectedReportType] = useState<string | undefined>(undefined);
|
||||
const [selectedReportTypeName, setSelectedReportTypeName] = useState<string | undefined>(undefined);
|
||||
const [totalReportsCount, setTotalReportsCount] = useState<number>(0);
|
||||
|
||||
// Новая система счетов на оплату
|
||||
const [paymentInvoicesView, setPaymentInvoicesView] = useState<PaymentInvoicesView>('list');
|
||||
const [selectedPaymentInvoice, setSelectedPaymentInvoice] = useState<PaymentInvoice | null>(null);
|
||||
const [paymentInvoices, setPaymentInvoices] = useState<PaymentInvoice[]>([]);
|
||||
const currentUserId = currentUser?.id ?? '';
|
||||
const [formPrefill, setFormPrefill] = useState<FinanceInvoicePrefill>(null);
|
||||
const [calendarEntries, setCalendarEntries] = useState<PaymentCalendarEntry[]>([]);
|
||||
const [calendarInterval, setCalendarInterval] = useState<'week' | 'month'>('month');
|
||||
|
||||
const [invoices, setInvoices] = useState<Invoice[]>(() =>
|
||||
MOCK_BUILDINGS.flatMap(b => b.financials.invoices.map(inv => ({
|
||||
...inv,
|
||||
closingDocsReceived: Math.random() > 0.5,
|
||||
status: inv.status as InvoiceStatus
|
||||
})))
|
||||
);
|
||||
|
||||
const fetchCalendarEntries = useCallback(async () => {
|
||||
const { dateFrom, dateTo } = getCalendarPeriod(calendarInterval);
|
||||
try {
|
||||
const res = await apiClient.get<{ entries: PaymentCalendarEntry[] }>(
|
||||
`/finance/payment-calendar/entries?dateFrom=${dateFrom}&dateTo=${dateTo}&limit=500`
|
||||
);
|
||||
setCalendarEntries(res?.entries ?? []);
|
||||
} catch (err) {
|
||||
console.error('Error fetching payment calendar entries:', err);
|
||||
setCalendarEntries([]);
|
||||
}
|
||||
}, [calendarInterval]);
|
||||
|
||||
// Загружаем счета и записи календаря при открытии вкладки «Календарь оплат»
|
||||
useEffect(() => {
|
||||
if (activeTab === 'calendar') {
|
||||
fetchPaymentInvoices();
|
||||
fetchCalendarEntries();
|
||||
}
|
||||
}, [activeTab, fetchCalendarEntries]);
|
||||
|
||||
// Загружаем количество отчетов для отображения на главном экране
|
||||
useEffect(() => {
|
||||
if (activeTab === 'reports' && reportsView === 'types') {
|
||||
fetchReportsCount();
|
||||
}
|
||||
}, [activeTab, reportsView]);
|
||||
|
||||
const fetchReportsCount = async () => {
|
||||
try {
|
||||
const response = await authFetch('/api/finance/reports');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTotalReportsCount(Array.isArray(data) ? data.length : 0);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching reports count:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Если пришел внешний запрос открыть конкретный счет
|
||||
useEffect(() => {
|
||||
const openFromExternal = async () => {
|
||||
if (!externalInvoiceId) return;
|
||||
try {
|
||||
const invoice = await apiClient.get<PaymentInvoice>(`/finance/payment-invoices/${externalInvoiceId}`);
|
||||
setSelectedPaymentInvoice(invoice);
|
||||
setActiveTab('invoices');
|
||||
setPaymentInvoicesView('detail');
|
||||
if (onInvoiceHandled) {
|
||||
onInvoiceHandled();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening external invoice:', err);
|
||||
}
|
||||
};
|
||||
openFromExternal();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [externalInvoiceId]);
|
||||
|
||||
// Если пришло предзаполнение формы счета (например, по мероприятию)
|
||||
useEffect(() => {
|
||||
if (!externalInvoicePrefill) return;
|
||||
setFormPrefill(externalInvoicePrefill);
|
||||
setPaymentInvoicesView('form');
|
||||
setSelectedPaymentInvoice(null);
|
||||
setActiveTab('invoices');
|
||||
if (onInvoiceHandled) {
|
||||
onInvoiceHandled();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [externalInvoicePrefill]);
|
||||
|
||||
const fetchPaymentInvoices = async () => {
|
||||
try {
|
||||
const response = await apiClient.get<{ invoices: PaymentInvoice[] }>('/finance/payment-invoices?status=scheduled&limit=100');
|
||||
setPaymentInvoices(response.invoices);
|
||||
} catch (err) {
|
||||
console.error('Error fetching payment invoices:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const financialData = useMemo(() => {
|
||||
const totalBalance = MOCK_BUILDINGS.reduce((sum, b) => sum + b.financials.balance, 0);
|
||||
const scheduledAmount = invoices.filter(i => i.status === 'scheduled').reduce((sum, i) => sum + i.amount, 0);
|
||||
const totalDebt = MOCK_BUILDINGS.reduce((sum, b) => sum + b.financials.debt, 0);
|
||||
const missingDocs = invoices.filter(i => i.status === 'paid' && !i.closingDocsReceived).length;
|
||||
|
||||
return { totalBalance, totalDebt, scheduledAmount, missingDocs };
|
||||
}, [invoices]);
|
||||
|
||||
const updateInvoiceStatus = (id: string, newStatus: InvoiceStatus, extra?: Partial<Invoice>) => {
|
||||
setInvoices(prev => prev.map(inv =>
|
||||
inv.id === id ? { ...inv, status: newStatus, ...extra } : inv
|
||||
));
|
||||
};
|
||||
|
||||
const handleAddInvoice = () => {
|
||||
setPaymentInvoicesView('form');
|
||||
setSelectedPaymentInvoice(null);
|
||||
setActiveTab('invoices');
|
||||
};
|
||||
|
||||
const handleSavePaymentInvoice = async (invoiceData: Partial<PaymentInvoice>) => {
|
||||
try {
|
||||
if (selectedPaymentInvoice) {
|
||||
// Обновление существующего счета
|
||||
await apiClient.put(`/finance/payment-invoices/${selectedPaymentInvoice.id}`, invoiceData);
|
||||
} else {
|
||||
// Создание нового счета
|
||||
await apiClient.post('/finance/payment-invoices', invoiceData);
|
||||
}
|
||||
setFormPrefill(null);
|
||||
setPaymentInvoicesView('list');
|
||||
setSelectedPaymentInvoice(null);
|
||||
fetchPaymentInvoices();
|
||||
} catch (err) {
|
||||
console.error('Error saving payment invoice:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePaymentInvoiceStatus = async (
|
||||
id: number,
|
||||
status: 'paid' | 'postponed' | 'cancelled',
|
||||
payload?: { paymentDate?: string; paymentRef?: string; isCash?: boolean; postponedDate?: string; cancelReason?: string }
|
||||
) => {
|
||||
try {
|
||||
await apiClient.post(`/finance/payment-invoices/${id}/update-payment-status`, {
|
||||
status,
|
||||
paymentDate: payload?.paymentDate,
|
||||
paymentRef: payload?.paymentRef,
|
||||
isCash: payload?.isCash,
|
||||
postponedDate: payload?.postponedDate,
|
||||
cancelReason: payload?.cancelReason
|
||||
});
|
||||
fetchPaymentInvoices();
|
||||
} catch (err) {
|
||||
console.error('Error updating payment invoice status:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in pb-20 space-y-6">
|
||||
<div className="flex flex-wrap justify-between items-center gap-4">
|
||||
<h2 className="text-xl font-bold text-slate-800">Финансовый отдел</h2>
|
||||
{canEditCurrentTab && activeTab === 'invoices' && (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleAddInvoice} className="bg-primary-600 text-white p-2 rounded-xl shadow-lg shadow-primary-500/30 flex items-center gap-2 px-3 text-xs font-bold active:scale-95 transition-transform">
|
||||
<Plus className="w-4 h-4" /> Новый счет
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex p-1 bg-slate-200/50 rounded-xl overflow-x-auto no-scrollbar">
|
||||
{[
|
||||
{id: 'summary', label: 'Сводка'},
|
||||
{id: 'invoices', label: 'Реестр счетов'},
|
||||
{id: 'calendar', label: 'Календарь оплат'},
|
||||
{id: 'reports', label: 'Отчеты'}
|
||||
].filter((tab) => visibleTabs.includes(tab.id as FinanceTab)).map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as FinanceTab)}
|
||||
className={`flex-shrink-0 min-w-[7rem] px-4 py-2 text-xs font-bold whitespace-nowrap rounded-lg transition-all ${activeTab === tab.id ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sheet Content Rendering */}
|
||||
<div className="min-h-[280px] sm:min-h-[360px] md:min-h-[500px]">
|
||||
{activeTab === 'summary' && <FinanceSummary data={financialData} />}
|
||||
{activeTab === 'invoices' && (
|
||||
<>
|
||||
{paymentInvoicesView === 'form' && (
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
||||
<PaymentInvoiceForm
|
||||
invoice={selectedPaymentInvoice || undefined}
|
||||
initialPrefill={formPrefill ?? undefined}
|
||||
currentUserId={currentUserId}
|
||||
onSave={handleSavePaymentInvoice}
|
||||
onCancel={() => {
|
||||
setFormPrefill(null);
|
||||
setPaymentInvoicesView('list');
|
||||
setSelectedPaymentInvoice(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{paymentInvoicesView === 'detail' && selectedPaymentInvoice && (
|
||||
<PaymentInvoiceDetail
|
||||
invoice={selectedPaymentInvoice}
|
||||
currentUserId={currentUserId}
|
||||
onBack={() => {
|
||||
setPaymentInvoicesView('list');
|
||||
setSelectedPaymentInvoice(null);
|
||||
}}
|
||||
onUpdate={() => {
|
||||
fetchPaymentInvoices();
|
||||
// Перезагружаем детали счета
|
||||
apiClient.get(`/finance/payment-invoices/${selectedPaymentInvoice.id}`)
|
||||
.then(inv => setSelectedPaymentInvoice(inv))
|
||||
.catch(err => console.error('Error fetching invoice details:', err));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{paymentInvoicesView === 'list' && (
|
||||
<PaymentInvoiceList
|
||||
onInvoiceClick={(invoice) => {
|
||||
setSelectedPaymentInvoice(invoice);
|
||||
setPaymentInvoicesView('detail');
|
||||
}}
|
||||
onCreateNew={handleAddInvoice}
|
||||
currentUserId={currentUserId}
|
||||
canEdit={canEditCurrentTab}
|
||||
scopeOwn={scopeOwnCurrentTab}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'calendar' && (
|
||||
<PaymentCalendar
|
||||
invoices={invoices}
|
||||
paymentInvoices={paymentInvoices}
|
||||
calendarEntries={calendarEntries}
|
||||
onUpdateStatus={updateInvoiceStatus}
|
||||
onUpdatePaymentInvoiceStatus={handleUpdatePaymentInvoiceStatus}
|
||||
onRefreshCalendar={() => {
|
||||
fetchPaymentInvoices();
|
||||
fetchCalendarEntries();
|
||||
}}
|
||||
currentBalance={financialData.totalBalance}
|
||||
currentUserId={currentUserId}
|
||||
interval={calendarInterval}
|
||||
onIntervalChange={setCalendarInterval}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'reports' && (
|
||||
<div className="space-y-6">
|
||||
{/* Детальный просмотр отчета */}
|
||||
{reportsView === 'detail' && selectedReport && selectedReport.reportType === 'debtors' && (
|
||||
<DebtorReportDetailView
|
||||
report={selectedReport}
|
||||
onBack={() => {
|
||||
setReportsView('list');
|
||||
setSelectedReport(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{reportsView === 'detail' && selectedReport && selectedReport.reportType !== 'debtors' && (
|
||||
<ReportDetailView
|
||||
report={selectedReport}
|
||||
onBack={() => {
|
||||
setReportsView('list');
|
||||
setSelectedReport(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Список загруженных отчетов */}
|
||||
{reportsView === 'list' && (
|
||||
<ReportsGrid
|
||||
onReportClick={(report) => {
|
||||
setSelectedReport(report);
|
||||
setReportsView('detail');
|
||||
}}
|
||||
reportTypeFilter={selectedReportType}
|
||||
onBack={() => {
|
||||
setReportsView('types');
|
||||
setSelectedReportType(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Загрузка данных (старый вариант без предустановленного типа) */}
|
||||
{reportsView === 'upload' && !selectedReportType && (
|
||||
<ReportUploader
|
||||
onUploadSuccess={(reportId, jobId) => {
|
||||
setCurrentReportId(reportId);
|
||||
setCurrentJobId(jobId ?? null);
|
||||
window.dispatchEvent(new CustomEvent('mkd-finance-reports-changed'));
|
||||
if (jobId) {
|
||||
setReportsView('processing');
|
||||
} else {
|
||||
fetchReportsCount();
|
||||
setReportsView('list');
|
||||
}
|
||||
}}
|
||||
onUploadError={(error) => {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
}}
|
||||
onClose={() => {
|
||||
setReportsView('types');
|
||||
setSelectedReportType(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Обработка */}
|
||||
{reportsView === 'processing' && currentJobId && (
|
||||
<ReportProcessing
|
||||
jobId={currentJobId}
|
||||
reportId={currentReportId || undefined}
|
||||
onComplete={() => {
|
||||
window.dispatchEvent(new CustomEvent('mkd-finance-reports-changed'));
|
||||
fetchReportsCount();
|
||||
setReportsView('types');
|
||||
setCurrentJobId(null);
|
||||
setCurrentReportId(null);
|
||||
setSelectedReportType(undefined);
|
||||
}}
|
||||
onViewReports={() => {
|
||||
// Обновляем счетчик отчетов перед переходом к списку
|
||||
fetchReportsCount();
|
||||
setReportsView('list');
|
||||
setCurrentJobId(null);
|
||||
setCurrentReportId(null);
|
||||
setSelectedReportType(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{reportsView === 'processing' && !currentJobId && (
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
|
||||
<p className="text-slate-500">Нет активных процессов обработки</p>
|
||||
<button
|
||||
onClick={() => setReportsView('types')}
|
||||
className="mt-4 text-primary-600 hover:text-primary-700 font-medium"
|
||||
>
|
||||
Вернуться к типам отчетов
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Типы отчетов (основной экран) */}
|
||||
{reportsView === 'types' && (
|
||||
<ReportTypesGrid
|
||||
onReportTypeClick={(reportType) => {
|
||||
// При клике на квадратик открываем модальное окно загрузки с предустановленным типом
|
||||
setSelectedReportType(reportType.reportType);
|
||||
setSelectedReportTypeName(reportType.name);
|
||||
setReportsView('upload');
|
||||
}}
|
||||
onViewReportsClick={() => {
|
||||
setReportsView('list');
|
||||
setSelectedReportType(undefined);
|
||||
}}
|
||||
totalReportsCount={totalReportsCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно загрузки с предустановленным типом */}
|
||||
{reportsView === 'upload' && selectedReportType && (
|
||||
<div className="relative">
|
||||
<ReportUploader
|
||||
presetReportType={selectedReportType as any}
|
||||
reportTypeName={selectedReportTypeName}
|
||||
onUploadSuccess={(reportId, jobId) => {
|
||||
setCurrentReportId(reportId);
|
||||
setCurrentJobId(jobId ?? null);
|
||||
window.dispatchEvent(new CustomEvent('mkd-finance-reports-changed'));
|
||||
fetchReportsCount();
|
||||
if (jobId) {
|
||||
setReportsView('processing');
|
||||
} else {
|
||||
setReportsView('list');
|
||||
setSelectedReportType(undefined);
|
||||
}
|
||||
}}
|
||||
onUploadError={(error) => {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
}}
|
||||
onViewReports={() => {
|
||||
setReportsView('list');
|
||||
setSelectedReportType(undefined);
|
||||
setSelectedReportTypeName(undefined);
|
||||
}}
|
||||
onClose={() => {
|
||||
setReportsView('types');
|
||||
setSelectedReportType(undefined);
|
||||
setSelectedReportTypeName(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user