Kimi chinh sua
This commit is contained in:
@@ -4,15 +4,13 @@ namespace App\Filament\Resources\Contracts;
|
||||
|
||||
use App\Filament\Resources\Contracts\Pages;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Product;
|
||||
use App\Models\PaymentTemplate;
|
||||
use App\Enums\NavigationGroup;
|
||||
use App\Services\ContractScheduleService;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager;
|
||||
|
||||
use App\Filament\Resources\Contracts\Schemas\ContractForm;
|
||||
|
||||
class ContractResource extends Resource
|
||||
@@ -37,6 +35,27 @@ class ContractResource extends Resource
|
||||
Tables\Columns\TextColumn::make('contract_number')->label('Số HĐ')->searchable(),
|
||||
Tables\Columns\TextColumn::make('product.code')->label('Sản phẩm'),
|
||||
Tables\Columns\TextColumn::make('total_value')->label('Giá trị')->money('VND'),
|
||||
Tables\Columns\TextColumn::make('paid_amount')->label('Đã thu')->money('VND'),
|
||||
Tables\Columns\TextColumn::make('remaining_amount')->label('Còn lại')->money('VND'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\Action::make('generateSchedule')
|
||||
->label('Tạo lịch TT')
|
||||
->icon('heroicon-o-calendar-days')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Tạo lịch thanh toán')
|
||||
->modalDescription('Hành động này sẽ xóa lịch thanh toán cũ (nếu có) và tạo lại từ mẫu của dự án.')
|
||||
->action(function (Contract $record) {
|
||||
try {
|
||||
ContractScheduleService::generateFromTemplate($record);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Filament sẽ tự động hiển thị lỗi nếu throw ra trong action
|
||||
throw $e;
|
||||
}
|
||||
})
|
||||
->visible(fn (Contract $record) => $record->signing_date !== null),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
namespace App\Filament\Resources\Contracts\Pages;
|
||||
|
||||
use App\Filament\Resources\Contracts\ContractResource;
|
||||
use App\Models\PaymentTemplate;
|
||||
use App\Models\PaymentSchedule;
|
||||
use App\Models\PaymentScheduleItem;
|
||||
use App\Services\ContractScheduleService;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CreateContract extends CreateRecord
|
||||
{
|
||||
@@ -19,42 +16,9 @@ class CreateContract extends CreateRecord
|
||||
$templateId = $this->data['payment_template_id'] ?? null;
|
||||
|
||||
if ($templateId) {
|
||||
$template = PaymentTemplate::find($templateId);
|
||||
|
||||
// 1. Tạo Schedule cho Hợp đồng
|
||||
$schedule = PaymentSchedule::create([
|
||||
'contract_id' => $contract->id,
|
||||
'template_id' => $template->id,
|
||||
]);
|
||||
|
||||
// 2. Clone từng Item từ Template sang Schedule thực tế
|
||||
$items = $template->items()->orderBy('installment_no')->get();
|
||||
$lastDueDate = Carbon::parse($contract->signing_date);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$dueDate = null;
|
||||
|
||||
// Logic tính ngày đến hạn chuẩn v5.5
|
||||
if ($item->days_after_signing !== null) {
|
||||
$dueDate = Carbon::parse($contract->signing_date)->addDays($item->days_after_signing);
|
||||
} elseif ($item->days_after_previous !== null) {
|
||||
$dueDate = $lastDueDate->copy()->addDays($item->days_after_previous);
|
||||
} elseif ($item->due_date !== null) {
|
||||
$dueDate = $item->due_date;
|
||||
}
|
||||
|
||||
PaymentScheduleItem::create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'installment_no' => $item->installment_no,
|
||||
'type' => $item->type,
|
||||
'percentage' => $item->percentage,
|
||||
'amount' => $contract->total_value * ($item->percentage / 100),
|
||||
'due_date' => $dueDate,
|
||||
]);
|
||||
|
||||
if ($dueDate) {
|
||||
$lastDueDate = $dueDate;
|
||||
}
|
||||
$template = \App\Models\PaymentTemplate::find($templateId);
|
||||
if ($template) {
|
||||
ContractScheduleService::generateFromTemplate($contract, $template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ContractForm
|
||||
{
|
||||
@@ -18,63 +21,154 @@ class ContractForm
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Liên kết & Mẫu thanh toán')
|
||||
->columns(2)
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Select::make('product_id')
|
||||
->label('Sản phẩm')
|
||||
->relationship('product', 'code')
|
||||
->required()
|
||||
->live()
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
if ($state) {
|
||||
$product = Product::find($state);
|
||||
if ($product) {
|
||||
$set('total_value', $product->total_price);
|
||||
}
|
||||
Section::make('Thông tin định danh')
|
||||
->columnSpan(2)
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('product_id')
|
||||
->label('Sản phẩm (Lô đất)')
|
||||
->relationship('product', 'code')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required()
|
||||
->live()
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
if ($state) {
|
||||
$product = Product::find($state);
|
||||
if ($product) {
|
||||
$set('total_value', $product->total_price);
|
||||
$set('land_value', $product->qsdd_value);
|
||||
$set('foundation_value', $product->foundation_temp_value);
|
||||
}
|
||||
}
|
||||
}),
|
||||
TextInput::make('contract_number')
|
||||
->label('Số HĐMB')
|
||||
->required()
|
||||
->unique(ignoreRecord: true),
|
||||
Select::make('contract_type')
|
||||
->label('Loại hợp đồng')
|
||||
->options([
|
||||
'HĐMB' => 'Hợp đồng mua bán',
|
||||
'HĐGV' => 'Hợp đồng góp vốn',
|
||||
'HĐDC' => 'Hợp đồng đặt cọc',
|
||||
])
|
||||
->default('HĐMB')
|
||||
->required(),
|
||||
TextInput::make('transfer_order')
|
||||
->label('Thứ tự chuyển nhượng')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText('0 là chủ hiện tại, 1 là F0, 2 là F1...'),
|
||||
]),
|
||||
|
||||
Section::make('Trạng thái')
|
||||
->columnSpan(1)
|
||||
->schema([
|
||||
Select::make('status')
|
||||
->label('Trạng thái pháp lý')
|
||||
->options([
|
||||
'Đang hiệu lực' => 'Đang hiệu lực',
|
||||
'Đã hoàn thành' => 'Đã hoàn thành',
|
||||
'Đã hủy' => 'Đã hủy',
|
||||
])
|
||||
->default('Đang hiệu lực')
|
||||
->required(),
|
||||
DatePicker::make('signing_date')
|
||||
->label('Ngày ký HĐ')
|
||||
->required(),
|
||||
DatePicker::make('sale_date')
|
||||
->label('Ngày bán thực tế'),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Chi tiết Tài chính & Chiết khấu')
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('land_value')
|
||||
->label('Giá trị QSDĐ')
|
||||
->numeric()
|
||||
->prefix('VND')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, $get, $set) => $set('total_value', (float)$state + (float)$get('foundation_value'))),
|
||||
TextInput::make('foundation_value')
|
||||
->label('Giá trị Móng')
|
||||
->numeric()
|
||||
->prefix('VND')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, $get, $set) => $set('total_value', (float)$get('land_value') + (float)$state)),
|
||||
TextInput::make('total_value')
|
||||
->label('Tổng giá trị niêm yết')
|
||||
->numeric()
|
||||
->prefix('VND')
|
||||
->readOnly(),
|
||||
|
||||
Placeholder::make('discount_overview')
|
||||
->label('Tổng quan chiết khấu (Dữ liệu từ Excel)')
|
||||
->columnSpanFull()
|
||||
->content(function ($record) {
|
||||
if (!$record || !$record->discount_details) return 'Không có chiết khấu';
|
||||
$details = $record->discount_details;
|
||||
$html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; background: #f9fafb; padding: 15px; border-radius: 8px; border: 1px solid #e5e7eb;">';
|
||||
foreach ($details as $key => $val) {
|
||||
if (empty($val)) continue;
|
||||
$label = match($key) {
|
||||
'open_sale' => 'Mở bán',
|
||||
'multi_lot' => 'Số nhiều',
|
||||
'wholesale' => 'Mua sỉ',
|
||||
'ctv' => 'Cộng tác viên',
|
||||
'full_payment' => 'Trả 1 lần',
|
||||
'total_amount' => 'Tổng tiền CK',
|
||||
'total_percentage' => 'Tổng % CK',
|
||||
default => $key
|
||||
};
|
||||
$style = str_contains($key, 'total') ? 'font-weight: bold; color: #16a34a;' : 'color: #4b5563;';
|
||||
$html .= "<div>
|
||||
<div style='font-size: 0.7rem; color: #9ca3af; text-transform: uppercase; margin-bottom: 4px;'>{$label}</div>
|
||||
<div style='{$style} font-size: 0.9rem;'>{$val}</div>
|
||||
</div>";
|
||||
}
|
||||
$html .= '</div>';
|
||||
return new HtmlString($html);
|
||||
}),
|
||||
Select::make('payment_template_id')
|
||||
->label('Mẫu thanh toán')
|
||||
->options(fn () => PaymentTemplate::pluck('name', 'id'))
|
||||
->required()
|
||||
->dehydrated(false),
|
||||
Select::make('customers')
|
||||
->label('Khách hàng')
|
||||
->relationship('customers', 'full_name')
|
||||
->multiple()
|
||||
->required()
|
||||
|
||||
KeyValue::make('discount_details')
|
||||
->label('Bảng chi tiết chiết khấu (Dạng Key-Value)')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Section::make('Chi tiết Hợp đồng')
|
||||
|
||||
Section::make('Thông tin quản lý & Khách hàng')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('contract_number')->label('Số HĐ')->required(),
|
||||
Select::make('contract_type')
|
||||
->label('Loại HĐ')
|
||||
->options([
|
||||
'HĐMB' => 'HĐMB',
|
||||
'HĐGV' => 'HĐGV',
|
||||
'HĐDC' => 'HĐDC',
|
||||
])
|
||||
->default('HĐMB')
|
||||
->required(),
|
||||
DatePicker::make('signing_date')->label('Ngày ký')->required(),
|
||||
TextInput::make('total_value')
|
||||
->label('Giá trị HĐ')
|
||||
->numeric()
|
||||
Select::make('customers')
|
||||
->label('Khách hàng đứng tên')
|
||||
->multiple()
|
||||
->relationship('customers', 'full_name')
|
||||
->preload()
|
||||
->required()
|
||||
->prefix('VND'),
|
||||
Select::make('status')
|
||||
->label('Trạng thái')
|
||||
->options([
|
||||
'Đang hiệu lực' => 'Đang hiệu lực',
|
||||
'Đã hoàn thành' => 'Đã hoàn thành',
|
||||
'Đã hủy' => 'Đã hủy',
|
||||
])
|
||||
->default('Đang hiệu lực')
|
||||
->required(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
TextInput::make('brokerage_name')
|
||||
->label('Đơn vị môi giới'),
|
||||
DatePicker::make('hql_confirmation_date')
|
||||
->label('Ngày HQL xác nhận'),
|
||||
TextInput::make('stored_contract_count')
|
||||
->label('Số lượng HĐ lưu')
|
||||
->numeric()
|
||||
->default(0),
|
||||
TextInput::make('filing_note')
|
||||
->label('Ghi chú hồ sơ')
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('payment_template_id')
|
||||
->label('Áp dụng mẫu thanh toán')
|
||||
->placeholder('Chọn mẫu để tự động tạo lịch trình...')
|
||||
->options(PaymentTemplate::pluck('name', 'id'))
|
||||
->searchable()
|
||||
->dehydrated(false)
|
||||
->helperText('Lưu ý: Chỉ chọn nếu bạn muốn khởi tạo lại lịch trình thanh toán.'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Filament\Resources\Contracts\Tables;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ContractsTable
|
||||
@@ -13,18 +15,70 @@ class ContractsTable
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
//
|
||||
TextColumn::make('contract_number')
|
||||
->label('Số HĐMB')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->copyable()
|
||||
->description(fn ($record) => "Lô: {$record->product?->code}"),
|
||||
|
||||
TextColumn::make('customers.full_name')
|
||||
->label('Khách hàng')
|
||||
->searchable()
|
||||
->listWithLineBreaks()
|
||||
->bulleted(),
|
||||
|
||||
TextColumn::make('signing_date')
|
||||
->label('Ngày ký')
|
||||
->date('d/m/Y')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('total_value')
|
||||
->label('Giá trị HĐ')
|
||||
->money('VND')
|
||||
->sortable()
|
||||
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng doanh thu')->money('VND')),
|
||||
|
||||
TextColumn::make('transfer_order')
|
||||
->label('Đời CN')
|
||||
->badge()
|
||||
->color(fn ($state) => $state == 0 ? 'success' : 'gray')
|
||||
->formatStateUsing(fn ($state) => $state == 0 ? 'Hiện tại' : "F{$state}")
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label('Trạng thái')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'Đang hiệu lực' => 'success',
|
||||
'Đã hoàn thành' => 'primary',
|
||||
'Đã hủy' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
\Filament\Tables\Filters\SelectFilter::make('status')
|
||||
->label('Trạng thái')
|
||||
->options([
|
||||
'Đang hiệu lực' => 'Đang hiệu lực',
|
||||
'Đã hoàn thành' => 'Đã hoàn thành',
|
||||
'Đã hủy' => 'Đã hủy',
|
||||
]),
|
||||
\Filament\Tables\Filters\TernaryFilter::make('is_current')
|
||||
->label('Chủ sở hữu hiện tại')
|
||||
->queries(
|
||||
true: fn ($query) => $query->where('transfer_order', 0),
|
||||
false: fn ($query) => $query->where('transfer_order', '>', 0),
|
||||
)
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,12 @@ namespace App\Filament\Resources\Customers;
|
||||
|
||||
use App\Filament\Resources\Customers\Pages;
|
||||
use App\Models\Customer;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
use App\Enums\NavigationGroup;
|
||||
use App\Filament\Resources\Customers\Schemas\CustomerForm;
|
||||
use App\Filament\Resources\Customers\Tables\CustomersTable;
|
||||
|
||||
class CustomerResource extends Resource
|
||||
{
|
||||
@@ -22,47 +18,17 @@ class CustomerResource extends Resource
|
||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::CUSTOMER->value;
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static ?string $modelLabel = 'Khách hàng';
|
||||
protected static ?string $pluralModelLabel = 'Khách hàng';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Thông tin định danh')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('full_name')->label('Họ và Tên')->required(),
|
||||
TextInput::make('cmnd_cccd')->label('Số CMND / CCCD')->required()->unique(ignoreRecord: true),
|
||||
DatePicker::make('dob')->label('Ngày sinh')->displayFormat('d/m/Y'),
|
||||
]),
|
||||
Section::make('Thông liên lạc')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('phone')->label('Số điện thoại')->tel()->required(),
|
||||
TextInput::make('email')->label('Email')->email(),
|
||||
]),
|
||||
Section::make('Địa chỉ chi tiết')
|
||||
->schema([
|
||||
Fieldset::make('address')
|
||||
->label('Cấu trúc địa chỉ')->columns(3)
|
||||
->schema([
|
||||
TextInput::make('address.street')->label('Số nhà, đường')->columnSpan(3),
|
||||
TextInput::make('address.ward')->label('Phường / Xã'),
|
||||
TextInput::make('address.district')->label('Quận / Huyện'),
|
||||
TextInput::make('address.city')->label('Tỉnh / Thành phố'),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
return CustomerForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('full_name')->label('Họ Tên')->searchable(),
|
||||
Tables\Columns\TextColumn::make('cmnd_cccd')->label('CMND/CCCD')->searchable(),
|
||||
Tables\Columns\TextColumn::make('phone')->label('Điện thoại'),
|
||||
Tables\Columns\TextColumn::make('address.city')->label('Tỉnh/Thành')->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
return CustomersTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
namespace App\Filament\Resources\Customers\Schemas;
|
||||
|
||||
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\Forms\Components\TagsInput;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
class CustomerForm
|
||||
{
|
||||
@@ -12,20 +18,75 @@ class CustomerForm
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('full_name')
|
||||
->required(),
|
||||
TextInput::make('cmnd_cccd')
|
||||
->required(),
|
||||
TextInput::make('phone')
|
||||
->tel(),
|
||||
TextInput::make('email')
|
||||
->label('Email address')
|
||||
->email(),
|
||||
TextInput::make('address_permanent'),
|
||||
TextInput::make('address_contact'),
|
||||
DatePicker::make('dob'),
|
||||
DatePicker::make('id_issue_date'),
|
||||
TextInput::make('id_issue_place'),
|
||||
Section::make('Thông tin định danh')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('type')
|
||||
->label('Loại khách hàng')
|
||||
->options([
|
||||
'INDIVIDUAL' => 'Cá nhân',
|
||||
'COMPANY' => 'Công ty',
|
||||
])
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('full_name')
|
||||
->label(fn ($get) => $get('type') === 'COMPANY' ? 'Tên công ty' : 'Họ và tên')
|
||||
->required(),
|
||||
TextInput::make('cmnd_cccd')
|
||||
->label(fn ($get) => $get('type') === 'COMPANY' ? 'GPKD / Mã số thuế' : 'CMND / CCCD')
|
||||
->required(),
|
||||
TextInput::make('tax_code')
|
||||
->label('Mã số thuế')
|
||||
->visible(fn ($get) => $get('type') === 'COMPANY'),
|
||||
Select::make('representative_id')
|
||||
->label('Người đại diện pháp luật')
|
||||
->options(Customer::where('type', 'INDIVIDUAL')->pluck('full_name', 'id'))
|
||||
->searchable()
|
||||
->visible(fn ($get) => $get('type') === 'COMPANY')
|
||||
->required(fn ($get) => $get('type') === 'COMPANY'),
|
||||
]),
|
||||
|
||||
Section::make('Liên lạc')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('phone')
|
||||
->label('Số điện thoại chính')
|
||||
->tel(),
|
||||
TagsInput::make('secondary_phones')
|
||||
->label('Số điện thoại phụ')
|
||||
->placeholder('Nhập số và nhấn Enter'),
|
||||
TextInput::make('email')
|
||||
->label('Địa chỉ Email')
|
||||
->email(),
|
||||
]),
|
||||
|
||||
Section::make('Địa chỉ')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('permanent_address')
|
||||
->label('Địa chỉ thường trú / Trụ sở')
|
||||
->required()
|
||||
->suffixAction(
|
||||
Action::make('clone_to_contact')
|
||||
->label('Copy sang liên hệ')
|
||||
->icon('heroicon-m-arrow-right-start-on-rectangle')
|
||||
->action(function (Set $set, $state) {
|
||||
$set('contact_address', $state);
|
||||
})
|
||||
),
|
||||
TextInput::make('contact_address')
|
||||
->label('Địa chỉ liên hệ')
|
||||
->required(),
|
||||
]),
|
||||
|
||||
Section::make('Thông tin bổ sung')
|
||||
->columns(3)
|
||||
->visible(fn ($get) => $get('type') === 'INDIVIDUAL')
|
||||
->schema([
|
||||
DatePicker::make('dob')->label('Ngày sinh'),
|
||||
DatePicker::make('id_issue_date')->label('Ngày cấp CMND'),
|
||||
TextInput::make('id_issue_place')->label('Nơi cấp'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CustomersTable
|
||||
@@ -14,45 +15,60 @@ class CustomersTable
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('ID'),
|
||||
TextColumn::make('full_name')
|
||||
->searchable(),
|
||||
TextColumn::make('cmnd_cccd')
|
||||
->searchable(),
|
||||
TextColumn::make('phone')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->label('Email address')
|
||||
->searchable(),
|
||||
TextColumn::make('address_permanent')
|
||||
->searchable(),
|
||||
TextColumn::make('address_contact')
|
||||
->searchable(),
|
||||
TextColumn::make('dob')
|
||||
->date()
|
||||
->sortable(),
|
||||
TextColumn::make('id_issue_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
TextColumn::make('id_issue_place')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->label('Họ tên / Công ty')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->description(fn ($record) => $get_desc = $record->type === 'COMPANY' ? "ĐD: {$record->representative?->full_name}" : $record->cmnd_cccd),
|
||||
|
||||
TextColumn::make('type')
|
||||
->label('Loại')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'COMPANY' => 'warning',
|
||||
'INDIVIDUAL' => 'success',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'COMPANY' => 'Công ty',
|
||||
'INDIVIDUAL' => 'Cá nhân',
|
||||
default => $state,
|
||||
}),
|
||||
|
||||
TextColumn::make('phone')
|
||||
->label('Điện thoại')
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('permanent_address')
|
||||
->label('Địa chỉ thường trú')
|
||||
->limit(30)
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('contact_address')
|
||||
->label('Địa chỉ liên hệ')
|
||||
->limit(30)
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Ngày tạo')
|
||||
->dateTime('d/m/Y')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
\Filament\Tables\Filters\SelectFilter::make('type')
|
||||
->label('Loại khách hàng')
|
||||
->options([
|
||||
'INDIVIDUAL' => 'Cá nhân',
|
||||
'COMPANY' => 'Công ty',
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
|
||||
11
app/Filament/Resources/Payments/Pages/CreatePayment.php
Normal file
11
app/Filament/Resources/Payments/Pages/CreatePayment.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Payments\Pages;
|
||||
|
||||
use App\Filament\Resources\Payments\PaymentResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePayment extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Payments/Pages/EditPayment.php
Normal file
19
app/Filament/Resources/Payments/Pages/EditPayment.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Payments\Pages;
|
||||
|
||||
use App\Filament\Resources\Payments\PaymentResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPayment extends EditRecord
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Payments/Pages/ListPayments.php
Normal file
19
app/Filament/Resources/Payments/Pages/ListPayments.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Payments\Pages;
|
||||
|
||||
use App\Filament\Resources\Payments\PaymentResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPayments extends ListRecords
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/Filament/Resources/Payments/PaymentResource.php
Normal file
42
app/Filament/Resources/Payments/PaymentResource.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Payments;
|
||||
|
||||
use App\Filament\Resources\Payments\Pages;
|
||||
use App\Models\Payment;
|
||||
use App\Enums\NavigationGroup;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Table;
|
||||
use App\Filament\Resources\Payments\Schemas\PaymentForm;
|
||||
use App\Filament\Resources\Payments\Tables\PaymentsTable;
|
||||
|
||||
class PaymentResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Payment::class;
|
||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-banknotes';
|
||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->value;
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
protected static ?string $modelLabel = 'Phiếu thu';
|
||||
protected static ?string $pluralModelLabel = 'Phiếu thu';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return PaymentForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return PaymentsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListPayments::route('/'),
|
||||
'create' => Pages\CreatePayment::route('/create'),
|
||||
'edit' => Pages\EditPayment::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
98
app/Filament/Resources/Payments/Schemas/PaymentForm.php
Normal file
98
app/Filament/Resources/Payments/Schemas/PaymentForm.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Payments\Schemas;
|
||||
|
||||
use App\Models\PaymentScheduleItem;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
class PaymentForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Section::make('Thông tin phiếu thu')
|
||||
->columnSpan(2)
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('contract_id')
|
||||
->label('Hợp đồng')
|
||||
->relationship('contract', 'contract_number')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required()
|
||||
->live()
|
||||
->afterStateUpdated(function (Set $set) {
|
||||
$set('schedule_item_id', null);
|
||||
}),
|
||||
|
||||
Select::make('schedule_item_id')
|
||||
->label('Đợt thanh toán')
|
||||
->placeholder('Để trống nếu là tạm ứng / không đối soát đợt')
|
||||
->options(function (callable $get) {
|
||||
$contractId = $get('contract_id');
|
||||
if (! $contractId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return PaymentScheduleItem::query()
|
||||
->whereHas('schedule', fn ($q) => $q->where('contract_id', $contractId))
|
||||
->get()
|
||||
->mapWithKeys(function ($item) {
|
||||
$label = 'Đợt '.$item->installment_no.' - '.$item->type;
|
||||
if ($item->amount) {
|
||||
$label .= ' ('.number_format($item->amount).' VNĐ)';
|
||||
}
|
||||
|
||||
return [$item->id => $label];
|
||||
});
|
||||
})
|
||||
->searchable(),
|
||||
|
||||
TextInput::make('amount')
|
||||
->label('Số tiền thu')
|
||||
->numeric()
|
||||
->prefix('VND')
|
||||
->required(),
|
||||
|
||||
DatePicker::make('paid_date')
|
||||
->label('Ngày thu')
|
||||
->required()
|
||||
->default(now()),
|
||||
|
||||
TextInput::make('receipt_number')
|
||||
->label('Số phiếu thu / Mã giao dịch'),
|
||||
|
||||
Select::make('method')
|
||||
->label('Phương thức thanh toán')
|
||||
->options([
|
||||
'Chuyển khoản' => 'Chuyển khoản',
|
||||
'Tiền mặt' => 'Tiền mặt',
|
||||
'Thẻ' => 'Thẻ',
|
||||
'Khác' => 'Khác',
|
||||
])
|
||||
->default('Chuyển khoản')
|
||||
->required(),
|
||||
]),
|
||||
|
||||
Section::make('Bổ sung')
|
||||
->columnSpan(1)
|
||||
->schema([
|
||||
KeyValue::make('metadata')
|
||||
->label('Dữ liệu bổ sung (nếu có)')
|
||||
->keyLabel('Thông tin')
|
||||
->valueLabel('Giá trị'),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
59
app/Filament/Resources/Payments/Tables/PaymentsTable.php
Normal file
59
app/Filament/Resources/Payments/Tables/PaymentsTable.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Payments\Tables;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PaymentsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('contract.contract_number')
|
||||
->label('Hợp đồng')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('amount')
|
||||
->label('Số tiền')
|
||||
->money('VND')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('paid_date')
|
||||
->label('Ngày thu')
|
||||
->date('d/m/Y')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('method')
|
||||
->label('Phương thức')
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('receipt_number')
|
||||
->label('Số phiếu thu')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('scheduleItem.installment_no')
|
||||
->label('Đợt TT')
|
||||
->placeholder('Tạm ứng'),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('method')
|
||||
->label('Phương thức')
|
||||
->options([
|
||||
'Chuyển khoản' => 'Chuyển khoản',
|
||||
'Tiền mặt' => 'Tiền mặt',
|
||||
'Thẻ' => 'Thẻ',
|
||||
'Khác' => 'Khác',
|
||||
]),
|
||||
Tables\Filters\Filter::make('paid_date')
|
||||
->label('Ngày thu')
|
||||
->form([
|
||||
\Filament\Forms\Components\DatePicker::make('from')->label('Từ ngày'),
|
||||
\Filament\Forms\Components\DatePicker::make('to')->label('Đến ngày'),
|
||||
])
|
||||
->query(function ($query, array $data) {
|
||||
return $query
|
||||
->when($data['from'], fn ($q) => $q->whereDate('paid_date', '>=', $data['from']))
|
||||
->when($data['to'], fn ($q) => $q->whereDate('paid_date', '<=', $data['to']));
|
||||
}),
|
||||
])
|
||||
->defaultSort('paid_date', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ProductForm
|
||||
{
|
||||
@@ -49,29 +52,64 @@ class ProductForm
|
||||
]),
|
||||
Tabs\Tab::make('Kỹ thuật & Hạ tầng')
|
||||
->icon('heroicon-o-cog')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('adjacent_road')->label('Đường tiếp giáp'),
|
||||
TextInput::make('frontage_count')->label('Số mặt tiền')->numeric(),
|
||||
TextInput::make('max_floors')->label('Số tầng tối đa')->numeric(),
|
||||
TextInput::make('construction_status')->label('Trạng thái XD'),
|
||||
KeyValue::make('infrastructure_status')
|
||||
->label('Trạng thái hạ tầng')
|
||||
->columnSpanFull(),
|
||||
Section::make('Thông số kỹ thuật')
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('adjacent_road')->label('Đường tiếp giáp'),
|
||||
TextInput::make('frontage_count')->label('Số mặt tiền')->numeric(),
|
||||
TextInput::make('max_floors')->label('Số tầng cao')->numeric(),
|
||||
]),
|
||||
Section::make('Chi tiết hạ tầng thực tế')
|
||||
->description('Trình trạng hạ tầng kỹ thuật thực tế tại lô đất')
|
||||
->schema([
|
||||
Placeholder::make('infra_overview')
|
||||
->label('Tổng quan trạng thái (Tự động cập nhật)')
|
||||
->content(function ($record) {
|
||||
if (!$record || !$record->infrastructure_status || !is_array($record->infrastructure_status)) {
|
||||
return new HtmlString('<span style="color: #6b7280;">Chưa có dữ liệu hạ tầng</span>');
|
||||
}
|
||||
|
||||
$html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; background: #f9fafb; padding: 15px; border-radius: 8px; border: 1px solid #e5e7eb;">';
|
||||
|
||||
foreach ($record->infrastructure_status as $key => $status) {
|
||||
$statusLower = mb_strtolower($status);
|
||||
$color = '#6b7280'; // Mặc định xám
|
||||
$icon = '○';
|
||||
|
||||
if (str_contains($statusLower, 'hoàn thành') || str_contains($statusLower, 'đã bàn giao')) {
|
||||
$color = '#16a34a'; // Xanh lá
|
||||
$icon = '●';
|
||||
} elseif (str_contains($statusLower, 'đang thi công') || str_contains($statusLower, 'đang triển khai')) {
|
||||
$color = '#ca8a04'; // Vàng cam
|
||||
$icon = '◐';
|
||||
}
|
||||
|
||||
$html .= "<div style='display: flex; align-items: center; gap: 8px;'>
|
||||
<span style='color: {$color}; font-size: 1.2rem;'>{$icon}</span>
|
||||
<div>
|
||||
<div style='font-size: 0.75rem; color: #6b7280; text-transform: uppercase;'>{$key}</div>
|
||||
<div style='font-weight: 600; color: {$color}; font-size: 0.875rem;'>{$status}</div>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return new HtmlString($html);
|
||||
}),
|
||||
KeyValue::make('infrastructure_status')
|
||||
->label('Chỉnh sửa hạ tầng chi tiết')
|
||||
->addActionLabel('Thêm hạng mục hạ tầng')
|
||||
->keyLabel('Hạng mục')
|
||||
->valueLabel('Trạng thái hiện tại')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
]),
|
||||
Tabs\Tab::make('Tiến độ thanh toán')
|
||||
->icon('heroicon-o-calendar-days')
|
||||
->schema([
|
||||
\Filament\Forms\Components\Repeater::make('payment_schedule_preview')
|
||||
->label('Lịch trình dự kiến / Thực tế')
|
||||
->relationship(function ($record) {
|
||||
// Nếu sản phẩm đã có hợp đồng, lấy lịch trình từ hợp đồng đầu tiên
|
||||
if ($record && $record->contracts()->exists()) {
|
||||
return $record->contracts()->first()->scheduleItems();
|
||||
}
|
||||
// Nếu chưa có, chúng ta sẽ hiển thị mẫu từ Project (phần này cần logic state giả lập hoặc view)
|
||||
return null;
|
||||
})
|
||||
->schema([
|
||||
TextInput::make('installment_no')->label('Đợt')->disabled(),
|
||||
TextInput::make('type')->label('Loại')->disabled(),
|
||||
@@ -83,7 +121,7 @@ class ProductForm
|
||||
->addable(false)
|
||||
->deletable(false)
|
||||
->reorderable(false)
|
||||
->placeholder('Sản phẩm chưa có hợp đồng hoặc Dự án chưa gán mẫu thanh toán mặc định.')
|
||||
->dehydrated(false)
|
||||
]),
|
||||
])->columnSpanFull()
|
||||
]);
|
||||
|
||||
@@ -4,14 +4,12 @@ namespace App\Filament\Resources\Projects;
|
||||
|
||||
use App\Filament\Resources\Projects\Pages;
|
||||
use App\Models\Project;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use App\Enums\NavigationGroup;
|
||||
use App\Filament\Resources\Projects\Schemas\ProjectForm;
|
||||
|
||||
class ProjectResource extends Resource
|
||||
{
|
||||
@@ -25,28 +23,7 @@ class ProjectResource extends Resource
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Thông tin Dự án')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true),
|
||||
TextInput::make('name')->label('Tên Dự án')->required(),
|
||||
Select::make('payment_template_id')
|
||||
->label('Mẫu thanh toán mặc định')
|
||||
->relationship('paymentTemplate', 'name')
|
||||
->placeholder('Chọn mẫu thanh toán cho toàn dự án')
|
||||
->columnSpanFull(),
|
||||
Select::make('type')
|
||||
->label('Loại hình')
|
||||
->options([
|
||||
'Khu đô thị' => 'Khu đô thị',
|
||||
'Chung cư' => 'Chung cư',
|
||||
'Đất nền phân lô' => 'Đất nền phân lô',
|
||||
])->required(),
|
||||
TextInput::make('address')->label('Địa chỉ')->columnSpanFull(),
|
||||
])
|
||||
]);
|
||||
return ProjectForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace App\Filament\Resources\Projects\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ProjectForm
|
||||
@@ -10,7 +13,25 @@ class ProjectForm
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
//
|
||||
Section::make('Thông tin Dự án')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true),
|
||||
TextInput::make('name')->label('Tên Dự án')->required(),
|
||||
Select::make('payment_template_id')
|
||||
->label('Mẫu thanh toán mặc định')
|
||||
->relationship('paymentTemplate', 'name')
|
||||
->placeholder('Chọn mẫu thanh toán cho toàn dự án')
|
||||
->columnSpanFull(),
|
||||
Select::make('type')
|
||||
->label('Loại hình')
|
||||
->options([
|
||||
'Khu đô thị' => 'Khu đô thị',
|
||||
'Chung cư' => 'Chung cư',
|
||||
'Đất nền phân lô' => 'Đất nền phân lô',
|
||||
])->required(),
|
||||
TextInput::make('address')->label('Địa chỉ')->columnSpanFull(),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user