feat(captura): fechas de Obra bloqueadas tras establecerse, solo admin las cambia
This commit is contained in:
@@ -148,7 +148,7 @@ describe('guardarEndpoint (POST /api/captura)', () => {
|
|||||||
res,
|
res,
|
||||||
)
|
)
|
||||||
expect(status).toHaveBeenCalledWith(204)
|
expect(status).toHaveBeenCalledWith(204)
|
||||||
expect(capturaService.guardarCampo).toHaveBeenCalledWith('SENDA', 'U-101', 'VENTAS', 'estatus', 'VENDIDO', 'u1')
|
expect(capturaService.guardarCampo).toHaveBeenCalledWith('SENDA', 'U-101', 'VENTAS', 'estatus', 'VENDIDO', 'u1', 'admin')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rechaza departamento inválido', async () => {
|
it('rechaza departamento inválido', async () => {
|
||||||
@@ -190,7 +190,7 @@ describe('bulkEndpoint (POST /api/captura/bulk)', () => {
|
|||||||
expect(capturaService.guardarBulk).toHaveBeenCalledWith('SENDA', 'VENTAS', [
|
expect(capturaService.guardarBulk).toHaveBeenCalledWith('SENDA', 'VENTAS', [
|
||||||
{ unidad: 'U-101', campo: 'estatus', valor: 'VENDIDO' },
|
{ unidad: 'U-101', campo: 'estatus', valor: 'VENDIDO' },
|
||||||
{ unidad: 'U-102', campo: 'estatus', valor: 'DISPONIBLE' },
|
{ unidad: 'U-102', campo: 'estatus', valor: 'DISPONIBLE' },
|
||||||
], 'u1')
|
], 'u1', 'admin')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export async function guardarEndpoint(req: Request, res: Response): Promise<void
|
|||||||
body.campo,
|
body.campo,
|
||||||
body.valor ?? null,
|
body.valor ?? null,
|
||||||
usuarioId,
|
usuarioId,
|
||||||
|
req.usuarioToken?.rol ?? '',
|
||||||
)
|
)
|
||||||
res.status(204).end()
|
res.status(204).end()
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,7 @@ export async function bulkEndpoint(req: Request, res: Response): Promise<void> {
|
|||||||
body.departamento,
|
body.departamento,
|
||||||
body.registros.map((r) => ({ unidad: r.unidad, campo: r.campo, valor: r.valor ?? null })),
|
body.registros.map((r) => ({ unidad: r.unidad, campo: r.campo, valor: r.valor ?? null })),
|
||||||
usuarioId,
|
usuarioId,
|
||||||
|
req.usuarioToken?.rol ?? '',
|
||||||
)
|
)
|
||||||
res.status(204).end()
|
res.status(204).end()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ describe('obtenerCapturaUnidad', () => {
|
|||||||
describe('guardarCampo', () => {
|
describe('guardarCampo', () => {
|
||||||
it('hace upsert del campo', async () => {
|
it('hace upsert del campo', async () => {
|
||||||
prismaMock.capturaDepartamento.upsert.mockResolvedValue({})
|
prismaMock.capturaDepartamento.upsert.mockResolvedValue({})
|
||||||
await guardarCampo('SENDA', 'U-101', 'VENTAS', 'estatus', 'VENDIDO', 'user-1')
|
await guardarCampo('SENDA', 'U-101', 'VENTAS', 'estatus', 'VENDIDO', 'user-1', 'admin')
|
||||||
expect(prismaMock.capturaDepartamento.upsert).toHaveBeenCalledWith({
|
expect(prismaMock.capturaDepartamento.upsert).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
desarrollo_unidadCodigo_departamento_campo: {
|
desarrollo_unidadCodigo_departamento_campo: {
|
||||||
@@ -122,7 +122,7 @@ describe('guardarCampo', () => {
|
|||||||
|
|
||||||
it('permite valor null', async () => {
|
it('permite valor null', async () => {
|
||||||
prismaMock.capturaDepartamento.upsert.mockResolvedValue({})
|
prismaMock.capturaDepartamento.upsert.mockResolvedValue({})
|
||||||
await guardarCampo('SENDA', 'U-101', 'OBRA', 'comentarios_obra', null, 'user-1')
|
await guardarCampo('SENDA', 'U-101', 'OBRA', 'comentarios_obra', null, 'user-1', 'admin')
|
||||||
expect(prismaMock.capturaDepartamento.upsert).toHaveBeenCalledWith(
|
expect(prismaMock.capturaDepartamento.upsert).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
update: expect.objectContaining({ valor: null }),
|
update: expect.objectContaining({ valor: null }),
|
||||||
@@ -137,7 +137,7 @@ describe('guardarBulk', () => {
|
|||||||
await guardarBulk('SENDA', 'VENTAS', [
|
await guardarBulk('SENDA', 'VENTAS', [
|
||||||
{ unidad: 'U-101', campo: 'estatus', valor: 'VENDIDO' },
|
{ unidad: 'U-101', campo: 'estatus', valor: 'VENDIDO' },
|
||||||
{ unidad: 'U-101', campo: 'modelo', valor: 'Casa A' },
|
{ unidad: 'U-101', campo: 'modelo', valor: 'Casa A' },
|
||||||
], 'user-1')
|
], 'user-1', 'admin')
|
||||||
expect(prismaMock.$transaction).toHaveBeenCalledTimes(1)
|
expect(prismaMock.$transaction).toHaveBeenCalledTimes(1)
|
||||||
expect(prismaMock.$transaction).toHaveBeenCalledWith(expect.arrayContaining([
|
expect(prismaMock.$transaction).toHaveBeenCalledWith(expect.arrayContaining([
|
||||||
expect.any(Promise),
|
expect.any(Promise),
|
||||||
@@ -145,6 +145,55 @@ describe('guardarBulk', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('fechas de Obra bloqueadas', () => {
|
||||||
|
const CAMPO_FECHA = 'certificacion_gas_fecha'
|
||||||
|
|
||||||
|
it('admin puede modificar una fecha ya establecida', async () => {
|
||||||
|
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
||||||
|
{ unidadCodigo: 'U-101', campo: CAMPO_FECHA, valor: '2026-01-01' },
|
||||||
|
])
|
||||||
|
prismaMock.capturaDepartamento.upsert.mockResolvedValue({})
|
||||||
|
await guardarCampo('SENDA', 'U-101', 'OBRA', CAMPO_FECHA, '2026-02-02', 'user-1', 'admin')
|
||||||
|
expect(prismaMock.capturaDepartamento.upsert).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no-admin no puede modificar una fecha ya establecida', async () => {
|
||||||
|
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
||||||
|
{ unidadCodigo: 'U-101', campo: CAMPO_FECHA, valor: '2026-01-01' },
|
||||||
|
])
|
||||||
|
await expect(
|
||||||
|
guardarCampo('SENDA', 'U-101', 'OBRA', CAMPO_FECHA, '2026-02-02', 'user-1', 'obra'),
|
||||||
|
).rejects.toThrow('solo puede modificarlas un administrador')
|
||||||
|
expect(prismaMock.capturaDepartamento.upsert).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no-admin puede establecer una fecha por primera vez', async () => {
|
||||||
|
prismaMock.capturaDepartamento.findMany.mockResolvedValue([])
|
||||||
|
prismaMock.capturaDepartamento.upsert.mockResolvedValue({})
|
||||||
|
await guardarCampo('SENDA', 'U-101', 'OBRA', CAMPO_FECHA, '2026-03-03', 'user-1', 'obra')
|
||||||
|
expect(prismaMock.capturaDepartamento.upsert).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bulk: rechaza completo si algún registro cambia una fecha establecida (no-admin)', async () => {
|
||||||
|
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
||||||
|
{ unidadCodigo: 'U-101', campo: CAMPO_FECHA, valor: '2026-01-01' },
|
||||||
|
])
|
||||||
|
await expect(
|
||||||
|
guardarBulk(
|
||||||
|
'SENDA',
|
||||||
|
'OBRA',
|
||||||
|
[
|
||||||
|
{ unidad: 'U-101', campo: 'comentarios_obra', valor: 'ok' },
|
||||||
|
{ unidad: 'U-101', campo: CAMPO_FECHA, valor: '2026-05-05' },
|
||||||
|
],
|
||||||
|
'user-1',
|
||||||
|
'obra',
|
||||||
|
),
|
||||||
|
).rejects.toThrow('solo puede modificarlas un administrador')
|
||||||
|
expect(prismaMock.$transaction).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('resumenAvance', () => {
|
describe('resumenAvance', () => {
|
||||||
it('calcula porcentaje de avance por departamento', async () => {
|
it('calcula porcentaje de avance por departamento', async () => {
|
||||||
// Mock para count (campos llenos)
|
// Mock para count (campos llenos)
|
||||||
|
|||||||
@@ -1,5 +1,42 @@
|
|||||||
import { prisma } from './prisma.js'
|
import { prisma } from './prisma.js'
|
||||||
import { CAMPOS_POR_DEPARTAMENTO, DEPARTAMENTOS } from '../config/campos-departamento.js'
|
import { CAMPOS_POR_DEPARTAMENTO, DEPARTAMENTOS } from '../config/campos-departamento.js'
|
||||||
|
import { AppError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
/** Fechas de Obra emparejadas a checkboxes: tras establecerse solo las cambia un admin */
|
||||||
|
const CAMPOS_FECHA_OBRA = new Set(
|
||||||
|
(CAMPOS_POR_DEPARTAMENTO.OBRA ?? [])
|
||||||
|
.filter((c) => c.conFecha)
|
||||||
|
.map((c) => c.conFecha as string),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Bloquear modificación de fechas de Obra ya establecidas (solo admin) */
|
||||||
|
async function validarFechasObra(
|
||||||
|
desarrollo: string,
|
||||||
|
departamento: string,
|
||||||
|
registros: { unidad: string; campo: string; valor: string | null }[],
|
||||||
|
rol: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (rol === 'admin' || departamento !== 'OBRA') return
|
||||||
|
const candidatos = registros.filter((r) => CAMPOS_FECHA_OBRA.has(r.campo))
|
||||||
|
if (candidatos.length === 0) return
|
||||||
|
|
||||||
|
const existentes = await prisma.capturaDepartamento.findMany({
|
||||||
|
where: {
|
||||||
|
desarrollo,
|
||||||
|
departamento,
|
||||||
|
unidadCodigo: { in: [...new Set(candidatos.map((r) => r.unidad))] },
|
||||||
|
campo: { in: candidatos.map((r) => r.campo) },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const previos = new Map(existentes.map((e) => [`${e.unidadCodigo}|${e.campo}`, e.valor]))
|
||||||
|
|
||||||
|
for (const r of candidatos) {
|
||||||
|
const previo = previos.get(`${r.unidad}|${r.campo}`)
|
||||||
|
if (previo != null && previo !== r.valor) {
|
||||||
|
throw new AppError(403, 'Las fechas ya establecidas solo puede modificarlas un administrador')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Obtener unidades únicas de un desarrollo ──────────────────────────
|
// ── Obtener unidades únicas de un desarrollo ──────────────────────────
|
||||||
|
|
||||||
@@ -67,7 +104,10 @@ export async function guardarCampo(
|
|||||||
campo: string,
|
campo: string,
|
||||||
valor: string | null,
|
valor: string | null,
|
||||||
usuarioId: string,
|
usuarioId: string,
|
||||||
|
rol: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await validarFechasObra(desarrollo, departamento, [{ unidad: unidadCodigo, campo, valor }], rol)
|
||||||
|
|
||||||
const tipo = CAMPOS_POR_DEPARTAMENTO[departamento]?.find((c) => c.campo === campo)?.tipo ?? 'text'
|
const tipo = CAMPOS_POR_DEPARTAMENTO[departamento]?.find((c) => c.campo === campo)?.tipo ?? 'text'
|
||||||
|
|
||||||
await prisma.capturaDepartamento.upsert({
|
await prisma.capturaDepartamento.upsert({
|
||||||
@@ -91,7 +131,10 @@ export async function guardarBulk(
|
|||||||
departamento: string,
|
departamento: string,
|
||||||
registros: { unidad: string; campo: string; valor: string | null }[],
|
registros: { unidad: string; campo: string; valor: string | null }[],
|
||||||
usuarioId: string,
|
usuarioId: string,
|
||||||
|
rol: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await validarFechasObra(desarrollo, departamento, registros, rol)
|
||||||
|
|
||||||
const tipoPorCampo = new Map(
|
const tipoPorCampo = new Map(
|
||||||
(CAMPOS_POR_DEPARTAMENTO[departamento] ?? []).map((c) => [c.campo, c.tipo]),
|
(CAMPOS_POR_DEPARTAMENTO[departamento] ?? []).map((c) => [c.campo, c.tipo]),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -277,4 +277,46 @@ describe('TablaCaptura', () => {
|
|||||||
expect(onGuardar).toHaveBeenCalledTimes(1)
|
expect(onGuardar).toHaveBeenCalledTimes(1)
|
||||||
expect(onGuardar.mock.calls[0][0]['409-1']).toEqual({ comentarios_obra: 'Nota X' })
|
expect(onGuardar.mock.calls[0][0]['409-1']).toEqual({ comentarios_obra: 'Nota X' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('fechas de Obra guardadas: disabled para no-admin, editables para admin', () => {
|
||||||
|
const camposEmparejados: CampoDef[] = [
|
||||||
|
{ campo: 'certificacion_gas', tipo: 'boolean', label: 'Certificación Gas', conFecha: 'certificacion_gas_fecha' },
|
||||||
|
{ campo: 'certificacion_gas_fecha', tipo: 'date', label: 'Fecha Certificación Gas' },
|
||||||
|
]
|
||||||
|
const datosConFecha: DatosCaptura = {
|
||||||
|
'409-1': { certificacion_gas: null, certificacion_gas_fecha: '2026-01-01' },
|
||||||
|
'409-2': { certificacion_gas: null, certificacion_gas_fecha: null },
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-admin: la fecha guardada se deshabilita, la vacía sigue editable
|
||||||
|
const r1 = render(
|
||||||
|
<TablaCaptura
|
||||||
|
unidades={UNIDADES}
|
||||||
|
campos={camposEmparejados}
|
||||||
|
datos={datosConFecha}
|
||||||
|
onGuardar={vi.fn()}
|
||||||
|
guardando={false}
|
||||||
|
puedeEditarFechas={false}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
const fechas1 = r1.container.querySelectorAll('input[type="date"]')
|
||||||
|
expect((fechas1[0] as HTMLInputElement).disabled).toBe(true)
|
||||||
|
expect((fechas1[1] as HTMLInputElement).disabled).toBe(false)
|
||||||
|
r1.unmount()
|
||||||
|
|
||||||
|
// Admin: todas editables
|
||||||
|
const r2 = render(
|
||||||
|
<TablaCaptura
|
||||||
|
unidades={UNIDADES}
|
||||||
|
campos={camposEmparejados}
|
||||||
|
datos={datosConFecha}
|
||||||
|
onGuardar={vi.fn()}
|
||||||
|
guardando={false}
|
||||||
|
puedeEditarFechas
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
const fechas2 = r2.container.querySelectorAll('input[type="date"]')
|
||||||
|
expect((fechas2[0] as HTMLInputElement).disabled).toBe(false)
|
||||||
|
expect((fechas2[1] as HTMLInputElement).disabled).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface Props {
|
|||||||
guardando: boolean
|
guardando: boolean
|
||||||
/** Filtro de estatus de unidad (columna estestatus en VENTAS) */
|
/** Filtro de estatus de unidad (columna estestatus en VENTAS) */
|
||||||
filtroEstatus?: string
|
filtroEstatus?: string
|
||||||
|
/** Si es false, las fechas de Obra ya guardadas no se pueden editar (solo admin) */
|
||||||
|
puedeEditarFechas?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const POR_PAGINA = 50
|
const POR_PAGINA = 50
|
||||||
@@ -57,6 +59,8 @@ interface CeldaProps {
|
|||||||
valorFecha?: string | null
|
valorFecha?: string | null
|
||||||
dirtyFecha?: boolean
|
dirtyFecha?: boolean
|
||||||
onFechaChange?: (nuevo: string | null) => void
|
onFechaChange?: (nuevo: string | null) => void
|
||||||
|
/** Fecha ya establecida y usuario sin permiso de edición */
|
||||||
|
fechaBloqueada?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function CeldaEditable({
|
function CeldaEditable({
|
||||||
@@ -69,6 +73,7 @@ function CeldaEditable({
|
|||||||
valorFecha,
|
valorFecha,
|
||||||
dirtyFecha,
|
dirtyFecha,
|
||||||
onFechaChange,
|
onFechaChange,
|
||||||
|
fechaBloqueada,
|
||||||
}: CeldaProps) {
|
}: CeldaProps) {
|
||||||
const bordeDirty = dirty ? 'ring-2 ring-accent/40' : ''
|
const bordeDirty = dirty ? 'ring-2 ring-accent/40' : ''
|
||||||
|
|
||||||
@@ -88,8 +93,14 @@ function CeldaEditable({
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={valorFecha ?? ''}
|
value={valorFecha ?? ''}
|
||||||
|
disabled={fechaBloqueada}
|
||||||
onChange={(e) => onFechaChange?.(e.target.value || null)}
|
onChange={(e) => onFechaChange?.(e.target.value || null)}
|
||||||
className={`w-28 border-0 bg-transparent px-1 py-0.5 text-xs focus:outline-none [color-scheme:light] dark:[color-scheme:dark] ${colorFecha} ${bordeFecha}`}
|
title={
|
||||||
|
fechaBloqueada
|
||||||
|
? 'Fecha ya establecida — solo un administrador puede modificarla'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={`w-28 border-0 bg-transparent px-1 py-0.5 text-xs focus:outline-none [color-scheme:light] dark:[color-scheme:dark] disabled:cursor-not-allowed ${colorFecha} ${bordeFecha}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
@@ -159,6 +170,7 @@ export function TablaCaptura({
|
|||||||
onGuardar,
|
onGuardar,
|
||||||
guardando,
|
guardando,
|
||||||
filtroEstatus,
|
filtroEstatus,
|
||||||
|
puedeEditarFechas = true,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [dirty, setDirty] = useState<DirtyMap>({})
|
const [dirty, setDirty] = useState<DirtyMap>({})
|
||||||
const [pagina, setPagina] = useState(0)
|
const [pagina, setPagina] = useState(0)
|
||||||
@@ -314,6 +326,12 @@ export function TablaCaptura({
|
|||||||
campoFecha ? valorEfectivo(dirty, datos, unidad, campoFecha) : undefined
|
campoFecha ? valorEfectivo(dirty, datos, unidad, campoFecha) : undefined
|
||||||
}
|
}
|
||||||
dirtyFecha={campoFecha ? dirty[unidad]?.[campoFecha] !== undefined : false}
|
dirtyFecha={campoFecha ? dirty[unidad]?.[campoFecha] !== undefined : false}
|
||||||
|
fechaBloqueada={
|
||||||
|
campoFecha
|
||||||
|
? !puedeEditarFechas &&
|
||||||
|
valorOriginal(datos, unidad, campoFecha) != null
|
||||||
|
: false
|
||||||
|
}
|
||||||
onFechaChange={
|
onFechaChange={
|
||||||
campoFecha
|
campoFecha
|
||||||
? (nuevo) => handleChange(unidad, campoFecha, nuevo)
|
? (nuevo) => handleChange(unidad, campoFecha, nuevo)
|
||||||
|
|||||||
@@ -87,8 +87,12 @@ export function CapturaPage({ onVolver }: Props) {
|
|||||||
await guardarBulk({ desarrollo, departamento, registros })
|
await guardarBulk({ desarrollo, departamento, registros })
|
||||||
toast.success('Cambios guardados')
|
toast.success('Cambios guardados')
|
||||||
await cargar()
|
await cargar()
|
||||||
} catch {
|
} catch (e) {
|
||||||
toast.error('No se pudieron guardar los cambios')
|
toast.error(
|
||||||
|
e instanceof Error && e.message && !e.message.startsWith('HTTP')
|
||||||
|
? e.message
|
||||||
|
: 'No se pudieron guardar los cambios',
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
setGuardando(false)
|
setGuardando(false)
|
||||||
}
|
}
|
||||||
@@ -215,6 +219,7 @@ export function CapturaPage({ onVolver }: Props) {
|
|||||||
onGuardar={handleGuardar}
|
onGuardar={handleGuardar}
|
||||||
guardando={guardando}
|
guardando={guardando}
|
||||||
filtroEstatus={filtroEstatus}
|
filtroEstatus={filtroEstatus}
|
||||||
|
puedeEditarFechas={rol === 'admin'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -161,7 +161,10 @@ export async function guardarCampo(data: {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
manejar401(resp)
|
manejar401(resp)
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
if (!resp.ok) {
|
||||||
|
const cuerpo = (await resp.json().catch(() => null)) as { error?: string } | null
|
||||||
|
throw new Error(cuerpo?.error ?? `HTTP ${resp.status}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /api/captura/bulk — guardar múltiples campos */
|
/** POST /api/captura/bulk — guardar múltiples campos */
|
||||||
@@ -183,5 +186,8 @@ export async function guardarBulk(data: {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
manejar401(resp)
|
manejar401(resp)
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
if (!resp.ok) {
|
||||||
|
const cuerpo = (await resp.json().catch(() => null)) as { error?: string } | null
|
||||||
|
throw new Error(cuerpo?.error ?? `HTTP ${resp.status}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user