chore: upload parte 1 - logica
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
This commit is contained in:
+582
@@ -0,0 +1,582 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Cpu,
|
||||
Package,
|
||||
AlertTriangle,
|
||||
Euro,
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
FileText,
|
||||
ExternalLink,
|
||||
ClipboardList,
|
||||
ShoppingCart,
|
||||
} from "lucide-react";
|
||||
import { useTableSort } from "@/hooks/use-table-sort";
|
||||
import { SortableTableHead } from "@/components/sortable-table-head";
|
||||
import { formatCurrency } from "@/lib/format";
|
||||
import { toast } from "sonner";
|
||||
import type { Component } from "@/lib/types";
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
|
||||
interface OrderRow {
|
||||
id: number;
|
||||
po_number: string;
|
||||
status: string;
|
||||
order_date: string;
|
||||
due_date: string | null;
|
||||
customer_id: number;
|
||||
customer_name: string;
|
||||
customer_code: string;
|
||||
total_ordered: number;
|
||||
total_shipped: number;
|
||||
total_remaining: number;
|
||||
total_value: number;
|
||||
item_count: number;
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
stats: {
|
||||
total_components: number;
|
||||
total_inventory_value: number;
|
||||
low_stock_count: number;
|
||||
total_products: number;
|
||||
active_products: number;
|
||||
draft_products: number;
|
||||
active_orders: number;
|
||||
in_production_orders: number;
|
||||
confirmed_orders: number;
|
||||
shipped_orders: number;
|
||||
};
|
||||
low_stock_items: Component[];
|
||||
category_breakdown: { name: string; count: number; total_stock: number }[];
|
||||
recent_orders: OrderRow[];
|
||||
}
|
||||
|
||||
const statusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
|
||||
draft: { label: "Draft", variant: "secondary" },
|
||||
confirmed: { label: "Confirmed", variant: "default" },
|
||||
in_production: { label: "In Production", variant: "outline" },
|
||||
shipped: { label: "Shipped", variant: "default" },
|
||||
completed: { label: "Completed", variant: "default" },
|
||||
cancelled: { label: "Cancelled", variant: "destructive" },
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||
const [poQuantities, setPoQuantities] = useState<Record<number, number>>({});
|
||||
const [generatingPo, setGeneratingPo] = useState(false);
|
||||
const { data, isLoading } = useSWR<DashboardData>("/api/dashboard", fetcher);
|
||||
const { sorted: sortedLowStock, sort: lowStockSort, toggleSort: toggleLowStockSort } = useTableSort(data?.low_stock_items ?? []);
|
||||
const { sorted: sortedOrders, sort: ordersSort, toggleSort: toggleOrdersSort } = useTableSort(data?.recent_orders ?? []);
|
||||
|
||||
function toggleItem(id: number) {
|
||||
setSelectedItems((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAll(items: Component[]) {
|
||||
if (selectedItems.size === items.length) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
setSelectedItems(new Set(items.map((i) => i.id)));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGeneratePo(lowStockItems: Component[]) {
|
||||
const selected = lowStockItems.filter((i) => selectedItems.has(i.id));
|
||||
if (selected.length === 0) return;
|
||||
|
||||
setGeneratingPo(true);
|
||||
try {
|
||||
// Fetch suppliers to resolve IDs from names
|
||||
const suppRes = await fetch("/api/suppliers");
|
||||
const suppliers = await suppRes.json();
|
||||
const supplierMap: Record<string, { id: number; name: string }> = {};
|
||||
for (const s of Array.isArray(suppliers) ? suppliers : []) {
|
||||
supplierMap[s.name] = { id: s.id, name: s.name };
|
||||
}
|
||||
|
||||
// Group items by supplier
|
||||
const grouped: Record<string, { supplier_id: number; supplier_name: string; items: Record<string, unknown>[] }> = {};
|
||||
for (const item of selected) {
|
||||
const supplierName = item.supplier || "Unknown Supplier";
|
||||
if (!grouped[supplierName]) {
|
||||
const sup = supplierMap[supplierName];
|
||||
if (!sup) {
|
||||
toast.error(`Supplier "${supplierName}" not found. Please add it first.`);
|
||||
setGeneratingPo(false);
|
||||
return;
|
||||
}
|
||||
grouped[supplierName] = {
|
||||
supplier_id: sup.id,
|
||||
supplier_name: sup.name,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
const qty = poQuantities[item.id] || Math.max(1, item.min_stock_level - item.stock_quantity);
|
||||
grouped[supplierName].items.push({
|
||||
component_id: item.id,
|
||||
part_number: item.part_number,
|
||||
component_name: item.name,
|
||||
supplier_part_number: item.supplier_part_number || "",
|
||||
quantity: qty,
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
const orders = Object.values(grouped);
|
||||
const res = await fetch("/api/supplier-pos", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orders }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to create POs");
|
||||
|
||||
const created = await res.json();
|
||||
const count = Array.isArray(created) ? created.length : 1;
|
||||
toast.success(`${count} purchase order(s) created in Draft status`);
|
||||
setSelectedItems(new Set());
|
||||
setPoQuantities({});
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to generate POs");
|
||||
} finally {
|
||||
setGeneratingPo(false);
|
||||
}
|
||||
}
|
||||
|
||||
const stats = data?.stats;
|
||||
|
||||
if (isLoading || !data) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 lg:p-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Overview of your component inventory and products
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="py-6">
|
||||
<div className="h-16 animate-pulse rounded bg-muted" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 lg:p-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Overview of your component inventory and products
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Cpu className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Components</p>
|
||||
<p className="text-2xl font-bold">{stats.total_components}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-accent/10">
|
||||
<Euro className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Inventory Value</p>
|
||||
<p className="text-2xl font-bold font-mono">
|
||||
{formatCurrency(stats.total_inventory_value)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-warning/10">
|
||||
<AlertTriangle className="h-5 w-5 text-warning" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Low Stock Alerts</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.low_stock_count}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Package className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Products</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.total_products}
|
||||
{stats.active_products > 0 && (
|
||||
<span className="ml-2 text-sm font-normal text-muted-foreground">
|
||||
({stats.active_products} active)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10">
|
||||
<ClipboardList className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Active Orders</p>
|
||||
<p className="text-2xl font-bold">{stats.active_orders}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* Low Stock Alerts */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<AlertTriangle className="h-4 w-4 text-warning" />
|
||||
Low Stock Alerts
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/components?lowStock=true">
|
||||
View All
|
||||
<ArrowRight className="ml-1 h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sortedLowStock.length === 0 ? (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<TrendingUp className="h-8 w-8 text-accent mb-2" />
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
All stock levels are healthy
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No components are below their minimum stock level
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* Generate PO action bar */}
|
||||
{selectedItems.size > 0 && (
|
||||
<div className="flex items-center justify-between rounded-md border border-primary/20 bg-primary/5 px-4 py-2.5">
|
||||
<span className="text-sm font-medium text-primary">
|
||||
{selectedItems.size} component(s) selected
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleGeneratePo(data?.low_stock_items ?? [])}
|
||||
disabled={generatingPo}
|
||||
>
|
||||
<ShoppingCart className="mr-1.5 h-3.5 w-3.5" />
|
||||
{generatingPo ? "Generating..." : "Generate PO"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40px]">
|
||||
<Checkbox
|
||||
checked={sortedLowStock.length > 0 && selectedItems.size === sortedLowStock.slice(0, 8).length}
|
||||
onCheckedChange={() => toggleAll(sortedLowStock.slice(0, 8))}
|
||||
/>
|
||||
</TableHead>
|
||||
<SortableTableHead column="part_number" label="Part Number" sort={lowStockSort} onToggle={toggleLowStockSort} />
|
||||
<SortableTableHead column="name" label="Name" sort={lowStockSort} onToggle={toggleLowStockSort} />
|
||||
<SortableTableHead column="supplier" label="Supplier" sort={lowStockSort} onToggle={toggleLowStockSort} />
|
||||
<SortableTableHead column="stock_quantity" label="Stock" sort={lowStockSort} onToggle={toggleLowStockSort} className="text-right" />
|
||||
<SortableTableHead column="min_stock_level" label="Min Level" sort={lowStockSort} onToggle={toggleLowStockSort} className="text-right" />
|
||||
<TableHead>Level</TableHead>
|
||||
<TableHead className="text-right w-[110px]">Order Qty</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedLowStock.slice(0, 8).map((item) => {
|
||||
const pct =
|
||||
item.min_stock_level > 0
|
||||
? Math.min(
|
||||
(item.stock_quantity / item.min_stock_level) * 100,
|
||||
100
|
||||
)
|
||||
: 0;
|
||||
const isSelected = selectedItems.has(item.id);
|
||||
const deficit = Math.max(1, item.min_stock_level - item.stock_quantity);
|
||||
return (
|
||||
<TableRow key={item.id} className={isSelected ? "bg-primary/5" : ""}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => toggleItem(item.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs font-medium">
|
||||
{item.part_number}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-sm">
|
||||
{item.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{item.supplier || <span className="italic">No supplier</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-semibold text-destructive">
|
||||
{item.stock_quantity}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{item.min_stock_level}
|
||||
</TableCell>
|
||||
<TableCell className="w-[100px]">
|
||||
<Progress
|
||||
value={pct}
|
||||
className="h-2 [&>div]:bg-destructive"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{isSelected ? (
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="h-7 w-24 text-xs font-mono text-right ml-auto"
|
||||
value={poQuantities[item.id] ?? deficit}
|
||||
onChange={(e) =>
|
||||
setPoQuantities((prev) => ({
|
||||
...prev,
|
||||
[item.id]: Math.max(1, Number(e.target.value) || 1),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground font-mono">{deficit}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Purchase Orders Status */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<FileText className="h-4 w-4 text-primary" />
|
||||
Purchase Order Status
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/customers">
|
||||
View All
|
||||
<ArrowRight className="ml-1 h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sortedOrders.length === 0 ? (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<ClipboardList className="h-8 w-8 text-muted-foreground mb-2" />
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
No active purchase orders
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Purchase orders from customers will appear here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<SortableTableHead column="po_number" label="PO Number" sort={ordersSort} onToggle={toggleOrdersSort} />
|
||||
<SortableTableHead column="customer_name" label="Customer" sort={ordersSort} onToggle={toggleOrdersSort} />
|
||||
<SortableTableHead column="status" label="Status" sort={ordersSort} onToggle={toggleOrdersSort} />
|
||||
<SortableTableHead column="order_date" label="Order Date" sort={ordersSort} onToggle={toggleOrdersSort} />
|
||||
<SortableTableHead column="due_date" label="Due Date" sort={ordersSort} onToggle={toggleOrdersSort} />
|
||||
<SortableTableHead column="total_value" label="Value" sort={ordersSort} onToggle={toggleOrdersSort} className="text-right" />
|
||||
<SortableTableHead column="total_ordered" label="Ordered" sort={ordersSort} onToggle={toggleOrdersSort} className="text-right" />
|
||||
<SortableTableHead column="total_shipped" label="Shipped" sort={ordersSort} onToggle={toggleOrdersSort} className="text-right" />
|
||||
<SortableTableHead column="total_remaining" label="Remaining" sort={ordersSort} onToggle={toggleOrdersSort} className="text-right" />
|
||||
<TableHead className="w-[140px]">Fulfillment</TableHead>
|
||||
<TableHead className="w-[60px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedOrders.map((order) => {
|
||||
const ordered = Number(order.total_ordered);
|
||||
const shipped = Number(order.total_shipped);
|
||||
const remaining = Number(order.total_remaining);
|
||||
const pct =
|
||||
ordered > 0 ? Math.round((shipped / ordered) * 100) : 0;
|
||||
const sc = statusConfig[order.status] || statusConfig.draft;
|
||||
const isOverdue =
|
||||
order.due_date &&
|
||||
new Date(order.due_date) < new Date() &&
|
||||
pct < 100;
|
||||
|
||||
return (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-mono text-sm font-medium">
|
||||
<Link
|
||||
href={`/customers/${order.customer_id}/orders/${order.id}`}
|
||||
className="flex items-center gap-1.5 text-primary hover:underline"
|
||||
>
|
||||
{order.po_number}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
{order.customer_name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{order.customer_code}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={sc.variant} className="text-xs">
|
||||
{sc.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{new Date(order.order_date).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{order.due_date ? (
|
||||
<span
|
||||
className={
|
||||
isOverdue
|
||||
? "font-medium text-destructive"
|
||||
: "text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{new Date(order.due_date).toLocaleDateString()}
|
||||
{isOverdue && " (overdue)"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">
|
||||
{formatCurrency(Number(order.total_value))}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">
|
||||
{ordered}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm text-accent">
|
||||
{shipped}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm font-semibold">
|
||||
<span
|
||||
className={
|
||||
remaining > 0
|
||||
? "text-warning"
|
||||
: "text-accent"
|
||||
}
|
||||
>
|
||||
{remaining}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-mono text-muted-foreground">
|
||||
{shipped}/{ordered}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
pct === 100
|
||||
? "font-semibold text-accent"
|
||||
: "text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{pct}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={pct} className="h-1.5" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={`/customers/${order.customer_id}/orders/${order.id}`}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Open order details</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user