chore(style): codebase em-dash sweep + minor layout polish
Replaces every em-dash and en-dash with regular ASCII hyphens across comments, JSX strings, and dev-facing logs. Mostly cosmetic but stops the inconsistent mix that crept in over the last few months (some files used em-dashes in comments, others didn't, some used both). Bundles two small dashboard-layout tweaks that touch a couple of already-modified files: - (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6 pb-6 so page content sits closer to the topbar. - Sidebar now receives the ports list it needs for the footer port switcher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,7 +87,7 @@ export function AuditLogList() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
// Filter state — debounce text inputs.
|
||||
// Filter state - debounce text inputs.
|
||||
const [search, setSearch] = useState('');
|
||||
const [entityType, setEntityType] = useState<string>('all');
|
||||
const [action, setAction] = useState<string>('all');
|
||||
@@ -215,7 +215,7 @@ export function AuditLogList() {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className="text-xs text-muted-foreground">—</span>;
|
||||
return <span className="text-xs text-muted-foreground">-</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -245,7 +245,7 @@ export function AuditLogList() {
|
||||
<PageHeader
|
||||
title="Audit Log"
|
||||
eyebrow="Admin"
|
||||
description="Every state change in this port — fully searchable."
|
||||
description="Every state change in this port - fully searchable."
|
||||
variant="gradient"
|
||||
/>
|
||||
|
||||
|
||||
@@ -59,12 +59,7 @@ const FIELD_TYPE_LABELS: Record<string, string> = {
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function CustomFieldForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
field,
|
||||
onSuccess,
|
||||
}: CustomFieldFormProps) {
|
||||
export function CustomFieldForm({ open, onOpenChange, field, onSuccess }: CustomFieldFormProps) {
|
||||
const isEdit = !!field;
|
||||
|
||||
// Form state
|
||||
@@ -72,9 +67,7 @@ export function CustomFieldForm({
|
||||
const [fieldName, setFieldName] = useState(field?.fieldName ?? '');
|
||||
const [fieldLabel, setFieldLabel] = useState(field?.fieldLabel ?? '');
|
||||
const [fieldType, setFieldType] = useState(field?.fieldType ?? 'text');
|
||||
const [selectOptions, setSelectOptions] = useState<string[]>(
|
||||
field?.selectOptions ?? [],
|
||||
);
|
||||
const [selectOptions, setSelectOptions] = useState<string[]>(field?.selectOptions ?? []);
|
||||
const [newOption, setNewOption] = useState('');
|
||||
const [isRequired, setIsRequired] = useState(field?.isRequired ?? false);
|
||||
const [sortOrder, setSortOrder] = useState(field?.sortOrder ?? 0);
|
||||
@@ -169,13 +162,11 @@ export function CustomFieldForm({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEdit ? 'Edit Custom Field' : 'New Custom Field'}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{isEdit ? 'Edit Custom Field' : 'New Custom Field'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5 py-2">
|
||||
{/* Entity Type — create only */}
|
||||
{/* Entity Type - create only */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cf-entity-type">Entity Type</Label>
|
||||
{isEdit ? (
|
||||
@@ -198,7 +189,7 @@ export function CustomFieldForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Field Name — create only */}
|
||||
{/* Field Name - create only */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cf-field-name">
|
||||
Field Name
|
||||
@@ -232,7 +223,7 @@ export function CustomFieldForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Field Type — create only */}
|
||||
{/* Field Type - create only */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cf-field-type">Field Type</Label>
|
||||
{isEdit ? (
|
||||
@@ -260,7 +251,7 @@ export function CustomFieldForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Select Options — visible when fieldType = 'select' */}
|
||||
{/* Select Options - visible when fieldType = 'select' */}
|
||||
{fieldType === 'select' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Options</Label>
|
||||
@@ -302,11 +293,7 @@ export function CustomFieldForm({
|
||||
{/* Is Required */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="cf-is-required">Required field</Label>
|
||||
<Switch
|
||||
id="cf-is-required"
|
||||
checked={isRequired}
|
||||
onCheckedChange={setIsRequired}
|
||||
/>
|
||||
<Switch id="cf-is-required" checked={isRequired} onCheckedChange={setIsRequired} />
|
||||
</div>
|
||||
|
||||
{/* Sort Order */}
|
||||
|
||||
@@ -11,13 +11,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetFooter,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { TEMPLATE_VARIABLES } from '@/lib/pdf/tiptap-to-pdfme';
|
||||
|
||||
@@ -61,20 +55,13 @@ interface TemplateFormProps {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function TemplateForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
template,
|
||||
onSuccess,
|
||||
}: TemplateFormProps) {
|
||||
export function TemplateForm({ open, onOpenChange, template, onSuccess }: TemplateFormProps) {
|
||||
const isEdit = !!template;
|
||||
|
||||
const [name, setName] = useState(template?.name ?? '');
|
||||
const [type, setType] = useState(template?.templateType ?? 'other');
|
||||
const [contentJson, setContentJson] = useState(
|
||||
template?.content
|
||||
? JSON.stringify(template.content, null, 2)
|
||||
: EMPTY_DOC,
|
||||
template?.content ? JSON.stringify(template.content, null, 2) : EMPTY_DOC,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -86,7 +73,7 @@ export function TemplateForm({
|
||||
setJsonError(null);
|
||||
return true;
|
||||
} catch {
|
||||
setJsonError('Invalid JSON — check syntax.');
|
||||
setJsonError('Invalid JSON - check syntax.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -115,8 +102,7 @@ export function TemplateForm({
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : 'Something went wrong';
|
||||
const message = err instanceof Error ? err.message : 'Something went wrong';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -127,9 +113,7 @@ export function TemplateForm({
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-full max-w-2xl overflow-y-auto sm:max-w-2xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{isEdit ? 'Edit Template' : 'New Document Template'}
|
||||
</SheetTitle>
|
||||
<SheetTitle>{isEdit ? 'Edit Template' : 'New Document Template'}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-6 space-y-6">
|
||||
@@ -145,7 +129,7 @@ export function TemplateForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Type — only on create */}
|
||||
{/* Type - only on create */}
|
||||
{!isEdit && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="template-type">Document Type</Label>
|
||||
@@ -166,15 +150,11 @@ export function TemplateForm({
|
||||
|
||||
{/* TipTap JSON Content */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="template-content">
|
||||
Document Content (TipTap JSON)
|
||||
</Label>
|
||||
<Label htmlFor="template-content">Document Content (TipTap JSON)</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Paste or edit TipTap JSON. Use{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">
|
||||
{'{{variable.key}}'}
|
||||
</code>{' '}
|
||||
tokens for dynamic content.
|
||||
<code className="rounded bg-muted px-1 text-xs">{'{{variable.key}}'}</code> tokens for
|
||||
dynamic content.
|
||||
</p>
|
||||
<textarea
|
||||
id="template-content"
|
||||
@@ -187,9 +167,7 @@ export function TemplateForm({
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs shadow-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{jsonError && (
|
||||
<p className="text-xs text-destructive">{jsonError}</p>
|
||||
)}
|
||||
{jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Available Variables Reference */}
|
||||
@@ -200,19 +178,15 @@ export function TemplateForm({
|
||||
<div className="mt-3 grid grid-cols-1 gap-1 sm:grid-cols-2">
|
||||
{TEMPLATE_VARIABLES.map((v) => (
|
||||
<div key={v.key} className="text-xs">
|
||||
<code className="rounded bg-muted px-1">
|
||||
{`{{${v.key}}}`}
|
||||
</code>{' '}
|
||||
<span className="text-muted-foreground">— {v.label}</span>
|
||||
<code className="rounded bg-muted px-1">{`{{${v.key}}}`}</code>{' '}
|
||||
<span className="text-muted-foreground">- {v.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{error && (
|
||||
<p className="rounded bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
<p className="rounded bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<SheetFooter>
|
||||
@@ -225,11 +199,7 @@ export function TemplateForm({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !!jsonError}>
|
||||
{loading
|
||||
? 'Saving…'
|
||||
: isEdit
|
||||
? 'Save Changes'
|
||||
: 'Create Template'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Template'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -9,12 +9,7 @@ import { PageHeader } from '@/components/shared/page-header';
|
||||
import { ConfirmationDialog } from '@/components/shared/confirmation-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { TemplateForm } from './template-form';
|
||||
import { TemplateVersionHistory } from './template-version-history';
|
||||
@@ -57,9 +52,7 @@ export function TemplateList() {
|
||||
const fetchTemplates = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiFetch<{ data: AdminTemplate[] }>(
|
||||
'/api/v1/admin/templates',
|
||||
);
|
||||
const res = await apiFetch<{ data: AdminTemplate[] }>('/api/v1/admin/templates');
|
||||
setTemplates(res.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -122,9 +115,7 @@ export function TemplateList() {
|
||||
accessorKey: 'version',
|
||||
header: 'Version',
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
v{row.original.version}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">v{row.original.version}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -151,10 +142,7 @@ export function TemplateList() {
|
||||
header: '',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<TemplatePreview
|
||||
content={row.original.content}
|
||||
templateName={row.original.name}
|
||||
/>
|
||||
<TemplatePreview content={row.original.content} templateName={row.original.name} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -177,9 +165,7 @@ export function TemplateList() {
|
||||
title={row.original.isActive ? 'Deactivate' : 'Activate'}
|
||||
onClick={() => handleToggleActive(row.original)}
|
||||
>
|
||||
<span className="text-xs">
|
||||
{row.original.isActive ? 'Off' : 'On'}
|
||||
</span>
|
||||
<span className="text-xs">{row.original.isActive ? 'Off' : 'On'}</span>
|
||||
</Button>
|
||||
<ConfirmationDialog
|
||||
trigger={
|
||||
@@ -233,9 +219,7 @@ export function TemplateList() {
|
||||
<Sheet open={historyOpen} onOpenChange={setHistoryOpen}>
|
||||
<SheetContent className="w-full max-w-xl sm:max-w-xl overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
Version History — {historyTemplate?.name}
|
||||
</SheetTitle>
|
||||
<SheetTitle>Version History - {historyTemplate?.name}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-6">
|
||||
{historyTemplate && (
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Eye, ExternalLink } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { TEMPLATE_VARIABLES } from '@/lib/pdf/tiptap-to-pdfme';
|
||||
|
||||
@@ -24,9 +19,7 @@ export function TemplatePreview({ content, templateName }: TemplatePreviewProps)
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Build sample data from TEMPLATE_VARIABLES examples
|
||||
const sampleData = Object.fromEntries(
|
||||
TEMPLATE_VARIABLES.map((v) => [v.key, v.example]),
|
||||
);
|
||||
const sampleData = Object.fromEntries(TEMPLATE_VARIABLES.map((v) => [v.key, v.example]));
|
||||
|
||||
async function handlePreview() {
|
||||
if (!content) {
|
||||
@@ -74,14 +67,9 @@ export function TemplatePreview({ content, templateName }: TemplatePreviewProps)
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle>Preview — {templateName}</DialogTitle>
|
||||
<DialogTitle>Preview - {templateName}</DialogTitle>
|
||||
{pdfBase64 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleOpenInNewTab}
|
||||
className="mr-6"
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={handleOpenInNewTab} className="mr-6">
|
||||
<ExternalLink className="mr-1.5 h-3.5 w-3.5" />
|
||||
Open in new tab
|
||||
</Button>
|
||||
@@ -100,9 +88,7 @@ export function TemplatePreview({ content, templateName }: TemplatePreviewProps)
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div className="rounded bg-destructive/10 p-4 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
<div className="rounded bg-destructive/10 p-4 text-sm text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{pdfBase64 && !loading && (
|
||||
|
||||
@@ -117,7 +117,7 @@ export function InvitationsManager() {
|
||||
{invites.map((i) => (
|
||||
<tr key={i.id} className="border-t">
|
||||
<td className="px-3 py-2 font-medium">{i.email}</td>
|
||||
<td className="px-3 py-2 text-muted-foreground">{i.name ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-muted-foreground">{i.name ?? '-'}</td>
|
||||
<td className="px-3 py-2 text-muted-foreground">
|
||||
{i.isSuperAdmin ? 'Super admin' : 'Standard user'}
|
||||
</td>
|
||||
@@ -163,7 +163,7 @@ export function InvitationsManager() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -160,7 +160,7 @@ function SettingsBlock({ scope, title, description, showUseGlobal }: SettingsBlo
|
||||
Enable AI receipt parsing for this port
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Off by default. Receipts are read on-device using Tesseract.js — accurate enough for
|
||||
Off by default. Receipts are read on-device using Tesseract.js - accurate enough for
|
||||
most receipts and incurs no AI cost. Turning this on lets the configured provider
|
||||
re-parse receipts server-side for higher accuracy on hard-to-read images.
|
||||
</p>
|
||||
@@ -214,7 +214,7 @@ function SettingsBlock({ scope, title, description, showUseGlobal }: SettingsBlo
|
||||
id={`apiKey-${scope}`}
|
||||
type={showKey ? 'text' : 'password'}
|
||||
autoComplete="off"
|
||||
placeholder={hasKey ? '•••••• (saved — leave blank to keep)' : 'sk-…'}
|
||||
placeholder={hasKey ? '•••••• (saved - leave blank to keep)' : 'sk-…'}
|
||||
value={apiKey}
|
||||
onChange={(e) => {
|
||||
setApiKey(e.target.value);
|
||||
|
||||
@@ -33,7 +33,7 @@ const statusVariant: Record<JobStatus, 'default' | 'secondary' | 'destructive' |
|
||||
};
|
||||
|
||||
function formatDate(ts: number | undefined): string {
|
||||
if (!ts) return '—';
|
||||
if (!ts) return '-';
|
||||
return new Date(ts).toLocaleString();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function truncateId(id: string): string {
|
||||
}
|
||||
|
||||
function truncateReason(reason: string | undefined): string {
|
||||
if (!reason) return '—';
|
||||
if (!reason) return '-';
|
||||
return reason.length > 80 ? `${reason.slice(0, 80)}…` : reason;
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ export function QueueDetailTable({ queueName }: QueueDetailTableProps) {
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
{total} total jobs — page {page} of {totalPages}
|
||||
{total} total jobs - page {page} of {totalPages}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
||||
@@ -95,7 +95,7 @@ export function RoleList() {
|
||||
accessorKey: 'description',
|
||||
header: 'Description',
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground text-sm">{row.original.description ?? '—'}</span>
|
||||
<span className="text-muted-foreground text-sm">{row.original.description ?? '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -363,7 +363,7 @@ export function SettingsManager() {
|
||||
);
|
||||
void saveSetting(setting.key, parsed);
|
||||
} catch {
|
||||
// invalid JSON — do nothing
|
||||
// invalid JSON - do nothing
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -108,7 +108,7 @@ export function UserCard({ user, onEdit, onRemove, isRemoving }: UserCardProps)
|
||||
<span aria-hidden className="block h-9 w-9 shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* Email subtitle — only when display name is shown as title */}
|
||||
{/* Email subtitle - only when display name is shown as title */}
|
||||
{user.displayName && user.displayName !== user.email ? (
|
||||
<p className="mt-0.5 inline-flex items-center gap-1 truncate text-sm text-muted-foreground">
|
||||
<Mail className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" aria-hidden />
|
||||
|
||||
@@ -57,7 +57,7 @@ export function WebhookDeliveryLog({ webhookId }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
void load(page);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [webhookId, page]);
|
||||
|
||||
if (loading && deliveries.length === 0) {
|
||||
@@ -87,13 +87,9 @@ export function WebhookDeliveryLog({ webhookId }: Props) {
|
||||
<TableRow key={d.id}>
|
||||
<TableCell className="font-mono text-xs">{d.eventType}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_VARIANTS[d.status] ?? 'outline'}>
|
||||
{d.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{d.responseStatus ?? '—'}
|
||||
<Badge variant={STATUS_VARIANTS[d.status] ?? 'outline'}>{d.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{d.responseStatus ?? '-'}</TableCell>
|
||||
<TableCell className="text-sm">{d.attempt}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{d.deliveredAt
|
||||
|
||||
Reference in New Issue
Block a user