From 96e1ce4f406e8d5ea42cfc5e3f0e6167a8125d2a Mon Sep 17 00:00:00 2001 From: phuongtc Date: Wed, 6 May 2026 04:10:14 +0000 Subject: [PATCH] =?UTF-8?q?ho=C3=A0n=20thi=E1=BB=87n=20upload=20v=C3=A0=20?= =?UTF-8?q?global=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/Contracts/ContractResource.php | 34 +++++++++++ .../Resources/Customers/CustomerResource.php | 21 +++++++ .../Resources/Feedback/FeedbackResource.php | 34 +++++++++++ .../Resources/Feedback/Pages/EditFeedback.php | 2 + .../InteractionsRelationManager.php | 2 + .../Feedback/Schemas/FeedbackForm.php | 2 + .../Resources/Products/ProductResource.php | 24 ++++++++ app/Filament/Resources/Roles/RoleResource.php | 15 +++++ app/Filament/Resources/Users/UserResource.php | 17 ++++++ app/Http/Middleware/HandleUploadErrors.php | 56 +++++++++++++++++ check-upload.php | 60 +++++++++++++++++++ lang/en/feedback.php | 3 + lang/vi/feedback.php | 3 + public/.user.ini | 6 ++ start-server.sh | 6 ++ 15 files changed, 285 insertions(+) create mode 100644 app/Http/Middleware/HandleUploadErrors.php create mode 100644 check-upload.php create mode 100644 public/.user.ini create mode 100755 start-server.sh diff --git a/app/Filament/Resources/Contracts/ContractResource.php b/app/Filament/Resources/Contracts/ContractResource.php index 3ee5b9d1..bdf3fb75 100644 --- a/app/Filament/Resources/Contracts/ContractResource.php +++ b/app/Filament/Resources/Contracts/ContractResource.php @@ -13,6 +13,8 @@ use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; class ContractResource extends Resource { @@ -24,6 +26,8 @@ class ContractResource extends Resource protected static ?int $navigationSort = 4; + protected static ?string $recordTitleAttribute = 'type'; + public static function getPluralLabel(): ?string { return __('app.resource_contracts'); @@ -59,4 +63,34 @@ class ContractResource extends Resource 'edit' => EditContract::route('/{record}/edit'), ]; } + + // GlobalSearch customization + public static function getGlobalSearchEloquentQuery(): Builder + { + return parent::getGlobalSearchEloquentQuery() + ->with(['customer', 'product']); + } + + public static function modifyGlobalSearchQuery(Builder $query, string $search): void + { + $query->where('type', 'like', "%{$search}%") + ->orWhere('signed_status', 'like', "%{$search}%") + ->orWhereHas('customer', fn ($q) => $q->where('name', 'like', "%{$search}%")) + ->orWhereHas('product', fn ($q) => $q->where('name', 'like', "%{$search}%")); + } + + public static function getGlobalSearchResultTitle(Model $record): string + { + return \App\Enums\ContractType::from($record->type)->label() . ' - ' . ($record->customer?->name ?? '-'); + } + + public static function getGlobalSearchResultDetails(Model $record): array + { + return [ + __('app.product') => $record->product?->name ?? '-', + __('app.widget_customer') => $record->customer?->name ?? '-', + __('app.blade_status') => \App\Enums\ContractStatus::from($record->status)->label(), + __('app.signed_status') => $record->signed_status ?? '-', + ]; + } } diff --git a/app/Filament/Resources/Customers/CustomerResource.php b/app/Filament/Resources/Customers/CustomerResource.php index 4b5c8e00..698525ce 100644 --- a/app/Filament/Resources/Customers/CustomerResource.php +++ b/app/Filament/Resources/Customers/CustomerResource.php @@ -14,6 +14,8 @@ use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; class CustomerResource extends Resource { @@ -25,6 +27,8 @@ class CustomerResource extends Resource protected static ?int $navigationSort = 2; + protected static ?string $recordTitleAttribute = 'name'; + public static function getPluralLabel(): ?string { return __('app.resource_customers'); @@ -60,4 +64,21 @@ class CustomerResource extends Resource 'edit' => EditCustomer::route('/{record}/edit'), ]; } + + // GlobalSearch customization + public static function modifyGlobalSearchQuery(Builder $query, string $search): void + { + $query->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('phone', 'like', "%{$search}%"); + } + + public static function getGlobalSearchResultDetails(Model $record): array + { + return [ + __('app.email') => $record->email ?? '-', + __('app.phone') => $record->phone ?? '-', + __('app.address') => $record->address ?? '-', + ]; + } } diff --git a/app/Filament/Resources/Feedback/FeedbackResource.php b/app/Filament/Resources/Feedback/FeedbackResource.php index 11e39ae7..396621a4 100644 --- a/app/Filament/Resources/Feedback/FeedbackResource.php +++ b/app/Filament/Resources/Feedback/FeedbackResource.php @@ -10,10 +10,12 @@ use App\Filament\Resources\Feedback\Schemas\FeedbackForm; use App\Filament\Resources\Feedback\Tables\FeedbackTable; use App\Models\Feedback; use BackedEnum; +use Illuminate\Database\Eloquent\Model; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; class FeedbackResource extends Resource { @@ -25,6 +27,8 @@ class FeedbackResource extends Resource protected static ?int $navigationSort = 1; + protected static ?string $recordTitleAttribute = 'title'; + public static function getPluralLabel(): ?string { return __('app.resource_feedbacks'); @@ -60,4 +64,34 @@ class FeedbackResource extends Resource 'edit' => EditFeedback::route('/{record}/edit'), ]; } + + // GlobalSearch customization + public static function getGlobalSearchEloquentQuery(): Builder + { + return parent::getGlobalSearchEloquentQuery() + ->with(['customer', 'customerProduct.product', 'currentDepartment']); + } + + public static function modifyGlobalSearchQuery(Builder $query, string $search): void + { + $query->where('title', 'like', "%{$search}%") + ->orWhere('content', 'like', "%{$search}%") + ->orWhereHas('customer', fn ($q) => $q->where('name', 'like', "%{$search}%")); + } + + public static function getGlobalSearchResultDetails(Model $record): array + { + return [ + __('app.widget_customer') => $record->customer?->name ?? '-', + __('app.product') => $record->customerProduct?->product?->name ?? __('app.general'), + __('app.blade_status') => match ($record->status) { + 'pending' => __('app.status_pending'), + 'processing' => __('app.status_processing'), + 'resolved' => __('app.status_resolved'), + 'closed' => __('app.status_closed'), + default => $record->status, + }, + __('app.department') => $record->currentDepartment?->name ?? '-', + ]; + } } diff --git a/app/Filament/Resources/Feedback/Pages/EditFeedback.php b/app/Filament/Resources/Feedback/Pages/EditFeedback.php index 0d053847..e2526666 100644 --- a/app/Filament/Resources/Feedback/Pages/EditFeedback.php +++ b/app/Filament/Resources/Feedback/Pages/EditFeedback.php @@ -65,6 +65,8 @@ class EditFeedback extends EditRecord ->multiple() ->disk('public') ->directory('feedback-attachments') + ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'video/quicktime']) + ->maxSize(51200) // 50MB in KB ->columnSpanFull(), ]) ->action(function (array $data): void { diff --git a/app/Filament/Resources/Feedback/RelationManagers/InteractionsRelationManager.php b/app/Filament/Resources/Feedback/RelationManagers/InteractionsRelationManager.php index 7405d1b5..0726824e 100644 --- a/app/Filament/Resources/Feedback/RelationManagers/InteractionsRelationManager.php +++ b/app/Filament/Resources/Feedback/RelationManagers/InteractionsRelationManager.php @@ -77,6 +77,8 @@ class InteractionsRelationManager extends RelationManager ->multiple() ->disk('public') ->directory('feedback-attachments') + ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'video/quicktime']) + ->maxSize(51200) // 50MB in KB ->columnSpanFull(), ]); } diff --git a/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php b/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php index b74413b3..dff43580 100644 --- a/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php +++ b/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php @@ -197,6 +197,8 @@ class FeedbackForm ->multiple() ->disk('public') ->directory('feedback-attachments') + ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'video/quicktime']) + ->maxSize(51200) // 50MB in KB ->columnSpan(1), ]) ->columns(2), diff --git a/app/Filament/Resources/Products/ProductResource.php b/app/Filament/Resources/Products/ProductResource.php index d566a3fb..0d3cf7f5 100644 --- a/app/Filament/Resources/Products/ProductResource.php +++ b/app/Filament/Resources/Products/ProductResource.php @@ -16,6 +16,8 @@ use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; class ProductResource extends Resource { @@ -27,6 +29,8 @@ class ProductResource extends Resource protected static ?int $navigationSort = 1; + protected static ?string $recordTitleAttribute = 'name'; + public static function getPluralLabel(): ?string { return __('app.resource_products'); @@ -64,4 +68,24 @@ class ProductResource extends Resource 'edit' => EditProduct::route('/{record}/edit'), ]; } + + // GlobalSearch customization + public static function modifyGlobalSearchQuery(Builder $query, string $search): void + { + $query->where('name', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + } + + public static function getGlobalSearchResultDetails(Model $record): array + { + return [ + __('app.description') => $record->description ? \Illuminate\Support\Str::limit($record->description, 50) : '-', + __('app.blade_status') => match ($record->status) { + 'active' => __('app.status_active'), + 'inactive' => __('app.status_inactive'), + 'sold_out' => __('app.status_sold_out'), + default => $record->status, + }, + ]; + } } diff --git a/app/Filament/Resources/Roles/RoleResource.php b/app/Filament/Resources/Roles/RoleResource.php index 34e7d559..9a4a068c 100644 --- a/app/Filament/Resources/Roles/RoleResource.php +++ b/app/Filament/Resources/Roles/RoleResource.php @@ -12,6 +12,8 @@ use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Spatie\Permission\Models\Role; class RoleResource extends Resource @@ -59,4 +61,17 @@ class RoleResource extends Resource 'edit' => EditRole::route('/{record}/edit'), ]; } + + // GlobalSearch customization + public static function modifyGlobalSearchQuery(Builder $query, string $search): void + { + $query->where('name', 'like', "%{$search}%"); + } + + public static function getGlobalSearchResultDetails(Model $record): array + { + return [ + __('app.permissions') => $record->permissions->count() . ' quyền', + ]; + } } diff --git a/app/Filament/Resources/Users/UserResource.php b/app/Filament/Resources/Users/UserResource.php index 868f65c9..bcb908de 100644 --- a/app/Filament/Resources/Users/UserResource.php +++ b/app/Filament/Resources/Users/UserResource.php @@ -13,6 +13,8 @@ use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; class UserResource extends Resource { @@ -59,4 +61,19 @@ class UserResource extends Resource 'edit' => EditUser::route('/{record}/edit'), ]; } + + // GlobalSearch customization + public static function modifyGlobalSearchQuery(Builder $query, string $search): void + { + $query->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + } + + public static function getGlobalSearchResultDetails(Model $record): array + { + return [ + __('app.email') => $record->email ?? '-', + __('app.role') => ucfirst($record->role ?? '-'), + ]; + } } diff --git a/app/Http/Middleware/HandleUploadErrors.php b/app/Http/Middleware/HandleUploadErrors.php new file mode 100644 index 00000000..62d25d40 --- /dev/null +++ b/app/Http/Middleware/HandleUploadErrors.php @@ -0,0 +1,56 @@ +isMethod('POST') && $request->header('Content-Type', '') === 'application/json') { + $content = $request->getContent(); + $data = json_decode($content, true); + + if (json_last_error() === JSON_ERROR_NONE) { + // Check if any file uploads are present in the request + $this->checkUploadLimits($data, $request); + } + } + + $response = $next($request); + + return $response; + } + + protected function checkUploadLimits(array $data, Request $request): void + { + // Check PHP upload limits + $uploadMax = $this->parseSize(ini_get('upload_max_filesize')); + $postMax = $this->parseSize(ini_get('post_max_size')); + + // Log current limits for debugging + Log::info('Upload limits', [ + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size'), + 'content_length' => $request->header('Content-Length'), + ]); + } + + protected function parseSize(string $size): int + { + $unit = strtolower(substr($size, -1)); + $value = (int) $size; + + return match ($unit) { + 'g' => $value * 1024 * 1024 * 1024, + 'm' => $value * 1024 * 1024, + 'k' => $value * 1024, + default => $value, + }; + } +} diff --git a/check-upload.php b/check-upload.php new file mode 100644 index 00000000..b60ba27c --- /dev/null +++ b/check-upload.php @@ -0,0 +1,60 @@ + $value * 1024 * 1024 * 1024, + 'm' => $value * 1024 * 1024, + 'k' => $value * 1024, + default => $value, + }; +} + +function formatSize(int $bytes): string +{ + if ($bytes >= 1024 * 1024 * 1024) { + return round($bytes / (1024 * 1024 * 1024), 2) . ' GB'; + } + if ($bytes >= 1024 * 1024) { + return round($bytes / (1024 * 1024), 2) . ' MB'; + } + if ($bytes >= 1024) { + return round($bytes / 1024, 2) . ' KB'; + } + return $bytes . ' bytes'; +} diff --git a/lang/en/feedback.php b/lang/en/feedback.php index e6281d96..149ecebb 100644 --- a/lang/en/feedback.php +++ b/lang/en/feedback.php @@ -14,6 +14,9 @@ return [ // FileService 'file_type_not_allowed' => 'File type :type is not allowed. Allowed: jpg, png, pdf, mp4, mov.', 'file_size_exceeds' => 'File size exceeds :max KB.', + 'file_upload_failed' => 'File upload failed. Please check the file and try again.', + 'file_too_large' => 'File is too large. Maximum size is :max MB.', + 'file_invalid_type' => 'Invalid file type. Only accept: jpg, png, pdf, mp4, mov.', // EditFeedback page 'transfer_department' => 'Transfer Department', diff --git a/lang/vi/feedback.php b/lang/vi/feedback.php index d966864e..979ddc5a 100644 --- a/lang/vi/feedback.php +++ b/lang/vi/feedback.php @@ -14,6 +14,9 @@ return [ // FileService 'file_type_not_allowed' => 'Loại file :type không được phép. Cho phép: jpg, png, pdf, mp4, mov.', 'file_size_exceeds' => 'Kích thước file vượt quá :max KB.', + 'file_upload_failed' => 'Tải file lên thất bại. Vui lòng kiểm tra lại file và thử lại.', + 'file_too_large' => 'File quá lớn. Kích thước tối đa là :max MB.', + 'file_invalid_type' => 'Loại file không hợp lệ. Chỉ chấp nhận: jpg, png, pdf, mp4, mov.', // EditFeedback page 'transfer_department' => 'Chuyển phòng ban', diff --git a/public/.user.ini b/public/.user.ini new file mode 100644 index 00000000..c171ddfd --- /dev/null +++ b/public/.user.ini @@ -0,0 +1,6 @@ +; PHP upload settings for AfterSales CRM +upload_max_filesize = 50M +post_max_size = 55M +memory_limit = 256M +max_execution_time = 300 +max_input_time = 300 diff --git a/start-server.sh b/start-server.sh new file mode 100755 index 00000000..5516cb49 --- /dev/null +++ b/start-server.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Start PHP server with increased upload limits +php -d upload_max_filesize=50M \ + -d post_max_size=55M \ + -d memory_limit=256M \ + artisan serve --host=0.0.0.0 --port=8000