feat(ui): add Realisasi Manager component for multiple realisasi CRUD
New Component - RealisasiManager: - Add modal form for create/edit realisasi - Input fields: Jumlah (Rp), Tanggal, Keterangan/Uraian - Display list of existing realisasi with edit/delete actions - Summary cards showing: Anggaran, Total Realisasi, Sisa Anggaran, Persentase - Color-coded percentage badges (teal ≥100%, blue ≥80%, yellow ≥60%, red <60%) - Auto-reload data after create/update/delete operations Features: - Multiple realisasi per APBDes item - Each realisasi has its own description (uraian) - Date picker for realisasi tanggal - Format currency in IDR (Rupiah) - Responsive table layout - Empty state when no realisasi exists Integration: - Integrated with existing state.realisasi CRUD functions - Auto-calculate totalRealisasi and persentase (handled by backend) - Display realisasi items from API response - Works with existing APBDes detail page UI/UX: - Clean modal design with form validation - Summary cards with color-coded backgrounds - Icon indicators for date and currency - Confirmation dialog before delete - Loading states during async operations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -0,0 +1,407 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
Title,
|
||||||
|
Table,
|
||||||
|
TableThead,
|
||||||
|
TableTbody,
|
||||||
|
TableTr,
|
||||||
|
TableTh,
|
||||||
|
TableTd,
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Modal,
|
||||||
|
Divider,
|
||||||
|
Loader,
|
||||||
|
Center,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconPlus,
|
||||||
|
IconEdit,
|
||||||
|
IconTrash,
|
||||||
|
IconCalendar,
|
||||||
|
IconCoin,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
|
interface RealisasiManagerProps {
|
||||||
|
itemId: string;
|
||||||
|
itemKode: string;
|
||||||
|
itemUraian: string;
|
||||||
|
itemAnggaran: number;
|
||||||
|
itemTotalRealisasi: number;
|
||||||
|
itemPersentase: number;
|
||||||
|
realisasiItems: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RealisasiManager({
|
||||||
|
itemId,
|
||||||
|
itemKode,
|
||||||
|
itemUraian,
|
||||||
|
itemAnggaran,
|
||||||
|
itemTotalRealisasi,
|
||||||
|
itemPersentase,
|
||||||
|
realisasiItems,
|
||||||
|
}: RealisasiManagerProps) {
|
||||||
|
const state = useProxy(apbdes);
|
||||||
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
jumlah: 0,
|
||||||
|
tanggal: new Date().toISOString().split('T')[0], // YYYY-MM-DD format for input
|
||||||
|
keterangan: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
jumlah: 0,
|
||||||
|
tanggal: new Date().toISOString().split('T')[0],
|
||||||
|
keterangan: '',
|
||||||
|
});
|
||||||
|
setEditingId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenCreate = () => {
|
||||||
|
resetForm();
|
||||||
|
setModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEdit = (realisasi: any) => {
|
||||||
|
const tanggal = new Date(realisasi.tanggal);
|
||||||
|
const tanggalStr = tanggal.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
jumlah: realisasi.jumlah,
|
||||||
|
tanggal: tanggalStr,
|
||||||
|
keterangan: realisasi.keterangan || '',
|
||||||
|
});
|
||||||
|
setEditingId(realisasi.id);
|
||||||
|
setModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (formData.jumlah <= 0) {
|
||||||
|
return toast.warn('Jumlah realisasi harus lebih dari 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (editingId) {
|
||||||
|
// Update existing realisasi
|
||||||
|
const success = await state.realisasi.update(editingId, {
|
||||||
|
jumlah: formData.jumlah,
|
||||||
|
tanggal: new Date(formData.tanggal).toISOString(),
|
||||||
|
keterangan: formData.keterangan,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Realisasi berhasil diperbarui');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new realisasi
|
||||||
|
const success = await state.realisasi.create(itemId, {
|
||||||
|
jumlah: formData.jumlah,
|
||||||
|
tanggal: new Date(formData.tanggal).toISOString(),
|
||||||
|
keterangan: formData.keterangan,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Realisasi berhasil ditambahkan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setModalOpened(false);
|
||||||
|
resetForm();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error saving realisasi:', error);
|
||||||
|
toast.error(error?.message || 'Gagal menyimpan realisasi');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (realisasiId: string) => {
|
||||||
|
if (!confirm('Apakah Anda yakin ingin menghapus realisasi ini?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const success = await state.realisasi.delete(realisasiId);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Realisasi berhasil dihapus');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error deleting realisasi:', error);
|
||||||
|
toast.error(error?.message || 'Gagal menghapus realisasi');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRupiah = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSisaAnggaran = () => {
|
||||||
|
return itemAnggaran - itemTotalRealisasi;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPersentaseColor = (persen: number) => {
|
||||||
|
if (persen >= 100) return 'teal';
|
||||||
|
if (persen >= 80) return 'blue';
|
||||||
|
if (persen >= 60) return 'yellow';
|
||||||
|
return 'red';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper withBorder p="md" radius="md" mt="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Title order={6}>
|
||||||
|
{itemKode} - {itemUraian}
|
||||||
|
</Title>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Kelola realisasi untuk item ini
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
onClick={handleOpenCreate}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
Tambah Realisasi
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<Group grow mb="md">
|
||||||
|
<Paper withBorder p="md" radius="md" bg="blue.0">
|
||||||
|
<Text fz="xs" c="blue.9" fw={600}>
|
||||||
|
ANGGARAN
|
||||||
|
</Text>
|
||||||
|
<Text fz="lg" c="blue.9" fw={700}>
|
||||||
|
{formatRupiah(itemAnggaran)}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md" bg="teal.0">
|
||||||
|
<Text fz="xs" c="teal.9" fw={600}>
|
||||||
|
TOTAL REALISASI
|
||||||
|
</Text>
|
||||||
|
<Text fz="lg" c="teal.9" fw={700}>
|
||||||
|
{formatRupiah(itemTotalRealisasi)}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md" bg={getSisaAnggaran() >= 0 ? 'green.0' : 'red.0'}>
|
||||||
|
<Text fz="xs" c={getSisaAnggaran() >= 0 ? 'green.9' : 'red.9'} fw={600}>
|
||||||
|
SISA ANGGARAN
|
||||||
|
</Text>
|
||||||
|
<Text fz="lg" c={getSisaAnggaran() >= 0 ? 'green.9' : 'red.9'} fw={700}>
|
||||||
|
{formatRupiah(getSisaAnggaran())}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md" bg={getPersentaseColor(itemPersentase) + '.0'}>
|
||||||
|
<Text fz="xs" c={getPersentaseColor(itemPersentase) + '.9'} fw={600}>
|
||||||
|
PERSENTASE
|
||||||
|
</Text>
|
||||||
|
<Text fz="lg" c={getPersentaseColor(itemPersentase) + '.9'} fw={700}>
|
||||||
|
{itemPersentase.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Realisasi List */}
|
||||||
|
{realisasiItems && realisasiItems.length > 0 ? (
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} mb="xs">
|
||||||
|
Daftar Realisasi ({realisasiItems.length})
|
||||||
|
</Text>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table striped highlightOnHover fz="sm">
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Tanggal</TableTh>
|
||||||
|
<TableTh>Uraian</TableTh>
|
||||||
|
<TableTh ta="right">Jumlah</TableTh>
|
||||||
|
<TableTh ta="center">Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{realisasiItems.map((realisasi) => (
|
||||||
|
<TableTr key={realisasi.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconCalendar size={16} />
|
||||||
|
<Text fz="sm">{formatDate(realisasi.tanggal)}</Text>
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm">{realisasi.keterangan || '-'}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta="right">
|
||||||
|
<Text fz="sm" fw={600} c="blue">
|
||||||
|
{formatRupiah(realisasi.jumlah)}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta="center">
|
||||||
|
<Group gap="xs" justify="center">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleOpenEdit(realisasi)}
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(realisasi.id)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Center py="xl">
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Belum ada realisasi untuk item ini
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
Klik tombol "Tambah Realisasi" untuk menambahkan
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal Create/Edit */}
|
||||||
|
<Modal
|
||||||
|
opened={modalOpened}
|
||||||
|
onClose={() => {
|
||||||
|
setModalOpened(false);
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Title order={5}>
|
||||||
|
{editingId ? 'Edit Realisasi' : 'Tambah Realisasi Baru'}
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Info Item */}
|
||||||
|
<Paper p="sm" bg="gray.0" radius="md">
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
Item: {itemKode} - {itemUraian}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
Anggaran: {formatRupiah(itemAnggaran)}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
Sudah terealisasi: {formatRupiah(itemTotalRealisasi)}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label="Jumlah Realisasi (Rp)"
|
||||||
|
value={formData.jumlah}
|
||||||
|
onChange={(val) => setFormData({ ...formData, jumlah: Number(val) || 0 })}
|
||||||
|
leftSection={<IconCoin size={16} />}
|
||||||
|
thousandSeparator
|
||||||
|
min={0}
|
||||||
|
step={100000}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Tanggal Realisasi"
|
||||||
|
type="date"
|
||||||
|
value={formData.tanggal}
|
||||||
|
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
|
||||||
|
leftSection={<IconCalendar size={18} />}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Keterangan / Uraian"
|
||||||
|
placeholder="Contoh: Penyaluran BLT Tahap 1"
|
||||||
|
value={formData.keterangan}
|
||||||
|
onChange={(e) => setFormData({ ...formData, keterangan: e.target.value })}
|
||||||
|
description="Deskripsi singkat tentang realisasi ini"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider my="xs" />
|
||||||
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => {
|
||||||
|
setModalOpened(false);
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={loading}
|
||||||
|
color="blue"
|
||||||
|
leftSection={editingId ? <IconEdit size={16} /> : <IconPlus size={16} />}
|
||||||
|
>
|
||||||
|
{editingId ? 'Perbarui' : 'Tambah'} Realisasi
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import apbdes from '../../../_state/landing-page/apbdes';
|
import apbdes from '../../../_state/landing-page/apbdes';
|
||||||
|
import RealisasiManager from './RealisasiManager';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -191,48 +192,60 @@ function DetailAPBDes() {
|
|||||||
|
|
||||||
{/* Tabel Items */}
|
{/* Tabel Items */}
|
||||||
{data.items && data.items.length > 0 ? (
|
{data.items && data.items.length > 0 ? (
|
||||||
<Paper withBorder p="md" radius="md">
|
<Stack gap="md">
|
||||||
<Text fz="lg" fw="bold" mb="sm">
|
<Text fz="lg" fw="bold">
|
||||||
Rincian Pendapatan & Belanja ({data.items.length} item)
|
Rincian Pendapatan & Belanja ({data.items.length} item)
|
||||||
</Text>
|
</Text>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Table striped highlightOnHover>
|
||||||
<Table striped highlightOnHover>
|
<TableThead>
|
||||||
<TableThead>
|
<TableTr>
|
||||||
<TableTr>
|
<TableTh>Uraian</TableTh>
|
||||||
<TableTh>Uraian</TableTh>
|
<TableTh>Anggaran (Rp)</TableTh>
|
||||||
<TableTh>Anggaran (Rp)</TableTh>
|
<TableTh>Realisasi (Rp)</TableTh>
|
||||||
<TableTh>Realisasi (Rp)</TableTh>
|
<TableTh>Selisih (Rp)</TableTh>
|
||||||
<TableTh>Selisih (Rp)</TableTh>
|
<TableTh>Persentase (%)</TableTh>
|
||||||
<TableTh>Persentase (%)</TableTh>
|
</TableTr>
|
||||||
</TableTr>
|
</TableThead>
|
||||||
</TableThead>
|
<TableTbody>
|
||||||
<TableTbody>
|
{[...data.items]
|
||||||
{[...data.items] // Create a new array before sorting
|
.sort((a, b) => a.kode.localeCompare(b.kode))
|
||||||
.sort((a, b) => a.kode.localeCompare(b.kode))
|
.map((item) => (
|
||||||
.map((item) => (
|
<TableTr key={item.id}>
|
||||||
<TableTr key={item.id}>
|
<TableTd style={getIndent(item.level)}>
|
||||||
<TableTd style={getIndent(item.level)}>
|
<Group>
|
||||||
<Group>
|
<Text fw={item.level === 1 ? 'bold' : 'normal'}>{item.kode}</Text>
|
||||||
<Text fw={item.level === 1 ? 'bold' : 'normal'}>{item.kode}</Text>
|
<Text fz="sm" c="dimmed">{item.uraian}</Text>
|
||||||
<Text fz="sm" c="dimmed">{item.uraian}</Text>
|
</Group>
|
||||||
</Group>
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd>{item.anggaran.toLocaleString('id-ID')}</TableTd>
|
||||||
<TableTd>{item.anggaran.toLocaleString('id-ID')}</TableTd>
|
<TableTd>{item.totalRealisasi.toLocaleString('id-ID')}</TableTd>
|
||||||
<TableTd>{item.totalRealisasi.toLocaleString('id-ID')}</TableTd>
|
<TableTd>
|
||||||
<TableTd>
|
<Text c={item.selisih >= 0 ? 'green' : 'red'}>
|
||||||
<Text c={item.selisih >= 0 ? 'green' : 'red'}>
|
{item.selisih.toLocaleString('id-ID')}
|
||||||
{item.selisih.toLocaleString('id-ID')}
|
</Text>
|
||||||
</Text>
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd>
|
||||||
<TableTd>
|
<Text fw={500}>{item.persentase.toFixed(2)}%</Text>
|
||||||
<Text fw={500}>{item.persentase.toFixed(2)}%</Text>
|
</TableTd>
|
||||||
</TableTd>
|
</TableTr>
|
||||||
</TableTr>
|
))}
|
||||||
))}
|
</TableTbody>
|
||||||
</TableTbody>
|
</Table>
|
||||||
</Table>
|
|
||||||
</Box>
|
{/* Realisasi Manager untuk setiap item */}
|
||||||
</Paper>
|
{data.items.map((item: any) => (
|
||||||
|
<RealisasiManager
|
||||||
|
key={item.id}
|
||||||
|
itemId={item.id}
|
||||||
|
itemKode={item.kode}
|
||||||
|
itemUraian={item.uraian}
|
||||||
|
itemAnggaran={item.anggaran}
|
||||||
|
itemTotalRealisasi={item.totalRealisasi}
|
||||||
|
itemPersentase={item.persentase}
|
||||||
|
realisasiItems={item.realisasiItems || []}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Text>Belum ada data item</Text>
|
<Text>Belum ada data item</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user