WIP: SoftDelete Contract/Payment/Customer, collected_by, Notifications, ProjectReport, ExportDebtReport
This commit is contained in:
151
app/Console/Commands/ExportDebtReport.php
Normal file
151
app/Console/Commands/ExportDebtReport.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\PaymentScheduleItem;
|
||||
use Illuminate\Console\Command;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
class ExportDebtReport extends Command
|
||||
{
|
||||
protected $signature = 'export:debt-report
|
||||
{--output=storage/app/reports/bao-cao-cong-no.xlsx : Đường dẫn file xuất}
|
||||
{--project= : Lọc theo UUID dự án}';
|
||||
|
||||
protected $description = 'Xuất báo cáo công nợ khách hàng ra file Excel';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$outputPath = $this->option('output');
|
||||
$projectId = $this->option('project');
|
||||
|
||||
// Đảm bảo thư mục tồn tại
|
||||
$dir = dirname($outputPath);
|
||||
if (! is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$this->info('Đang tải dữ liệu...');
|
||||
|
||||
// ===== Sheet 1: Tổng hợp công nợ =====
|
||||
$contractsQuery = Contract::query()
|
||||
->with(['customers', 'product.project', 'paymentSchedule.items'])
|
||||
->when($projectId, fn ($q) => $q->whereHas('product', fn ($q2) => $q2->where('project_id', $projectId)))
|
||||
->orderBy('contract_number');
|
||||
|
||||
$contracts = $contractsQuery->get();
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet1 = $spreadsheet->getActiveSheet();
|
||||
$sheet1->setTitle('Tổng hợp công nợ');
|
||||
|
||||
// Header
|
||||
$headers1 = ['STT', 'Số HĐMB', 'Khách hàng', 'Dự án', 'Lô đất', 'Giá trị HĐ (VNĐ)', 'Đã thu (VNĐ)', 'Còn lại (VNĐ)', 'Trạng thái', 'Ngày ký'];
|
||||
$this->writeHeader($sheet1, $headers1);
|
||||
|
||||
$row = 2;
|
||||
foreach ($contracts as $index => $contract) {
|
||||
$sheet1->setCellValue('A' . $row, $index + 1);
|
||||
$sheet1->setCellValue('B' . $row, $contract->contract_number);
|
||||
$sheet1->setCellValue('C' . $row, $contract->customers->pluck('full_name')->implode(', '));
|
||||
$sheet1->setCellValue('D' . $row, $contract->product->project->name ?? '');
|
||||
$sheet1->setCellValue('E' . $row, $contract->product->code ?? '');
|
||||
$sheet1->setCellValue('F' . $row, (float) $contract->total_value);
|
||||
$sheet1->setCellValue('G' . $row, (float) $contract->paid_amount);
|
||||
$sheet1->setCellValue('H' . $row, (float) $contract->remaining_amount);
|
||||
$sheet1->setCellValue('I' . $row, $contract->status);
|
||||
$sheet1->setCellValue('J' . $row, $contract->signing_date ? $contract->signing_date->format('d/m/Y') : '');
|
||||
$row++;
|
||||
}
|
||||
|
||||
$this->formatNumberColumns($sheet1, ['F', 'G', 'H'], $row - 1);
|
||||
$this->autoSizeColumns($sheet1, $headers1);
|
||||
|
||||
// ===== Sheet 2: Chi tiết đợt thanh toán chưa đủ =====
|
||||
$sheet2 = $spreadsheet->createSheet();
|
||||
$sheet2->setTitle('Chi tiết đợt TT');
|
||||
|
||||
$headers2 = ['STT', 'Số HĐMB', 'Khách hàng', 'Đợt', 'Loại', 'Ngày đến hạn', 'Số tiền đợt (VNĐ)', 'Đã thu (VNĐ)', 'Còn thiếu (VNĐ)'];
|
||||
$this->writeHeader($sheet2, $headers2);
|
||||
|
||||
$itemsQuery = PaymentScheduleItem::query()
|
||||
->with(['schedule.contract.customers', 'payments'])
|
||||
->whereHas('schedule.contract')
|
||||
->when($projectId, fn ($q) => $q->whereHas('schedule.contract.product', fn ($q2) => $q2->where('project_id', $projectId)))
|
||||
->orderBy('due_date');
|
||||
|
||||
$items = $itemsQuery->get();
|
||||
|
||||
$row = 2;
|
||||
$stt = 1;
|
||||
foreach ($items as $item) {
|
||||
$contract = $item->schedule?->contract;
|
||||
if (! $contract) continue;
|
||||
|
||||
$paid = (float) $item->paid_amount;
|
||||
$remaining = (float) $item->remaining_amount;
|
||||
|
||||
$sheet2->setCellValue('A' . $row, $stt);
|
||||
$sheet2->setCellValue('B' . $row, $contract->contract_number);
|
||||
$sheet2->setCellValue('C' . $row, $contract->customers->pluck('full_name')->implode(', '));
|
||||
$sheet2->setCellValue('D' . $row, $item->installment_no);
|
||||
$sheet2->setCellValue('E' . $row, $item->type?->getLabel() ?? (string) $item->type);
|
||||
$sheet2->setCellValue('F' . $row, $item->due_date ? $item->due_date->format('d/m/Y') : '');
|
||||
$sheet2->setCellValue('G' . $row, (float) $item->amount);
|
||||
$sheet2->setCellValue('H' . $row, $paid);
|
||||
$sheet2->setCellValue('I' . $row, $remaining);
|
||||
$row++;
|
||||
$stt++;
|
||||
}
|
||||
|
||||
$this->formatNumberColumns($sheet2, ['G', 'H', 'I'], $row - 1);
|
||||
$this->autoSizeColumns($sheet2, $headers2);
|
||||
|
||||
// Lưu file
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$writer->save($outputPath);
|
||||
|
||||
$this->info("Xuất báo cáo thành công: {$outputPath}");
|
||||
$this->info("- Tổng hợp: {$contracts->count()} hợp đồng");
|
||||
$this->info("- Chi tiết đợt: {$items->count()} dòng");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function writeHeader($sheet, array $headers): void
|
||||
{
|
||||
foreach ($headers as $colIndex => $header) {
|
||||
$coord = [$colIndex + 1, 1];
|
||||
$sheet->setCellValue($coord, $header);
|
||||
$cell = $sheet->getCell($coord);
|
||||
$cell->getStyle()->getFont()->setBold(true);
|
||||
$cell->getStyle()->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('E5E7EB');
|
||||
$cell->getStyle()->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$cell->getStyle()->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
|
||||
}
|
||||
}
|
||||
|
||||
private function formatNumberColumns($sheet, array $columns, int $lastRow): void
|
||||
{
|
||||
foreach ($columns as $col) {
|
||||
$sheet->getStyle("{$col}2:{$col}{$lastRow}")
|
||||
->getNumberFormat()
|
||||
->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1);
|
||||
}
|
||||
}
|
||||
|
||||
private function autoSizeColumns($sheet, array $headers): void
|
||||
{
|
||||
foreach (range(1, count($headers)) as $colIndex) {
|
||||
$colLetter = Coordinate::stringFromColumnIndex($colIndex);
|
||||
$sheet->getColumnDimension($colLetter)->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
app/Console/Commands/SendPaymentDueNotifications.php
Normal file
76
app/Console/Commands/SendPaymentDueNotifications.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PaymentScheduleItem;
|
||||
use App\Models\User;
|
||||
use App\Notifications\PaymentDueNotification;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SendPaymentDueNotifications extends Command
|
||||
{
|
||||
protected $signature = 'notifications:send-due-payments
|
||||
{--days=7 : Số ngày trước hạn để cảnh báo}
|
||||
{--dry-run : Chỉ liệt kê, không gửi}';
|
||||
|
||||
protected $description = 'Gửi cảnh báo đợt thanh toán sắp đến hạn cho tất cả users';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$from = now();
|
||||
$to = now()->addDays($days);
|
||||
|
||||
$items = PaymentScheduleItem::query()
|
||||
->with(['schedule.contract', 'payments'])
|
||||
->whereHas('schedule.contract')
|
||||
->whereDate('due_date', '>=', $from)
|
||||
->whereDate('due_date', '<=', $to)
|
||||
->whereRaw('amount > (SELECT COALESCE(SUM(amount), 0) FROM payments WHERE payments.schedule_item_id = payment_schedule_items.id)')
|
||||
->orderBy('due_date')
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
$this->warn('Không có đợt thanh toán nào sắp đến hạn trong ' . $days . ' ngày tới.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Tìm thấy {$items->count()} đợt thanh toán sắp đến hạn.");
|
||||
|
||||
$users = User::all();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->warn('Không có user nào trong hệ thống để nhận thông báo.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$contract = $item->schedule?->contract;
|
||||
$remaining = (float) $item->remaining_amount;
|
||||
|
||||
$this->line(sprintf(
|
||||
'- HĐ %s | Đợt %d | Ngày %s | Còn thiếu: %s VNĐ',
|
||||
$contract?->contract_number ?? 'N/A',
|
||||
$item->installment_no,
|
||||
$item->due_date?->format('d/m/Y'),
|
||||
number_format($remaining)
|
||||
));
|
||||
|
||||
if (! $dryRun) {
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new PaymentDueNotification($item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info('Chế độ dry-run: Không có thông báo nào được gửi.');
|
||||
} else {
|
||||
$this->info("Đã gửi thông báo cho {$users->count()} user(s).");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
84
app/Filament/Pages/ProjectReport.php
Normal file
84
app/Filament/Pages/ProjectReport.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Project;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
|
||||
class ProjectReport extends Page implements HasTable
|
||||
{
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-chart-bar';
|
||||
protected static ?string $navigationLabel = 'Báo cáo theo Dự án';
|
||||
protected static ?string $title = 'Báo cáo Thống kê theo Dự án';
|
||||
protected static string | \UnitEnum | null $navigationGroup = 'Quản lý Dòng tiền';
|
||||
protected static ?int $navigationSort = 50;
|
||||
protected string $view = 'filament.pages.project-report';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
Project::query()
|
||||
->select('projects.id', 'projects.name', 'projects.code')
|
||||
->selectRaw('COUNT(DISTINCT products.id) as product_count')
|
||||
->selectRaw('COUNT(DISTINCT CASE WHEN contracts.id IS NOT NULL THEN products.id END) as sold_product_count')
|
||||
->selectRaw('COUNT(DISTINCT contracts.id) as contract_count')
|
||||
->selectRaw('COALESCE(SUM(contracts.total_value), 0) as total_revenue')
|
||||
->selectRaw('COALESCE(SUM(contracts.paid_amount), 0) as total_paid')
|
||||
->selectRaw('COALESCE(SUM(contracts.remaining_amount), 0) as total_remaining')
|
||||
->leftJoin('products', 'products.project_id', '=', 'projects.id')
|
||||
->leftJoin('contracts', 'contracts.product_id', '=', 'products.id')
|
||||
->groupBy('projects.id', 'projects.name', 'projects.code')
|
||||
)
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label('Dự án')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('product_count')
|
||||
->label('Tổng SP')
|
||||
->alignCenter()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('sold_product_count')
|
||||
->label('Đã bán')
|
||||
->alignCenter()
|
||||
->sortable()
|
||||
->color('success'),
|
||||
|
||||
TextColumn::make('contract_count')
|
||||
->label('Số HĐ')
|
||||
->alignCenter()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('total_revenue')
|
||||
->label('Tổng giá trị HĐ')
|
||||
->money('VND')
|
||||
->sortable()
|
||||
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng')->money('VND')),
|
||||
|
||||
TextColumn::make('total_paid')
|
||||
->label('Đã thu')
|
||||
->money('VND')
|
||||
->sortable()
|
||||
->color('success')
|
||||
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng')->money('VND')),
|
||||
|
||||
TextColumn::make('total_remaining')
|
||||
->label('Công nợ phải thu')
|
||||
->money('VND')
|
||||
->sortable()
|
||||
->color('danger')
|
||||
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng')->money('VND')),
|
||||
])
|
||||
->defaultSort('total_revenue', 'desc')
|
||||
->paginated([10, 25, 50]);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use App\Models\Customer;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
@@ -149,6 +149,14 @@ class PaymentForm
|
||||
])
|
||||
->default('Chuyển khoản')
|
||||
->required(),
|
||||
|
||||
Select::make('collected_by')
|
||||
->label('Ngườ thu')
|
||||
->relationship('collector', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->default(auth()->id())
|
||||
->required(),
|
||||
]),
|
||||
|
||||
Section::make('Bổ sung')
|
||||
|
||||
@@ -73,6 +73,11 @@ class PaymentsTable
|
||||
->money('VND')
|
||||
->placeholder('-')
|
||||
->color('danger'),
|
||||
|
||||
Tables\Columns\TextColumn::make('collector.name')
|
||||
->label('Ngườ thu')
|
||||
->placeholder('-')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('method')
|
||||
|
||||
53
app/Filament/Widgets/RecentNotifications.php
Normal file
53
app/Filament/Widgets/RecentNotifications.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
|
||||
class RecentNotifications extends BaseWidget
|
||||
{
|
||||
protected int | string | array $columnSpan = 'full';
|
||||
protected static ?int $sort = 1;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(function () {
|
||||
$user = auth()->user();
|
||||
if (! $user) {
|
||||
return \Illuminate\Notifications\DatabaseNotification::query()->whereRaw('1=0');
|
||||
}
|
||||
return $user->notifications()->whereNull('read_at')->latest()->getQuery();
|
||||
})
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('data.title')
|
||||
->label('Tiêu đề')
|
||||
->badge()
|
||||
->color('warning'),
|
||||
|
||||
Tables\Columns\TextColumn::make('data.message')
|
||||
->label('Nội dung')
|
||||
->limit(100),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('Thờ gian')
|
||||
->dateTime('d/m/Y H:i')
|
||||
->color('gray'),
|
||||
])
|
||||
->actions([
|
||||
Action::make('markAsRead')
|
||||
->label('Đánh dấu đã đọc')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->color('success')
|
||||
->action(function ($record) {
|
||||
$record->markAsRead();
|
||||
}),
|
||||
])
|
||||
->paginated([5, 10, 25])
|
||||
->emptyStateHeading('Không có thông báo mới')
|
||||
->emptyStateDescription('Bạn sẽ nhận được cảnh báo khi có đợt thanh toán sắp đến hạn.');
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Contract extends Model
|
||||
{
|
||||
use HasUuids, HasFactory;
|
||||
use HasUuids, HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Customer extends Model
|
||||
{
|
||||
use HasUuids, HasFactory;
|
||||
use HasUuids, HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
use HasUuids, HasFactory;
|
||||
use HasUuids, HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -27,4 +28,9 @@ class Payment extends Model
|
||||
{
|
||||
return $this->belongsTo(PaymentScheduleItem::class, 'schedule_item_id');
|
||||
}
|
||||
|
||||
public function collector()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'collected_by');
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Notifications/PaymentDueNotification.php
Normal file
43
app/Notifications/PaymentDueNotification.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\PaymentScheduleItem;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class PaymentDueNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public PaymentScheduleItem $scheduleItem
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toDatabase(object $notifiable): array
|
||||
{
|
||||
$contract = $this->scheduleItem->schedule?->contract;
|
||||
$remaining = (float) $this->scheduleItem->remaining_amount;
|
||||
|
||||
return [
|
||||
'title' => 'Cảnh báo đợt thanh toán sắp đến hạn',
|
||||
'message' => sprintf(
|
||||
'HĐ %s - Đợt %d (%s) sẽ đến hạn vào %s. Còn thiếu: %s VNĐ.',
|
||||
$contract?->contract_number ?? 'N/A',
|
||||
$this->scheduleItem->installment_no,
|
||||
$this->scheduleItem->type?->getLabel() ?? 'N/A',
|
||||
$this->scheduleItem->due_date?->format('d/m/Y') ?? 'N/A',
|
||||
number_format($remaining)
|
||||
),
|
||||
'contract_id' => $contract?->id,
|
||||
'schedule_item_id' => $this->scheduleItem->id,
|
||||
'due_date' => $this->scheduleItem->due_date?->toDateString(),
|
||||
'remaining_amount' => $remaining,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use App\Filament\Widgets\ContractStatsOverview;
|
||||
use App\Filament\Widgets\UpcomingPaymentsTable;
|
||||
use App\Filament\Widgets\RecentNotifications;
|
||||
use Filament\Widgets\AccountWidget;
|
||||
use Filament\Widgets\FilamentInfoWidget;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
@@ -41,6 +42,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
])
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
|
||||
->widgets([
|
||||
RecentNotifications::class,
|
||||
ContractStatsOverview::class,
|
||||
UpcomingPaymentsTable::class,
|
||||
AccountWidget::class,
|
||||
|
||||
Reference in New Issue
Block a user