Session 2026-05-30: task detail page overhaul
- New TaskDetail replaces /tasks/:id (was v2 at /requests/:id/v2) - Overview tab: R00 request info, Requested By/Date/Sign Count inline row, Notes, Sign Family, files, amendments - Revisions tab: R01+ from submissions, newest first, same layout as overview, per-entry Amend button - Comments tab: single-line input, post on Enter, delete own comments, Supabase task_comments table - Folder tab: placeholder for file sharing - Tab badges showing revision/comment counts - Amend Request modal: drag-drop file zone, popupOverlayStyle/Surface, Save+Cancel buttons - Request Revision modal for clients on approved/invoiced/paid tasks - JSZip download-all with progress popup for submission files - Upload progress popup for Add Task and amend file uploads - FileAttachment drop zone: 8px radius, transparent bg, label style fix (no all-caps override) - PageLoader on task detail load - task_comments migration with RLS policies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,12 +60,12 @@ export default function FileAttachment({ files, onChange }) {
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<div style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.8, color: 'var(--text-secondary)', marginBottom: 6 }}>
|
||||
Attach Files
|
||||
<span style={{ fontWeight: 400, color: 'var(--text-muted)', marginLeft: 6 }}>
|
||||
<span style={{ fontWeight: 400, color: 'var(--text-muted)', marginLeft: 6, textTransform: 'none', letterSpacing: 0 }}>
|
||||
Up to {MAX_FILES} files · Max {MAX_SIZE_MB} MB each · Any file type
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onDragEnter={handleDragEnter}
|
||||
@@ -74,15 +74,15 @@ export default function FileAttachment({ files, onChange }) {
|
||||
onDrop={handleDrop}
|
||||
style={{
|
||||
border: `2px dashed ${dragging ? 'var(--accent)' : files.length > 0 ? 'var(--accent)' : 'var(--border)'}`,
|
||||
borderRadius: 4, padding: '18px 16px', textAlign: 'center',
|
||||
background: dragging ? 'color-mix(in srgb, var(--accent) 8%, var(--bg))' : 'var(--bg)',
|
||||
transition: 'all 0.15s',
|
||||
borderRadius: 8, padding: '16px', textAlign: 'center',
|
||||
background: dragging ? 'color-mix(in srgb, var(--accent) 8%, var(--bg))' : 'transparent',
|
||||
transition: 'all 160ms', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input type="file" multiple onChange={handleChange} style={{ display: 'none' }} id="req-file-upload" />
|
||||
<label htmlFor="req-file-upload" style={{ cursor: 'pointer' }}>
|
||||
<div style={{ fontSize: 22, marginBottom: 4 }}>{dragging ? '📂' : '📎'}</div>
|
||||
<div style={{ fontWeight: 400, fontSize: 13, color: 'var(--text-primary)' }}>
|
||||
<label htmlFor="req-file-upload" style={{ cursor: 'pointer', textTransform: 'none', letterSpacing: 'normal', fontWeight: 400 }}>
|
||||
<div style={{ fontSize: 20, marginBottom: 4 }}>{dragging ? '📂' : '📎'}</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--text-primary)' }}>
|
||||
{dragging
|
||||
? 'Drop files here'
|
||||
: files.length > 0
|
||||
|
||||
+28
-10
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import PageLoader from './PageLoader';
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { NavLink, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const ICONS = {
|
||||
@@ -41,11 +41,12 @@ function TeamNav({ onNav }) {
|
||||
|
||||
const isCompaniesActive = location.pathname === '/company' && !location.search.includes('tab=users');
|
||||
const isUsersActive = location.pathname === '/company' && location.search.includes('tab=users');
|
||||
const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/requests/');
|
||||
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
{primaryLinks.map(({ to, label, icon }) => (
|
||||
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<NavLink key={to} to={to} onClick={onNav} className={to === '/tasks' ? () => `sidebar-link${isTasksActive ? ' active' : ''}` : ({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
@@ -67,6 +68,8 @@ function TeamNav({ onNav }) {
|
||||
}
|
||||
|
||||
function ClientNav({ onNav }) {
|
||||
const location = useLocation();
|
||||
const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/requests/');
|
||||
const links = [
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||
{ to: '/tasks', label: 'Tasks', icon: ICONS.requests },
|
||||
@@ -77,7 +80,7 @@ function ClientNav({ onNav }) {
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
{links.map(({ to, label, icon }) => (
|
||||
<NavLink key={label} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<NavLink key={label} to={to} onClick={onNav} className={to === '/tasks' ? () => `sidebar-link${isTasksActive ? ' active' : ''}` : ({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
@@ -86,6 +89,8 @@ function ClientNav({ onNav }) {
|
||||
}
|
||||
|
||||
function ExternalNav({ onNav }) {
|
||||
const location = useLocation();
|
||||
const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/requests/');
|
||||
const links = [
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||
{ to: '/tasks', label: 'Tasks', icon: ICONS.requests },
|
||||
@@ -106,7 +111,7 @@ function ExternalNav({ onNav }) {
|
||||
<div className="sidebar-tools-label">Tools</div>
|
||||
</>
|
||||
)}
|
||||
<NavLink to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<NavLink to={to} onClick={onNav} className={to === '/tasks' ? () => `sidebar-link${isTasksActive ? ' active' : ''}` : ({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -140,15 +145,16 @@ export default function Layout({ children }) {
|
||||
const firstName = currentUser?.name?.split(' ')[0] || '';
|
||||
const isProfileRoute = location.pathname === '/profile' || location.pathname.startsWith('/profile/');
|
||||
const isFileSharingRoute = location.pathname === '/file-sharing';
|
||||
const isRequestsRoute = location.pathname === '/tasks' || location.pathname === '/requests' || location.pathname === '/team/tasks';
|
||||
const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isRequestsRoute ? 'Tasks & Projects' : `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}`;
|
||||
const isTaskDetailRoute = location.pathname.startsWith('/requests/');
|
||||
const isRequestsRoute = location.pathname === '/tasks' || location.pathname === '/requests' || location.pathname === '/team/tasks' || isTaskDetailRoute;
|
||||
const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isRequestsRoute && !isTaskDetailRoute ? 'Tasks & Projects' : !isTaskDetailRoute ? `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}` : null;
|
||||
const headerSubtitle = isProfileRoute
|
||||
? 'Account details and security settings.'
|
||||
: isFileSharingRoute
|
||||
? 'Browse, share and manage files.'
|
||||
: isRequestsRoute
|
||||
: isRequestsRoute && !isTaskDetailRoute
|
||||
? 'Browse and manage all tasks and projects.'
|
||||
: "Here's what's happening today.";
|
||||
: isTaskDetailRoute ? null : "Here's what's happening today.";
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
@@ -236,8 +242,20 @@ export default function Layout({ children }) {
|
||||
<main className="main-content">
|
||||
<div className="site-header">
|
||||
<div>
|
||||
<div className="site-header-greeting">{headerTitle}</div>
|
||||
<div className="site-header-sub">{headerSubtitle}</div>
|
||||
{isTaskDetailRoute ? (
|
||||
<div>
|
||||
<Link to="/tasks" className="dashboard-inline-link" style={{ display: 'inline-flex', alignItems: 'center', gap: 1, color: 'var(--text-muted)', textDecoration: 'none' }}>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ width: 24, height: 24, flexShrink: 0 }}><polyline points="10 4 6 8 10 12"/></svg>
|
||||
<span style={{ fontSize: 24, fontWeight: 500, lineHeight: 1.2, letterSpacing: '-0.3px', position: 'relative', top: 2 }}>Tasks & Projects</span>
|
||||
</Link>
|
||||
<div className="site-header-sub" style={{ paddingLeft: 25 }}>Return back to previous page</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="site-header-greeting">{headerTitle}</div>
|
||||
<div className="site-header-sub">{headerSubtitle}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="site-header-right">
|
||||
<div className="site-header-search-wrap">
|
||||
|
||||
+133
-67
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { serviceTypes } from '../data/mockData';
|
||||
import FileAttachment from './FileAttachment';
|
||||
@@ -12,6 +12,7 @@ const emptyForm = (companyId = '') => ({
|
||||
companyId,
|
||||
project: '',
|
||||
serviceType: '',
|
||||
signFamily: '',
|
||||
title: '',
|
||||
deadline: defaultDeadline(),
|
||||
description: '',
|
||||
@@ -39,14 +40,41 @@ export default function RequestForm({
|
||||
error = '',
|
||||
submitLabel = 'Submit Request',
|
||||
initialCompanyId = '',
|
||||
initialValues = null,
|
||||
}) {
|
||||
const [form, setForm] = useState(() => emptyForm(initialCompanyId));
|
||||
const [form, setForm] = useState(() => ({ ...emptyForm(initialCompanyId), ...(initialValues || {}) }));
|
||||
const prevCompanyIdRef = useRef(initialValues?.companyId || initialCompanyId || null);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [existingProjects, setExistingProjects] = useState([]);
|
||||
const [customProjects, setCustomProjects] = useState([]);
|
||||
const [isTypingProject, setIsTypingProject] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState('');
|
||||
const [companyUsers, setCompanyUsers] = useState([]);
|
||||
const [signFamilies, setSignFamilies] = useState([]);
|
||||
const [signCount, setSignCount] = useState('');
|
||||
const [signs, setSigns] = useState([]);
|
||||
|
||||
const isBrandBook = form.serviceType === 'Brand Book';
|
||||
|
||||
useEffect(() => {
|
||||
supabase.from('sign_families').select('name').order('sort_order').then(({ data }) => setSignFamilies((data || []).map(r => r.name)));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrandBook) { setSignCount(''); setSigns([]); }
|
||||
}, [isBrandBook]);
|
||||
|
||||
useEffect(() => {
|
||||
const n = Math.max(0, parseInt(signCount) || 0);
|
||||
setSigns(prev => {
|
||||
if (n < prev.length) return prev.slice(0, n);
|
||||
if (n > prev.length) {
|
||||
const extra = Array.from({ length: n - prev.length }, () => ({ id: crypto.randomUUID(), signName: '', signFamily: '' }));
|
||||
return [...prev, ...extra];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, [signCount]);
|
||||
|
||||
const companyId = form.companyId || initialCompanyId;
|
||||
|
||||
@@ -75,14 +103,19 @@ export default function RequestForm({
|
||||
setCompanyUsers(merged);
|
||||
}
|
||||
});
|
||||
setForm(f => ({ ...f, project: '', requestedBy: '' }));
|
||||
setCustomProjects([]);
|
||||
setIsTypingProject(false);
|
||||
setNewProjectName('');
|
||||
if (companyId !== prevCompanyIdRef.current) {
|
||||
setForm(f => ({ ...f, project: '', requestedBy: '' }));
|
||||
setCustomProjects([]);
|
||||
setIsTypingProject(false);
|
||||
setNewProjectName('');
|
||||
}
|
||||
prevCompanyIdRef.current = companyId;
|
||||
}, [companyId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const set = (field) => (e) => setForm(f => ({ ...f, [field]: e.target.value }));
|
||||
|
||||
const setSign = (id, field, value) => setSigns(prev => prev.map(s => s.id === id ? { ...s, [field]: value } : s));
|
||||
|
||||
const allProjectNames = [
|
||||
...existingProjects.map(p => p.name),
|
||||
...customProjects.filter(name => !existingProjects.some(p => p.name === name)),
|
||||
@@ -108,62 +141,70 @@ export default function RequestForm({
|
||||
setNewProjectName('');
|
||||
};
|
||||
|
||||
const requesterOptions = showRequester ? [
|
||||
...(currentUser ? [{ id: currentUser.id, name: `${currentUser.name} (You)`, email: currentUser.email || '' }] : []),
|
||||
...companyUsers.filter(u => u.id !== currentUser?.id),
|
||||
] : [];
|
||||
|
||||
const showCompanySelect = companies.length > 1 || showRequester;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const requester = requesterOptions.find(u => u.id === form.requestedBy);
|
||||
const requestedByName = requester ? requester.name.replace(' (You)', '') : '';
|
||||
onSubmit({ ...form, companyId, requestedByName }, files, existingProjects);
|
||||
const requestedByName = currentUser?.name || '';
|
||||
onSubmit(
|
||||
{
|
||||
...form,
|
||||
companyId,
|
||||
requestedBy: currentUser?.id || '',
|
||||
requestedByName,
|
||||
signCount: isBrandBook ? (parseInt(signCount) || null) : null,
|
||||
signs: isBrandBook ? signs : [],
|
||||
},
|
||||
files,
|
||||
existingProjects
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{showCompanySelect && (
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Company *</label>
|
||||
<select
|
||||
value={form.companyId}
|
||||
onChange={e => setForm(f => ({ ...f, companyId: e.target.value }))}
|
||||
required
|
||||
style={modalInputStyle}
|
||||
>
|
||||
<option value="">Select company...</option>
|
||||
{companies.map(co => <option key={co.id} value={co.id}>{co.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Project *</label>
|
||||
{isTypingProject ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter project name..."
|
||||
value={newProjectName}
|
||||
onChange={e => setNewProjectName(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }}
|
||||
autoFocus
|
||||
style={{ ...modalInputStyle, flex: 1 }}
|
||||
/>
|
||||
<button type="button" className="btn btn-outline" onClick={handleAddProject} disabled={!newProjectName.trim()}>Add</button>
|
||||
<button type="button" className="btn btn-outline" onClick={() => { setIsTypingProject(false); setNewProjectName(''); }}>Cancel</button>
|
||||
{/* Row 1: Company + Project */}
|
||||
<div className="grid-2">
|
||||
{showCompanySelect ? (
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Company *</label>
|
||||
<select
|
||||
value={form.companyId}
|
||||
onChange={e => setForm(f => ({ ...f, companyId: e.target.value }))}
|
||||
required
|
||||
style={modalInputStyle}
|
||||
>
|
||||
<option value="">Select company...</option>
|
||||
{companies.map(co => <option key={co.id} value={co.id}>{co.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<select value={form.project} onChange={handleProjectSelect} required disabled={showCompanySelect && !companyId} style={modalInputStyle}>
|
||||
<option value="">{showCompanySelect && !companyId ? 'Select company first' : 'Select a project...'}</option>
|
||||
{allProjectNames.map(name => <option key={name} value={name}>{name}</option>)}
|
||||
{(!showCompanySelect || companyId) && <option value="__new__">+ Create new project...</option>}
|
||||
</select>
|
||||
)}
|
||||
) : <div />}
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Project *</label>
|
||||
{isTypingProject ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter project name..."
|
||||
value={newProjectName}
|
||||
onChange={e => setNewProjectName(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }}
|
||||
autoFocus
|
||||
style={{ ...modalInputStyle, flex: 1 }}
|
||||
/>
|
||||
<button type="button" className="btn btn-outline" onClick={handleAddProject} disabled={!newProjectName.trim()}>Add</button>
|
||||
<button type="button" className="btn btn-outline" onClick={() => { setIsTypingProject(false); setNewProjectName(''); }}>Cancel</button>
|
||||
</div>
|
||||
) : (
|
||||
<select value={form.project} onChange={handleProjectSelect} required disabled={showCompanySelect && !companyId} style={modalInputStyle}>
|
||||
<option value="">{showCompanySelect && !companyId ? 'Select company first' : 'Select a project...'}</option>
|
||||
{allProjectNames.map(name => <option key={name} value={name}>{name}</option>)}
|
||||
{(!showCompanySelect || companyId) && <option value="__new__">+ Create new project...</option>}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Service Type + Desired Deadline */}
|
||||
<div className="grid-2">
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Service Type *</label>
|
||||
@@ -178,32 +219,57 @@ export default function RequestForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginTop: -4 }}>
|
||||
<label style={{ ...modalLabelStyle, display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', marginBottom: 0 }}>
|
||||
{/* Row 3: Title/Location + Mark as Hot inline */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12, alignItems: 'end', marginBottom: 16 }}>
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label style={modalLabelStyle}>Title / Location *</label>
|
||||
<input type="text" placeholder="e.g. City, State or Site Name" value={form.title} onChange={set('title')} required style={modalInputStyle} />
|
||||
</div>
|
||||
<label style={{ ...modalLabelStyle, display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginBottom: 4, whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={form.isHot} onChange={e => setForm(f => ({ ...f, isHot: e.target.checked }))} />
|
||||
<span>Mark as Hot</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{showRequester && (
|
||||
{isBrandBook && (
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Requested By *</label>
|
||||
<select value={form.requestedBy} onChange={set('requestedBy')} disabled={!companyId} required style={modalInputStyle}>
|
||||
<option value="">{companyId ? 'Select requester...' : 'Select company first'}</option>
|
||||
{requesterOptions.map(user => (
|
||||
<option key={user.id} value={user.id}>{user.name}{user.email ? ` (${user.email})` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
<label style={modalLabelStyle}>Sign Count *</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
placeholder="How many signs?"
|
||||
value={signCount}
|
||||
onChange={e => setSignCount(e.target.value)}
|
||||
required
|
||||
style={{ ...modalInputStyle, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isBrandBook && signs.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 16 }}>
|
||||
{signs.map((sign, i) => (
|
||||
<div key={sign.id} style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '10px 12px' }}>
|
||||
<div style={{ ...modalLabelStyle, marginBottom: 8 }}>Sign {i + 1}</div>
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
<label style={modalLabelStyle}>Sign Information *</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Main Entry, Drive Thru"
|
||||
value={sign.signName}
|
||||
onChange={e => setSign(sign.id, 'signName', e.target.value)}
|
||||
required
|
||||
style={modalInputStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Request Title *</label>
|
||||
<input type="text" placeholder="e.g. City, State or Site Name" value={form.title} onChange={set('title')} required style={modalInputStyle} />
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Description *</label>
|
||||
<label style={modalLabelStyle}>{isBrandBook ? 'Notes' : 'Description'} *</label>
|
||||
<textarea placeholder="Notes on the request..." value={form.description} onChange={set('description')} style={modalTextAreaStyle} required />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user