feat: relocate orders and deliveries to store console & polish store cover images
This commit is contained in:
478
src/components/ReportsView.tsx
Normal file
478
src/components/ReportsView.tsx
Normal file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Download,
|
||||
Filter,
|
||||
ArrowUpRight,
|
||||
ChevronDown,
|
||||
TrendingUp as TrendUp,
|
||||
TrendingDown as TrendDown,
|
||||
Equal as TrendFlat,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { LeaderboardNode } from '../types';
|
||||
import {
|
||||
useFiestaOrderSummary,
|
||||
useFiestaLocationSummary,
|
||||
useFiestaOrderInsight,
|
||||
useFiestaStockStatement,
|
||||
} from '../services/fiestaQueries';
|
||||
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
|
||||
import { stockRowToProduct } from '../services/fiestaMappers';
|
||||
|
||||
interface ReportsViewProps {
|
||||
searchQuery: string;
|
||||
isCoimbatoreView: boolean;
|
||||
}
|
||||
|
||||
const MONTH_KEYS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dece'];
|
||||
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
export default function ReportsView({ searchQuery, isCoimbatoreView }: ReportsViewProps) {
|
||||
const [selectedTimeframe, setSelectedTimeframe] = useState('Month to Date');
|
||||
const [stockFilter, setStockFilter] = useState<'All' | 'Healthy' | 'Low Stock' | 'Critical'>('All');
|
||||
const [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// ── Live analytics (Fiesta) ───────────────────────────────────────────────
|
||||
const today = new Date();
|
||||
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const yearStart = new Date(today.getFullYear(), 0, 1);
|
||||
const fromdate = ymd(monthStart);
|
||||
const todate = ymd(today);
|
||||
|
||||
const summaryQ = useFiestaOrderSummary(FIESTA_TENANT_ID, ymd(yearStart), todate);
|
||||
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
|
||||
const insightQ = useFiestaOrderInsight(FIESTA_TENANT_ID);
|
||||
const stockQ = useFiestaStockStatement({
|
||||
tenantid: FIESTA_TENANT_ID,
|
||||
locationid: FIESTA_PRIMARY_LOCATION_ID,
|
||||
keyword: '',
|
||||
pageno: 1,
|
||||
pagesize: 100,
|
||||
});
|
||||
|
||||
const s = summaryQ.data;
|
||||
const activeSkus = (stockQ.data ?? []).length;
|
||||
|
||||
// KPI row — all live.
|
||||
const reportsKPIs = [
|
||||
{
|
||||
title: 'Total Orders (YTD)',
|
||||
value: (s?.total ?? 0).toLocaleString('en-IN'),
|
||||
subtext: `${ymd(yearStart)} → ${todate}`,
|
||||
trend: `${s?.delivered ?? 0} delivered`,
|
||||
isPositive: true,
|
||||
},
|
||||
{
|
||||
title: 'Delivered',
|
||||
value: (s?.delivered ?? 0).toLocaleString('en-IN'),
|
||||
subtext: `${s ? Math.round(((s.delivered) / Math.max(s.total, 1)) * 100) : 0}% of all orders`,
|
||||
trend: `${s?.pending ?? 0} pending`,
|
||||
isPositive: true,
|
||||
},
|
||||
{
|
||||
title: 'Cancelled',
|
||||
value: (s?.cancelled ?? 0).toLocaleString('en-IN'),
|
||||
subtext: `${s ? Math.round(((s.cancelled) / Math.max(s.total, 1)) * 100) : 0}% of all orders`,
|
||||
trend: `${s?.created ?? 0} created`,
|
||||
isPositive: false,
|
||||
},
|
||||
{
|
||||
title: 'Active SKUs',
|
||||
value: activeSkus.toLocaleString('en-IN'),
|
||||
subtext: 'Live stock statement entries',
|
||||
trend: 'In catalogue',
|
||||
isPositive: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Leaderboard — outlets ranked by total live orders.
|
||||
const leaderboard: LeaderboardNode[] = (() => {
|
||||
const rows = [...(locSummaryQ.data ?? [])].sort((a, b) => b.total - a.total).slice(0, 4);
|
||||
const max = rows.length ? rows[0].total : 0;
|
||||
return rows.map((r, i) => ({
|
||||
rank: String(i + 1).padStart(2, '0'),
|
||||
name: r.locationname || `Location ${r.locationid}`,
|
||||
percentage: max > 0 ? Math.round((r.total / max) * 100) : 0,
|
||||
revenue: `${r.total.toLocaleString('en-IN')} ord`,
|
||||
}));
|
||||
})();
|
||||
const currentLeaderboard = leaderboard;
|
||||
|
||||
// Monthly order distribution per outlet (replaces the static hourly heatmap).
|
||||
const insightRows = (insightQ.data ?? []).map((r) => ({
|
||||
name: fstr(r.locationname) || `Location ${fstr(r.locationid)}`,
|
||||
months: (r.ordermonths ?? {}) as Record<string, unknown>,
|
||||
}));
|
||||
const heatmapMax = Math.max(
|
||||
1,
|
||||
...insightRows.flatMap((row) => MONTH_KEYS.map((k) => fnum(row.months[k]))),
|
||||
);
|
||||
|
||||
// Live product performance matrix.
|
||||
const liveProducts = (stockQ.data ?? []).map(stockRowToProduct);
|
||||
const filteredProducts = liveProducts.filter((prod) => {
|
||||
const matchesSearch =
|
||||
prod.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
prod.sku.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesStock = stockFilter === 'All' ? true : prod.stockStatus === stockFilter;
|
||||
return matchesSearch && matchesStock;
|
||||
});
|
||||
|
||||
// Heatmap cell colour, scaled relative to the busiest month/outlet.
|
||||
const getHeatmapColorClass = (val: number) => {
|
||||
const ratio = val / heatmapMax;
|
||||
if (ratio < 0.15) return 'bg-[#581c87]/10 text-[#0f172a] hover:bg-[#581c87]/20';
|
||||
if (ratio <= 0.5) return 'bg-[#a78bfa]/40 text-[#581c87] hover:bg-[#a78bfa]/50';
|
||||
return 'bg-[#581c87] text-white hover:bg-purple-800';
|
||||
};
|
||||
|
||||
// Export alerts
|
||||
const triggerExport = (format: 'PDF' | 'CSV') => {
|
||||
alert(`BI Engine initiating automated ${format} bundle export. Generating compiled schema reports...`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-lg animate-in fade-in duration-500">
|
||||
|
||||
{/* Context filter header row */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-md bg-[#f8fafc] border border-[#e2e8f0] p-md rounded-xl shadow-sm">
|
||||
<div>
|
||||
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
|
||||
Business Intelligence Center
|
||||
</h1>
|
||||
<p className="text-zinc-500 font-sans text-xs mt-1">
|
||||
Real-time analytical metrics engine surfacing regional performance deltas and potential logistic constraints.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action picker filters */}
|
||||
<div className="flex items-center gap-sm flex-wrap text-xs">
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-lg px-sm py-1.5 flex items-center gap-sm shadow-sm select-none">
|
||||
<span className="text-zinc-400 font-medium">Timeframe:</span>
|
||||
<select
|
||||
value={selectedTimeframe}
|
||||
onChange={(e) => setSelectedTimeframe(e.target.value)}
|
||||
className="bg-transparent border-none focus:ring-0 font-sans font-semibold text-zinc-700 cursor-pointer outline-none"
|
||||
>
|
||||
<option>Month to Date</option>
|
||||
<option>Year to Date</option>
|
||||
<option>Last 12 Months</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-lg px-sm py-1.5 flex items-center gap-sm shadow-sm font-semibold text-zinc-700">
|
||||
<Filter size={14} className="text-zinc-400 font-medium" />
|
||||
<span>{isCoimbatoreView ? 'Coimbatore Zones (5)' : 'All Regions (12)'}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => triggerExport('PDF')}
|
||||
className="bg-[#581c87] text-white font-sans font-semibold px-4 py-1.5 rounded-lg flex items-center gap-sm cursor-pointer transition-colors hover:bg-purple-800 active:bg-purple-900 shadow-sm"
|
||||
>
|
||||
<Download size={13} />
|
||||
Export PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary KPI Row - 4 Key cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-gutter mb-xl text-xs">
|
||||
{reportsKPIs.map((kpi, idx) => (
|
||||
<div key={idx} className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm">
|
||||
<div className="flex justify-between items-start mb-sm">
|
||||
<span className="text-[11px] font-sans font-bold text-zinc-400 uppercase tracking-widest block font-sans">
|
||||
{kpi.title}
|
||||
</span>
|
||||
|
||||
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded flex items-center gap-px ${
|
||||
kpi.isPositive ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'
|
||||
}`}>
|
||||
{kpi.isPositive ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
|
||||
{kpi.trend}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="font-sans font-bold text-[#0f172a] text-xl tracking-tight">
|
||||
{kpi.value}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] text-zinc-400 font-medium tracking-wide mt-sm uppercase font-sans">
|
||||
{kpi.subtext}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bento split maps */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-gutter">
|
||||
|
||||
{/* Revenue Heatmap table - 8 Cols */}
|
||||
<div className="lg:col-span-8 bg-white border border-[#e2e8f0] rounded-xl overflow-hidden flex flex-col justify-between shadow-sm">
|
||||
|
||||
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm flex justify-between items-center">
|
||||
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
|
||||
Monthly Order Distribution by Outlet
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold text-zinc-400 uppercase tracking-tight">
|
||||
<span className="w-2 h-2 rounded-full bg-[#581c87] animate-pulse"></span>
|
||||
<span>Busiest Month</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-md flex-1 overflow-x-auto">
|
||||
{selectedCell && (
|
||||
<div className="mb-sm p-sm bg-purple-50 border border-purple-100 rounded-lg text-xs flex justify-between items-center animate-in fade-in">
|
||||
<span className="font-sans text-blue-900 font-medium">
|
||||
<strong className="font-bold">{selectedCell.day}</strong> registered <strong className="font-bold font-mono">{selectedCell.val}</strong> order(s) in <strong className="font-bold">{selectedCell.hour}</strong>.
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setSelectedCell(null)}
|
||||
className="text-xs font-bold text-[#581c87] hover:underline cursor-pointer"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{insightRows.length === 0 ? (
|
||||
<div className="py-xl text-center text-zinc-400 text-xs">
|
||||
{insightQ.isLoading ? 'Loading monthly order distribution…' : 'No order insight available for this tenant.'}
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-center border-collapse text-xs font-sans">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-xs text-[10px] font-bold text-zinc-400 w-32 uppercase italic">Outlet</th>
|
||||
{MONTH_LABELS.map((m) => (
|
||||
<th key={m} className="p-sm text-[10px] font-bold text-[#0f172a] border-b border-[#f1f5f9]">
|
||||
{m}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{insightRows.map((row, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="p-sm text-left font-bold text-[#0f172a] tracking-wide truncate max-w-[8rem] border-r border-[#f1f5f9]">
|
||||
{row.name}
|
||||
</td>
|
||||
{MONTH_KEYS.map((key, mIdx) => {
|
||||
const val = fnum(row.months[key]);
|
||||
return (
|
||||
<td key={key} className="p-1 border border-white">
|
||||
<button
|
||||
onClick={() => setSelectedCell({ day: row.name, hour: MONTH_LABELS[mIdx], val })}
|
||||
className={`w-full py-2.5 rounded-lg font-semibold transition-all duration-100 cursor-pointer ${getHeatmapColorClass(val)}`}
|
||||
>
|
||||
{val}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-md py-sm bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between text-[11px] text-zinc-400 font-sans font-medium">
|
||||
<span>Colour intensity scales with monthly order volume per outlet</span>
|
||||
<span>Click cells to inspect metrics</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leaderboard nodes bar list - 4 Cols */}
|
||||
<div className="lg:col-span-4 bg-white border border-[#e2e8f0] rounded-xl flex flex-col shadow-sm mt-0">
|
||||
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm">
|
||||
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
|
||||
Top Performing Nodes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-md flex-1 space-y-md flex flex-col justify-center">
|
||||
{currentLeaderboard.map((node) => (
|
||||
<div key={node.rank} className="flex items-center gap-md text-xs">
|
||||
<span className="text-xs font-bold text-zinc-400 font-mono w-4">
|
||||
{node.rank}
|
||||
</span>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-center font-medium">
|
||||
<span className="text-[#0f172a] font-semibold">{node.name}</span>
|
||||
<span className="text-[#581c87] font-mono font-bold">{node.revenue}</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-[#eceef0] h-1.5 rounded-full mt-1 overflow-hidden">
|
||||
<div
|
||||
className="bg-[#581c87] h-full rounded-full transition-all duration-300"
|
||||
style={{ width: `${node.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Performance Matrix table */}
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden shadow-sm">
|
||||
|
||||
{/* Table header with filters control */}
|
||||
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] p-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-sm">
|
||||
<div>
|
||||
<h3 className="font-sans font-bold text-sm text-[#0f172a]">
|
||||
Product Performance Matrix
|
||||
</h3>
|
||||
<p className="text-zinc-500 text-xs font-sans mt-0.5">
|
||||
Assortment unit sales and physical balance trend indices.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick interactive filter pills */}
|
||||
<div className="flex gap-2 text-xs font-semibold">
|
||||
{(['All', 'Healthy', 'Low Stock', 'Critical'] as const).map((filter) => (
|
||||
<button
|
||||
key={filter}
|
||||
onClick={() => setStockFilter(filter)}
|
||||
className={`px-3 py-1 rounded-lg cursor-pointer transition-colors ${
|
||||
stockFilter === filter
|
||||
? 'bg-[#581c87] text-white shadow-sm'
|
||||
: 'bg-white border border-[#e2e8f0] text-zinc-600 hover:bg-zinc-50'
|
||||
}`}
|
||||
>
|
||||
{filter}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => triggerExport('CSV')}
|
||||
className="px-3 py-1 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-800 transition-colors shadow-sm"
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Matrix Data table */}
|
||||
<div className="overflow-x-auto text-xs font-sans">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[10px] uppercase font-bold tracking-wider">
|
||||
<tr>
|
||||
<th className="px-md py-sm">Product Name</th>
|
||||
<th className="px-md py-sm">SKU ID</th>
|
||||
<th className="px-md py-sm text-right">Units Sold</th>
|
||||
<th className="px-md py-sm text-right">Revenue</th>
|
||||
<th className="px-md py-sm text-right">Stock Status</th>
|
||||
<th className="px-md py-sm text-center">Trend index</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#f1f5f9]">
|
||||
{filteredProducts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-zinc-400">
|
||||
No matching items matching stock filter criteria.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredProducts.map((prod) => (
|
||||
<tr key={prod.id} className="hover:bg-[#f2f4f6]/40 transition-all">
|
||||
<td className="px-md py-md flex items-center gap-sm">
|
||||
<div className="w-8 h-8 rounded-lg shrink-0 border border-[#e2e8f0] overflow-hidden bg-zinc-50">
|
||||
<img
|
||||
src={prod.image}
|
||||
alt={prod.name}
|
||||
referrerPolicy="no-referrer"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="font-semibold text-[#0f172a]">{prod.name}</span>
|
||||
</td>
|
||||
<td className="px-md py-md font-mono text-zinc-500 font-medium">
|
||||
{prod.sku}
|
||||
</td>
|
||||
<td className="px-md py-md text-right font-mono text-zinc-600 font-semibold">
|
||||
{prod.unitsSold.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-md py-md text-right font-mono text-zinc-700 font-bold">
|
||||
₹{prod.revenue.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-md py-md text-right">
|
||||
<span className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-tight ${
|
||||
prod.stockStatus === 'Healthy'
|
||||
? 'bg-emerald-100 text-emerald-750 font-bold font-sans'
|
||||
: prod.stockStatus === 'Low Stock'
|
||||
? 'bg-amber-100 text-amber-750 font-bold font-sans'
|
||||
: 'bg-rose-100 text-rose-750 font-bold font-sans'
|
||||
}`}>
|
||||
{prod.stockStatus}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-md py-md text-center">
|
||||
<span className="inline-block p-1 bg-[#f8fafc] border border-zinc-100 rounded-full">
|
||||
{prod.trend === 'up' ? (
|
||||
<TrendUp size={14} className="text-emerald-500" />
|
||||
) : prod.trend === 'down' ? (
|
||||
<TrendDown size={14} className="text-rose-500" />
|
||||
) : (
|
||||
<TrendFlat size={14} className="text-zinc-400" />
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Matrix table pagination */}
|
||||
<div className="p-md bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between items-center text-[10px] text-zinc-500 font-semibold font-sans">
|
||||
<span>Showing 1-{filteredProducts.length} of {liveProducts.length} live products</span>
|
||||
|
||||
<div className="flex gap-xs">
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
|
||||
>
|
||||
<ChevronLeft size={12} />
|
||||
</button>
|
||||
<button className={`w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center font-bold text-[10px] bg-[#0f172a] text-white`}>
|
||||
1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => alert('Proceeding to page 2 details representation')}
|
||||
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-500 font-bold font-mono text-[10px] cursor-pointer"
|
||||
>
|
||||
2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => alert('Proceeding to page 3 details representation')}
|
||||
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-500 font-bold font-mono text-[10px] cursor-pointer"
|
||||
>
|
||||
3
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => prev + 1)}
|
||||
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
|
||||
>
|
||||
<ChevronRight size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user