Frontend overhaul: manager dashboard restructure, waiter PWA rework, new order drawer and components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,31 +2,218 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import TableCard from '../components/TableCard'
|
||||
import ConnectionBanner from '../components/ConnectionBanner'
|
||||
import UserMenu from '../components/UserMenu'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import useTableColourStore from '../store/tableColourStore'
|
||||
import client from '../api/client'
|
||||
import { useNotifications } from '../context/NotificationContext'
|
||||
import { FlagsIcon, TransferIcon, MergeIcon, PrintIcon, WaiterIcon } from '../components/Icons'
|
||||
|
||||
const FILTERS = ['all', 'mine', 'free']
|
||||
const FILTER_LABELS = { all: 'Όλα', mine: 'Δικά μου', free: 'Ελεύθερα' }
|
||||
|
||||
function fmtPrice(v) { return Number(v || 0).toFixed(2) + ' €' }
|
||||
|
||||
// ─── Notification history drawer ─────────────────────────────────────────────
|
||||
|
||||
function NotificationDrawer({ messages, onClose, onAck }) {
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-sheet" onClick={e => e.stopPropagation()} style={{ maxHeight: '80svh' }}>
|
||||
<div className="modal-handle" />
|
||||
<h2 className="modal-title" style={{ marginBottom: 16 }}>Ειδοποιήσεις</h2>
|
||||
{messages.length === 0 && (
|
||||
<p style={{ textAlign: 'center', color: 'var(--muted)', padding: 24 }}>Δεν υπάρχουν ειδοποιήσεις</p>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, overflowY: 'auto', flex: 1 }}>
|
||||
{messages.map(msg => {
|
||||
const tableIds = (() => { try { return JSON.parse(msg.table_ids || '[]') } catch { return [] } })()
|
||||
return (
|
||||
<div key={msg.id} style={{
|
||||
padding: '12px 4px', borderBottom: '1px solid var(--border)',
|
||||
display: 'flex', gap: 12, alignItems: 'flex-start',
|
||||
opacity: msg._acked ? 0.5 : 1,
|
||||
}}>
|
||||
<span style={{ fontSize: 20, flexShrink: 0 }}>📢</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{msg.sender_name && (
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: '#a5b4fc', marginBottom: 2 }}>
|
||||
{msg.sender_name}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{msg.body}</div>
|
||||
{tableIds.length > 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 2 }}>Τραπέζι: {tableIds.join(', ')}</div>
|
||||
)}
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 2 }}>
|
||||
{new Date(msg.created_at).toLocaleTimeString('el-GR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button className="btn btn--secondary" style={{ width: '100%', marginTop: 12 }} onClick={onClose}>Κλείσιμο</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Table quick-view + actions popup (long-press) ────────────────────────────
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
{ Icon: FlagsIcon, label: 'Ενδείξεις Τραπεζιού', key: 'flags', color: '#fac823', iconBg: 'rgba(251,191,36,0.15)' },
|
||||
{ Icon: TransferIcon, label: 'Μεταφορά', key: 'transfer', color: '#6099db', iconBg: 'rgba(96,165,250,0.15)' },
|
||||
{ Icon: MergeIcon, label: 'Συγχώνευση', key: 'merge', color: '#6099db', iconBg: 'rgba(96,165,250,0.15)' },
|
||||
{ Icon: PrintIcon, label: 'Εκτύπωση Σύνοψης', key: 'print_synopsis', color: '#cbd5e1', iconBg: 'rgba(148,163,184,0.15)' },
|
||||
{ Icon: WaiterIcon, label: 'Ανάθεση Σερβιτόρου', key: 'assign_waiter', color: '#39b861', iconBg: 'rgba(34,197,94,0.15)' },
|
||||
]
|
||||
|
||||
function TableQuickModal({ table, order, flags, onClose, onNavigate, onAction }) {
|
||||
const tableName = table.label || `T${table.number}`
|
||||
const activeItems = order?.items?.filter(i => i.status === 'active') || []
|
||||
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
|
||||
const paid = order?.payments?.reduce((s, p) => s + p.amount, 0) || 0
|
||||
const due = Math.max(0, total - paid)
|
||||
|
||||
const statusLabel = {
|
||||
open: 'Ανοιχτό',
|
||||
partially_paid: 'Μερικώς πληρωμένο',
|
||||
paid: 'Πληρωμένο',
|
||||
}[order?.status] || 'Ελεύθερο'
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
{/* Status overview card */}
|
||||
<div style={{ width: '100%', maxWidth: 480, margin: '0 auto' }} onClick={e => e.stopPropagation()}>
|
||||
<div style={{
|
||||
background: 'var(--bg2)', borderRadius: '16px 16px 0 0',
|
||||
padding: '16px 20px', borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<div className="modal-handle" style={{ marginBottom: 12 }} />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 12 }}>
|
||||
<span style={{ fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>{tableName}</span>
|
||||
<span style={{ fontSize: 13, color: 'var(--muted)' }}>{statusLabel}</span>
|
||||
</div>
|
||||
|
||||
{order ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 14 }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Σύνολο</span>
|
||||
<span style={{ fontWeight: 600, color: 'var(--text)' }}>{fmtPrice(total)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 14 }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Πληρωμένο</span>
|
||||
<span style={{ fontWeight: 600, color: '#22c55e' }}>{fmtPrice(paid)}</span>
|
||||
</div>
|
||||
{due > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 14 }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Υπόλοιπο</span>
|
||||
<span style={{ fontWeight: 700, color: '#f59e0b' }}>{fmtPrice(due)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ fontSize: 13, color: 'var(--muted)', marginBottom: 12 }}>Δεν υπάρχει ενεργή παραγγελία</p>
|
||||
)}
|
||||
|
||||
{flags.length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{flags.map(f => (
|
||||
<div key={f.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
background: (f.color || '#6295F3') + '22',
|
||||
border: `1px solid ${f.color || '#6295F3'}`,
|
||||
borderRadius: 20, padding: '4px 10px',
|
||||
}}>
|
||||
<span style={{ fontSize: 14 }}>{f.emoji || '🏷️'}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: f.color || '#6295F3' }}>{f.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn btn--primary"
|
||||
style={{ width: '100%', marginTop: 14 }}
|
||||
onClick={() => { onClose(); onNavigate() }}
|
||||
>
|
||||
Άνοιγμα τραπεζιού
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick actions card */}
|
||||
<div style={{
|
||||
background: 'var(--bg2)', borderRadius: '0 0 16px 16px',
|
||||
padding: '8px 20px 24px',
|
||||
borderTop: '2px solid var(--border)',
|
||||
}}>
|
||||
<p style={{ fontSize: 11, fontWeight: 700, color: 'var(--muted)', letterSpacing: 1, marginBottom: 8, marginTop: 8 }}>
|
||||
ACTIONS
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{QUICK_ACTIONS.map((a, i) => {
|
||||
const disabled = !order && a.key !== 'flags'
|
||||
return (
|
||||
<button
|
||||
key={a.key}
|
||||
disabled={disabled}
|
||||
onClick={() => { onClose(); onAction(a.key) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '12px 0', background: 'none', border: 'none',
|
||||
borderBottom: i < QUICK_ACTIONS.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.35 : 1, textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 36, height: 36, borderRadius: 9, flexShrink: 0,
|
||||
background: a.iconBg,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: a.color,
|
||||
}}>
|
||||
<a.Icon width="18" height="18" />
|
||||
</span>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: a.color }}>{a.label}</span>
|
||||
{!disabled && <span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: 18 }}>›</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main page ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function TableListPage() {
|
||||
const { user, logout } = useAuthStore()
|
||||
const { user } = useAuthStore()
|
||||
const [tables, setTables] = useState([])
|
||||
const [groups, setGroups] = useState([])
|
||||
const [orders, setOrders] = useState([])
|
||||
const [flagDefs, setFlagDefs] = useState([])
|
||||
const [flagAssignments, setFlagAssignments] = useState([])
|
||||
const [filter, setFilter] = useState('all')
|
||||
const [offline, setOffline] = useState(false)
|
||||
const [zoneOpen, setZoneOpen] = useState(false)
|
||||
const [selectedZones, setSelectedZones] = useState(new Set())
|
||||
const [showNotifs, setShowNotifs] = useState(false)
|
||||
const [quickModal, setQuickModal] = useState(null) // { table, order, flags }
|
||||
const zoneRef = useRef(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { unreadCount, recentMessages, ackMessage, fetchRecent } = useNotifications() || {}
|
||||
const loadFromBackend = useTableColourStore(s => s.loadFromBackend)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setOffline(true)
|
||||
window.addEventListener('backend-offline', handler)
|
||||
return () => window.removeEventListener('backend-offline', handler)
|
||||
}, [])
|
||||
|
||||
// Close zone dropdown on outside click
|
||||
useEffect(() => {
|
||||
function onClick(e) {
|
||||
if (zoneRef.current && !zoneRef.current.contains(e.target)) setZoneOpen(false)
|
||||
@@ -37,22 +224,42 @@ export default function TableListPage() {
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [tablesRes, ordersRes, groupsRes] = await Promise.all([
|
||||
const [tablesRes, ordersRes, groupsRes, flagDefsRes, flagAssignRes, settingsRes] = await Promise.all([
|
||||
client.get('/api/tables/'),
|
||||
client.get('/api/orders/my'),
|
||||
client.get('/api/orders/active'),
|
||||
client.get('/api/tables/groups'),
|
||||
client.get('/api/flags/defs'),
|
||||
client.get('/api/flags/assignments'),
|
||||
client.get('/api/settings/'),
|
||||
])
|
||||
setTables(tablesRes.data)
|
||||
setOrders(ordersRes.data)
|
||||
setGroups(groupsRes.data)
|
||||
setFlagDefs(flagDefsRes.data)
|
||||
setFlagAssignments(flagAssignRes.data)
|
||||
const raw = settingsRes.data?.['ui.table_colours']?.value
|
||||
if (raw) loadFromBackend(raw)
|
||||
setOffline(false)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const flagDefMap = Object.fromEntries(flagDefs.map(f => [f.id, f]))
|
||||
const tableFlagsMap = {}
|
||||
flagAssignments.forEach(a => {
|
||||
if (!tableFlagsMap[a.table_id]) tableFlagsMap[a.table_id] = []
|
||||
const def = flagDefMap[a.flag_id]
|
||||
if (def) tableFlagsMap[a.table_id].push(def)
|
||||
})
|
||||
|
||||
function getOrder(tableId) {
|
||||
return orders.find(o => o.table_id === tableId && ['open', 'partially_paid'].includes(o.status))
|
||||
return orders.find(o => o.table_id === tableId)
|
||||
}
|
||||
|
||||
function isMyOrder(order) {
|
||||
if (!order || !user) return false
|
||||
return order.waiter_ids?.includes(user.id)
|
||||
}
|
||||
|
||||
function toggleZone(id) {
|
||||
@@ -66,24 +273,50 @@ export default function TableListPage() {
|
||||
const filtered = tables.filter(t => {
|
||||
const order = getOrder(t.id)
|
||||
if (filter === 'free' && order) return false
|
||||
if (filter === 'mine' && !(order && order.waiters?.some(w => w.waiter_id === user?.id))) return false
|
||||
if (filter === 'mine' && !isMyOrder(order)) return false
|
||||
if (selectedZones.size > 0 && !selectedZones.has(t.group_id ?? 'none')) return false
|
||||
return true
|
||||
})
|
||||
|
||||
function handleLogout() {
|
||||
logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const zoneActive = selectedZones.size > 0
|
||||
|
||||
function handleQuickAction(tableId, actionKey) {
|
||||
// Navigate to table then trigger action via URL param so TableDetailPage can handle it
|
||||
navigate(`/tables/${tableId}?action=${actionKey}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<header className="top-bar">
|
||||
<span className="top-bar__title">Τραπέζια</span>
|
||||
<span className="top-bar__user">{user?.username}</span>
|
||||
<button className="icon-btn" onClick={handleLogout} title="Αποσύνδεση">⏏</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowNotifs(true); fetchRecent?.() }}
|
||||
style={{
|
||||
position: 'relative', background: 'none', border: 'none',
|
||||
color: 'var(--text)', fontSize: 22, cursor: 'pointer',
|
||||
minWidth: 44, minHeight: 44, borderRadius: 8,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.3399 14.49L18.3399 12.83C18.1299 12.46 17.9399 11.76 17.9399 11.35V8.82C17.9399 6.47 16.5599 4.44 14.5699 3.49C14.0499 2.57 13.0899 2 11.9899 2C10.8999 2 9.91994 2.59 9.39994 3.52C7.44994 4.49 6.09994 6.5 6.09994 8.82V11.35C6.09994 11.76 5.90994 12.46 5.69994 12.82L4.68994 14.49C4.28994 15.16 4.19994 15.9 4.44994 16.58C4.68994 17.25 5.25994 17.77 5.99994 18.02C7.93994 18.68 9.97994 19 12.0199 19C14.0599 19 16.0999 18.68 18.0399 18.03C18.7399 17.8 19.2799 17.27 19.5399 16.58C19.7999 15.89 19.7299 15.13 19.3399 14.49Z" fill="currentColor"/>
|
||||
<path d="M14.8297 20.01C14.4097 21.17 13.2997 22 11.9997 22C11.2097 22 10.4297 21.68 9.87969 21.11C9.55969 20.81 9.31969 20.41 9.17969 20C9.30969 20.02 9.43969 20.03 9.57969 20.05C9.80969 20.08 10.0497 20.11 10.2897 20.13C10.8597 20.18 11.4397 20.21 12.0197 20.21C12.5897 20.21 13.1597 20.18 13.7197 20.13C13.9297 20.11 14.1397 20.1 14.3397 20.07C14.4997 20.05 14.6597 20.03 14.8297 20.01Z" fill="currentColor"/>
|
||||
</svg>
|
||||
{(unreadCount || 0) > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute', top: 6, right: 6,
|
||||
background: '#ef4444', color: 'white',
|
||||
fontSize: 10, fontWeight: 700,
|
||||
borderRadius: '50%', width: 16, height: 16,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<UserMenu />
|
||||
</header>
|
||||
|
||||
{offline && <ConnectionBanner />}
|
||||
@@ -95,7 +328,6 @@ export default function TableListPage() {
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Zone filter */}
|
||||
<div ref={zoneRef} style={{ position: 'relative' }}>
|
||||
<button
|
||||
className={`filter-tab ${zoneActive ? 'filter-tab--active' : ''}`}
|
||||
@@ -106,16 +338,16 @@ export default function TableListPage() {
|
||||
{zoneOpen && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '110%', right: 0, zIndex: 100,
|
||||
background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', minWidth: 180, padding: 8,
|
||||
background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 12,
|
||||
boxShadow: '0 4px 16px var(--shadow)', minWidth: 180, padding: 8,
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setSelectedZones(new Set())}
|
||||
style={{
|
||||
display: 'block', width: '100%', textAlign: 'left',
|
||||
padding: '12px 14px', borderRadius: 8, fontSize: 15,
|
||||
color: selectedZones.size === 0 ? '#fff' : '#374151',
|
||||
background: selectedZones.size === 0 ? '#4f46e5' : 'transparent',
|
||||
color: selectedZones.size === 0 ? 'var(--primary-fg)' : 'var(--text)',
|
||||
background: selectedZones.size === 0 ? 'var(--primary)' : 'transparent',
|
||||
border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -128,8 +360,8 @@ export default function TableListPage() {
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, width: '100%',
|
||||
textAlign: 'left', padding: '12px 14px', borderRadius: 8, fontSize: 15,
|
||||
color: selectedZones.has(g.id) ? '#fff' : '#374151',
|
||||
background: selectedZones.has(g.id) ? '#4f46e5' : 'transparent',
|
||||
color: selectedZones.has(g.id) ? 'var(--primary-fg)' : 'var(--text)',
|
||||
background: selectedZones.has(g.id) ? 'var(--primary)' : 'transparent',
|
||||
border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -143,8 +375,8 @@ export default function TableListPage() {
|
||||
style={{
|
||||
display: 'block', width: '100%', textAlign: 'left',
|
||||
padding: '12px 14px', borderRadius: 8, fontSize: 15,
|
||||
color: selectedZones.has('none') ? '#fff' : '#374151',
|
||||
background: selectedZones.has('none') ? '#4f46e5' : 'transparent',
|
||||
color: selectedZones.has('none') ? 'var(--primary-fg)' : 'var(--text)',
|
||||
background: selectedZones.has('none') ? 'var(--primary)' : 'transparent',
|
||||
border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -156,19 +388,52 @@ export default function TableListPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-grid">
|
||||
{filtered.map(t => (
|
||||
<TableCard
|
||||
key={t.id}
|
||||
table={t}
|
||||
order={getOrder(t.id)}
|
||||
currentUserId={user?.id}
|
||||
onClick={() => navigate(`/tables/${t.id}`)}
|
||||
/>
|
||||
))}
|
||||
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0, overscrollBehavior: 'contain' }}>
|
||||
<div className="table-grid">
|
||||
{filtered.map(t => {
|
||||
const order = getOrder(t.id)
|
||||
const tableFlags = tableFlagsMap[t.id] || []
|
||||
const grp = groups.find(g => g.id === t.group_id)
|
||||
// Free tables go straight to the item picker; occupied tables go to detail
|
||||
const destination = order
|
||||
? `/tables/${t.id}`
|
||||
: `/tables/${t.id}/add?new=1`
|
||||
return (
|
||||
<TableCard
|
||||
key={t.id}
|
||||
table={t}
|
||||
order={order}
|
||||
isMine={isMyOrder(order)}
|
||||
flags={tableFlags}
|
||||
groupName={grp?.name || ''}
|
||||
onClick={() => navigate(destination)}
|
||||
onLongPress={() => setQuickModal({ table: t, order, flags: tableFlags })}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button className="fab" onClick={load} title="Ανανέωση">↺</button>
|
||||
</div>
|
||||
|
||||
<button className="fab" onClick={load} title="Ανανέωση">↺</button>
|
||||
{showNotifs && (
|
||||
<NotificationDrawer
|
||||
messages={recentMessages || []}
|
||||
onClose={() => setShowNotifs(false)}
|
||||
onAck={ackMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{quickModal && (
|
||||
<TableQuickModal
|
||||
table={quickModal.table}
|
||||
order={quickModal.order}
|
||||
flags={quickModal.flags}
|
||||
onClose={() => setQuickModal(null)}
|
||||
onNavigate={() => navigate(`/tables/${quickModal.table.id}`)}
|
||||
onAction={(key) => handleQuickAction(quickModal.table.id, key)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user