feat(tickets): expose normalized values for dynamic columns
This commit is contained in:
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticket\Resources\AdminApp;
|
namespace App\Domains\Ticket\Resources\AdminApp;
|
||||||
|
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
|
||||||
use App\Domains\Purchase\Models\PurchaseItem;
|
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use App\Domains\Ticket\Resources\TicketResource;
|
use App\Domains\Ticket\Resources\TicketResource;
|
||||||
|
use App\Domains\Ticket\Services\AdminAppTicketRowService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/** @mixin Ticket */
|
/** @mixin Ticket */
|
||||||
@@ -14,74 +13,13 @@ class AdminAppTicketResource extends TicketResource
|
|||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
$purchaseItem = $this->sourcePurchaseItem();
|
$rowService = app(AdminAppTicketRowService::class);
|
||||||
|
$details = $rowService->details($this->resource);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...parent::toArray($request),
|
...parent::toArray($request),
|
||||||
'source_purchase_id' => $this->source_purchase_id,
|
...$details,
|
||||||
'order_number' => $this->source_purchase_id,
|
'values' => $rowService->values($this->resource, $details),
|
||||||
'product' => $purchaseItem?->item_nombre
|
|
||||||
?? $this->sourceCatalogItem?->nombre
|
|
||||||
?? $this->name,
|
|
||||||
'amount' => $purchaseItem?->precio_unitario,
|
|
||||||
'client' => $this->sourcePurchase?->nombre_apellido ?? $this->user?->nombre_apellido,
|
|
||||||
'date' => $this->sourcePurchase?->created_at,
|
|
||||||
'status' => $this->status,
|
|
||||||
'scanned_by' => $this->scannerUser?->nombre_apellido,
|
|
||||||
'variant_properties' => $this->variantProperties(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sourcePurchaseItem(): ?PurchaseItem
|
|
||||||
{
|
|
||||||
return $this->sourcePurchase?->items->first(function (PurchaseItem $item): bool {
|
|
||||||
if ($this->source_variant_id !== null) {
|
|
||||||
return $item->source_variant_id === $this->source_variant_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $item->source_catalog_item_id === $this->source_catalog_item_id
|
|
||||||
&& $item->source_variant_id === null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{
|
|
||||||
* code: string,
|
|
||||||
* label: string,
|
|
||||||
* values: list<array{value: string, label: string}>
|
|
||||||
* }>
|
|
||||||
*/
|
|
||||||
private function variantProperties(): array
|
|
||||||
{
|
|
||||||
$variant = $this->sourceVariant;
|
|
||||||
|
|
||||||
if ($variant === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$itemAttributes = $variant->definitions
|
|
||||||
->map(fn ($definition) => $definition->itemAttribute)
|
|
||||||
->filter()
|
|
||||||
->merge($variant->catalogItem?->itemAttributes ?? collect())
|
|
||||||
->unique('id')
|
|
||||||
->values();
|
|
||||||
|
|
||||||
return $variant->selectionOptions($itemAttributes)
|
|
||||||
->map(function (array $selection, string $attributeCode) use ($itemAttributes): array {
|
|
||||||
$itemAttribute = $itemAttributes->first(
|
|
||||||
fn (ItemAttribute $itemAttribute): bool => $itemAttribute->attribute?->codigo
|
|
||||||
=== $attributeCode,
|
|
||||||
);
|
|
||||||
$values = array_is_list($selection) ? $selection : [$selection];
|
|
||||||
|
|
||||||
return [
|
|
||||||
'code' => $attributeCode,
|
|
||||||
'label' => $itemAttribute?->attribute?->nombre
|
|
||||||
?? ($attributeCode === 'event_date' ? 'Fecha' : $attributeCode),
|
|
||||||
'values' => array_values($values),
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,11 @@
|
|||||||
namespace App\Domains\Ticket\Services;
|
namespace App\Domains\Ticket\Services;
|
||||||
|
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use App\Domains\Ticket\Resources\AdminApp\AdminAppTicketResource;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class AdminAppTicketReportService
|
class AdminAppTicketReportService
|
||||||
{
|
{
|
||||||
private const CATEGORY_PRESENTATIONS = [
|
public function __construct(private readonly AdminAppTicketRowService $rowService) {}
|
||||||
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null],
|
|
||||||
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null],
|
|
||||||
'entradas' => ['category' => null, 'product' => 'product', 'type' => null],
|
|
||||||
'comidas' => ['category' => 'Comida', 'product' => 'event_date', 'type' => 'horario'],
|
|
||||||
'comida' => ['category' => null, 'product' => 'event_date', 'type' => 'horario'],
|
|
||||||
'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color'],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, Ticket> $tickets
|
* @param Collection<int, Ticket> $tickets
|
||||||
@@ -23,90 +15,21 @@ class AdminAppTicketReportService
|
|||||||
*/
|
*/
|
||||||
public function rows(Collection $tickets): Collection
|
public function rows(Collection $tickets): Collection
|
||||||
{
|
{
|
||||||
return $tickets->values()->map(fn (Ticket $ticket): array => $this->row($ticket));
|
return $this->rowService->rows($tickets);
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
|
||||||
private function row(Ticket $ticket): array
|
|
||||||
{
|
|
||||||
$data = (new AdminAppTicketResource($ticket))->resolve();
|
|
||||||
$presentation = $this->presentation($data);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'order_number' => $data['order_number'],
|
|
||||||
'category' => $presentation['category'],
|
|
||||||
'product' => $presentation['product'],
|
|
||||||
'type' => $presentation['type'],
|
|
||||||
'amount' => $data['amount'] === null ? null : (float) $data['amount'],
|
|
||||||
'client' => $data['client'] ?? 'Sin nombre',
|
|
||||||
'ticket' => $data['ticket'],
|
|
||||||
'date' => $data['date'],
|
|
||||||
'status' => $this->statusLabel($data['status']),
|
|
||||||
'scanned_by' => $data['scanned_by'] ?? '-',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $ticket
|
* @param Collection<int, array<string, mixed>> $rows
|
||||||
* @return array{category: string, product: string, type: string}
|
* @param list<array<string, mixed>> $columns
|
||||||
|
* @return Collection<int, array<string, string>>
|
||||||
*/
|
*/
|
||||||
private function presentation(array $ticket): array
|
public function displayRows(Collection $rows, array $columns, string $timeZone): Collection
|
||||||
{
|
{
|
||||||
$sourceCategory = trim((string) ($ticket['category'] ?? '')) ?: '-';
|
return $this->rowService->displayRows($rows, $columns, $timeZone);
|
||||||
$configuration = self::CATEGORY_PRESENTATIONS[mb_strtolower($sourceCategory)] ?? null;
|
|
||||||
|
|
||||||
if ($configuration === null) {
|
|
||||||
return [
|
|
||||||
'category' => $sourceCategory,
|
|
||||||
'product' => (string) ($ticket['product'] ?: $ticket['name'] ?: '-'),
|
|
||||||
'type' => $this->allPropertyLabels($ticket) ?: '-',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'category' => $configuration['category'] ?? $sourceCategory,
|
|
||||||
'product' => $configuration['product'] === 'product'
|
|
||||||
? (string) ($ticket['product'] ?: $ticket['name'] ?: '-')
|
|
||||||
: ($this->propertyLabels($ticket, $configuration['product']) ?: '-'),
|
|
||||||
'type' => $configuration['type'] === null
|
|
||||||
? '-'
|
|
||||||
: ($this->propertyLabels($ticket, $configuration['type']) ?: '-'),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $ticket */
|
public function displayValue(mixed $value, string $type, string $timeZone): string
|
||||||
private function propertyLabels(array $ticket, string $code): string
|
|
||||||
{
|
{
|
||||||
$property = collect($ticket['variant_properties'] ?? [])->firstWhere('code', $code);
|
return $this->rowService->displayValue($value, $type, $timeZone);
|
||||||
$labels = collect($property['values'] ?? [])->pluck('label')->filter();
|
|
||||||
|
|
||||||
if ($code === 'event_date') {
|
|
||||||
$labels = $labels->map(function (string $label): string {
|
|
||||||
[$day, $month] = array_pad(explode('/', $label), 2, null);
|
|
||||||
|
|
||||||
return $day !== null && $month !== null ? "{$day}/{$month}" : $label;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $labels->implode(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param array<string, mixed> $ticket */
|
|
||||||
private function allPropertyLabels(array $ticket): string
|
|
||||||
{
|
|
||||||
return collect($ticket['variant_properties'] ?? [])
|
|
||||||
->flatMap(fn (array $property): array => $property['values'] ?? [])
|
|
||||||
->pluck('label')
|
|
||||||
->filter()
|
|
||||||
->implode(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function statusLabel(string $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
Ticket::STATUS_USED => 'Usado',
|
|
||||||
Ticket::STATUS_EXPIRED => 'Vencido',
|
|
||||||
default => 'Activo',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
217
app/Domains/Ticket/Services/AdminAppTicketRowService.php
Normal file
217
app/Domains/Ticket/Services/AdminAppTicketRowService.php
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticket\Services;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
|
use App\Domains\Purchase\Models\PurchaseItem;
|
||||||
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class AdminAppTicketRowService
|
||||||
|
{
|
||||||
|
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
||||||
|
|
||||||
|
private const CATEGORY_PRESENTATIONS = [
|
||||||
|
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null],
|
||||||
|
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null],
|
||||||
|
'entradas' => ['category' => null, 'product' => 'product', 'type' => null],
|
||||||
|
'comidas' => ['category' => 'Comida', 'product' => 'event_date', 'type' => 'horario'],
|
||||||
|
'comida' => ['category' => null, 'product' => 'event_date', 'type' => 'horario'],
|
||||||
|
'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function details(Ticket $ticket): array
|
||||||
|
{
|
||||||
|
$purchaseItem = $this->sourcePurchaseItem($ticket);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'source_purchase_id' => $ticket->source_purchase_id,
|
||||||
|
'order_number' => $ticket->source_purchase_id,
|
||||||
|
'product' => $purchaseItem?->item_nombre
|
||||||
|
?? $ticket->sourceCatalogItem?->nombre
|
||||||
|
?? $ticket->name,
|
||||||
|
'amount' => $purchaseItem?->precio_unitario,
|
||||||
|
'client' => $ticket->sourcePurchase?->nombre_apellido ?? $ticket->user?->nombre_apellido,
|
||||||
|
'date' => $ticket->sourcePurchase?->created_at,
|
||||||
|
'status' => $ticket->status,
|
||||||
|
'scanned_by' => $ticket->scannerUser?->nombre_apellido,
|
||||||
|
'variant_properties' => $this->variantProperties($ticket),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed>|null $details */
|
||||||
|
public function values(Ticket $ticket, ?array $details = null): array
|
||||||
|
{
|
||||||
|
$details ??= $this->details($ticket);
|
||||||
|
$presentation = $this->presentation($ticket, $details);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'order_number' => $details['order_number'],
|
||||||
|
'category' => $presentation['category'],
|
||||||
|
'product' => $presentation['product'],
|
||||||
|
'type' => $presentation['type'],
|
||||||
|
'amount' => $details['amount'] === null ? null : (float) $details['amount'],
|
||||||
|
'client' => $details['client'] ?? 'Sin nombre',
|
||||||
|
'ticket' => $ticket->ticket,
|
||||||
|
'date' => $details['date'],
|
||||||
|
'status' => $details['status'],
|
||||||
|
'scanned_by' => $details['scanned_by'] ?? '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, Ticket> $tickets
|
||||||
|
* @return Collection<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function rows(Collection $tickets): Collection
|
||||||
|
{
|
||||||
|
return $tickets->values()->map(fn (Ticket $ticket): array => $this->values($ticket));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, array<string, mixed>> $rows
|
||||||
|
* @param list<array<string, mixed>> $columns
|
||||||
|
* @return Collection<int, array<string, string>>
|
||||||
|
*/
|
||||||
|
public function displayRows(Collection $rows, array $columns, string $timeZone): Collection
|
||||||
|
{
|
||||||
|
return $rows->map(fn (array $row): array => collect($columns)
|
||||||
|
->mapWithKeys(fn (array $column): array => [
|
||||||
|
$column['key'] => $this->displayValue(
|
||||||
|
$row[$column['key']] ?? null,
|
||||||
|
$column['type'],
|
||||||
|
$timeZone,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function displayValue(mixed $value, string $type, string $timeZone): string
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
'order_number' => '#'.$value,
|
||||||
|
'currency' => '$'.number_format((float) $value, 2, ',', '.'),
|
||||||
|
'date' => ($value instanceof CarbonInterface ? $value : Carbon::parse((string) $value))
|
||||||
|
->copy()->timezone($timeZone)->format('d/m/Y H:i'),
|
||||||
|
'status' => match ((string) $value) {
|
||||||
|
Ticket::STATUS_USED => 'Usado',
|
||||||
|
Ticket::STATUS_EXPIRED => 'Vencido',
|
||||||
|
default => 'Activo',
|
||||||
|
},
|
||||||
|
default => (string) $value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $details */
|
||||||
|
private function presentation(Ticket $ticket, array $details): array
|
||||||
|
{
|
||||||
|
$sourceCategory = trim((string) ($ticket->sourceCatalogItem?->category?->nombre ?? '')) ?: '-';
|
||||||
|
|
||||||
|
if ($ticket->tenant_code !== self::FIESTA_FUTBOL_INFANTIL) {
|
||||||
|
return [
|
||||||
|
'category' => $sourceCategory,
|
||||||
|
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
|
||||||
|
'type' => $this->allPropertyLabels($details) ?: '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuration = self::CATEGORY_PRESENTATIONS[mb_strtolower($sourceCategory)] ?? null;
|
||||||
|
if ($configuration === null) {
|
||||||
|
return [
|
||||||
|
'category' => $sourceCategory,
|
||||||
|
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
|
||||||
|
'type' => $this->allPropertyLabels($details) ?: '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'category' => $configuration['category'] ?? $sourceCategory,
|
||||||
|
'product' => $configuration['product'] === 'product'
|
||||||
|
? (string) ($details['product'] ?: $ticket->name ?: '-')
|
||||||
|
: ($this->propertyLabels($details, $configuration['product']) ?: '-'),
|
||||||
|
'type' => $configuration['type'] === null
|
||||||
|
? '-'
|
||||||
|
: ($this->propertyLabels($details, $configuration['type']) ?: '-'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $details */
|
||||||
|
private function propertyLabels(array $details, string $code): string
|
||||||
|
{
|
||||||
|
$property = collect($details['variant_properties'] ?? [])->firstWhere('code', $code);
|
||||||
|
$labels = collect($property['values'] ?? [])->pluck('label')->filter();
|
||||||
|
|
||||||
|
if ($code === 'event_date') {
|
||||||
|
$labels = $labels->map(function (string $label): string {
|
||||||
|
[$day, $month] = array_pad(explode('/', $label), 2, null);
|
||||||
|
|
||||||
|
return $day !== null && $month !== null ? "{$day}/{$month}" : $label;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $labels->implode(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $details */
|
||||||
|
private function allPropertyLabels(array $details): string
|
||||||
|
{
|
||||||
|
return collect($details['variant_properties'] ?? [])
|
||||||
|
->flatMap(fn (array $property): array => $property['values'] ?? [])
|
||||||
|
->pluck('label')
|
||||||
|
->filter()
|
||||||
|
->implode(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sourcePurchaseItem(Ticket $ticket): ?PurchaseItem
|
||||||
|
{
|
||||||
|
return $ticket->sourcePurchase?->items->first(function (PurchaseItem $item) use ($ticket): bool {
|
||||||
|
if ($ticket->source_variant_id !== null) {
|
||||||
|
return $item->source_variant_id === $ticket->source_variant_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item->source_catalog_item_id === $ticket->source_catalog_item_id
|
||||||
|
&& $item->source_variant_id === null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{code: string, label: string, values: list<array{value: string, label: string}>}> */
|
||||||
|
private function variantProperties(Ticket $ticket): array
|
||||||
|
{
|
||||||
|
$variant = $ticket->sourceVariant;
|
||||||
|
if ($variant === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemAttributes = $variant->definitions
|
||||||
|
->map(fn ($definition) => $definition->itemAttribute)
|
||||||
|
->filter()
|
||||||
|
->merge($variant->catalogItem?->itemAttributes ?? collect())
|
||||||
|
->unique('id')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return $variant->selectionOptions($itemAttributes)
|
||||||
|
->map(function (array $selection, string $attributeCode) use ($itemAttributes): array {
|
||||||
|
$itemAttribute = $itemAttributes->first(
|
||||||
|
fn (ItemAttribute $itemAttribute): bool => $itemAttribute->attribute?->codigo
|
||||||
|
=== $attributeCode,
|
||||||
|
);
|
||||||
|
$values = array_is_list($selection) ? $selection : [$selection];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $attributeCode,
|
||||||
|
'label' => $itemAttribute?->attribute?->nombre
|
||||||
|
?? ($attributeCode === 'event_date' ? 'Fecha' : $attributeCode),
|
||||||
|
'values' => array_values($values),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -226,7 +226,10 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
|
|||||||
]))
|
]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonCount(1, 'data')
|
->assertJsonCount(1, 'data')
|
||||||
->assertJsonPath('data.0.id', $ticket->id);
|
->assertJsonPath('data.0.id', $ticket->id)
|
||||||
|
->assertJsonPath('data.0.values.category', 'Comida')
|
||||||
|
->assertJsonPath('data.0.values.product', '12/10')
|
||||||
|
->assertJsonPath('data.0.values.type', 'Cena');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createFiestaFutbolInfantilTenant(): Tenant
|
private function createFiestaFutbolInfantilTenant(): Tenant
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ class AdminAppTicketControllerTest extends TestCase
|
|||||||
->assertJsonCount(1, 'data')
|
->assertJsonCount(1, 'data')
|
||||||
->assertJsonPath('data.0.id', $ticket->id)
|
->assertJsonPath('data.0.id', $ticket->id)
|
||||||
->assertJsonPath('data.0.tenant_code', $tenant->codigo)
|
->assertJsonPath('data.0.tenant_code', $tenant->codigo)
|
||||||
|
->assertJsonPath('data.0.values.ticket', $ticket->ticket)
|
||||||
|
->assertJsonPath('data.0.values.status', Ticket::STATUS_ACTIVE)
|
||||||
->assertJsonPath('meta.total', 1);
|
->assertJsonPath('meta.total', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user