them log, chinh upload
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ActivityLogger;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -71,6 +72,17 @@ class ExportBackup extends Command
|
||||
|
||||
$this->printSummary($backupDir, $totalRows);
|
||||
|
||||
ActivityLogger::log(
|
||||
'export',
|
||||
__('feedback.log_export_done', ['tables' => count($tables), 'rows' => $totalRows]),
|
||||
null,
|
||||
[
|
||||
'tables' => $this->stats,
|
||||
'total_rows' => $totalRows,
|
||||
'path' => $backupDir,
|
||||
],
|
||||
);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ActivityLogger;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Customer;
|
||||
use App\Models\CustomerProduct;
|
||||
@@ -52,6 +53,8 @@ class ImportCskh extends Command
|
||||
$this->warn('=== DRY RUN MODE — No data will be written ===');
|
||||
}
|
||||
|
||||
$startTime = now();
|
||||
|
||||
$rows = SimpleExcelReader::create($filePath)->getRows();
|
||||
|
||||
$total = iterator_count($rows);
|
||||
@@ -94,6 +97,29 @@ class ImportCskh extends Command
|
||||
$this->newLine(2);
|
||||
$this->printSummary($rowCount, $dryRun);
|
||||
|
||||
$duration = now()->diffInSeconds($startTime);
|
||||
$modeLabel = $dryRun ? '[DRY RUN] ' : '';
|
||||
|
||||
ActivityLogger::log(
|
||||
'import',
|
||||
$modeLabel . __('feedback.log_import_done', ['file' => basename($filePath), 'rows' => $rowCount]),
|
||||
null,
|
||||
[
|
||||
'file' => $filePath,
|
||||
'total_rows' => $rowCount,
|
||||
'skipped' => $this->statsSkipped,
|
||||
'products' => $this->statsProducts,
|
||||
'customers' => $this->statsCustomers,
|
||||
'contracts' => $this->statsContracts,
|
||||
'handovers' => $this->statsHandovers,
|
||||
'services' => $this->statsServices,
|
||||
'feedbacks' => $this->statsFeedbacks,
|
||||
'interactions' => $this->statsInteractions,
|
||||
'duration_seconds' => $duration,
|
||||
'dry_run' => $dryRun,
|
||||
],
|
||||
);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/**
|
||||
* Contract Status Enum
|
||||
*
|
||||
* Để thêm trạng thái mới:
|
||||
* 1. Thêm case mới vào enum
|
||||
* 2. Thêm label trong lang/vi/enums.php và lang/en/enums.php
|
||||
* 3. Reload browser
|
||||
*/
|
||||
enum ContractStatus: string
|
||||
{
|
||||
case ACTIVE = 'active';
|
||||
@@ -17,12 +25,19 @@ enum ContractStatus: string
|
||||
};
|
||||
}
|
||||
|
||||
public function options(): array
|
||||
public function color(): string
|
||||
{
|
||||
return [
|
||||
self::ACTIVE->value => self::ACTIVE->label(),
|
||||
self::EXPIRED->value => self::EXPIRED->label(),
|
||||
self::LIQUIDATED->value => self::LIQUIDATED->label(),
|
||||
];
|
||||
return match ($this) {
|
||||
self::ACTIVE => 'success',
|
||||
self::EXPIRED => 'warning',
|
||||
self::LIQUIDATED => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return collect(self::cases())->mapWithKeys(fn ($case) => [
|
||||
$case->value => $case->label(),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/**
|
||||
* Contract Type Enum
|
||||
*
|
||||
* Để thêm loại hợp đồng mới:
|
||||
* 1. Thêm case mới vào enum
|
||||
* 2. Thêm label trong lang/vi/enums.php và lang/en/enums.php
|
||||
* 3. Reload browser
|
||||
*/
|
||||
enum ContractType: string
|
||||
{
|
||||
case SALE = 'sale';
|
||||
@@ -17,12 +25,19 @@ enum ContractType: string
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::SALE => 'success',
|
||||
self::LEASE => 'info',
|
||||
self::BCC => 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return [
|
||||
self::SALE->value => self::SALE->label(),
|
||||
self::LEASE->value => self::LEASE->label(),
|
||||
self::BCC->value => self::BCC->label(),
|
||||
];
|
||||
return collect(self::cases())->mapWithKeys(fn ($case) => [
|
||||
$case->value => $case->label(),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/**
|
||||
* Handover Status Enum
|
||||
*
|
||||
* Để thêm trạng thái mới:
|
||||
* 1. Thêm case mới vào enum
|
||||
* 2. Thêm label trong lang/vi/enums.php và lang/en/enums.php
|
||||
* 3. Reload browser
|
||||
*/
|
||||
enum HandoverStatus: string
|
||||
{
|
||||
case NOT_READY = 'chua_du_dieu_kien';
|
||||
@@ -19,13 +27,20 @@ enum HandoverStatus: string
|
||||
};
|
||||
}
|
||||
|
||||
public function options(): array
|
||||
public function color(): string
|
||||
{
|
||||
return [
|
||||
self::NOT_READY->value => self::NOT_READY->label(),
|
||||
self::READY->value => self::READY->label(),
|
||||
self::SCHEDULED->value => self::SCHEDULED->label(),
|
||||
self::HANDED_OVER->value => self::HANDED_OVER->label(),
|
||||
];
|
||||
return match ($this) {
|
||||
self::NOT_READY => 'danger',
|
||||
self::READY => 'warning',
|
||||
self::HANDED_OVER => 'success',
|
||||
self::SCHEDULED => 'info',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return collect(self::cases())->mapWithKeys(fn ($case) => [
|
||||
$case->value => $case->label(),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/**
|
||||
* Service Type Enum
|
||||
*
|
||||
* Để thêm loại dịch vụ mới:
|
||||
* 1. Thêm case mới vào enum
|
||||
* 2. Thêm label trong lang/vi/enums.php và lang/en/enums.php
|
||||
* 3. Reload browser
|
||||
*/
|
||||
enum ServiceType: string
|
||||
{
|
||||
case MANAGEMENT_FEE = 'management_fee';
|
||||
@@ -17,12 +25,19 @@ enum ServiceType: string
|
||||
};
|
||||
}
|
||||
|
||||
public function options(): array
|
||||
public function color(): string
|
||||
{
|
||||
return [
|
||||
self::MANAGEMENT_FEE->value => self::MANAGEMENT_FEE->label(),
|
||||
self::GRATITUDE->value => self::GRATITUDE->label(),
|
||||
self::SELF_BUSINESS->value => self::SELF_BUSINESS->label(),
|
||||
];
|
||||
return match ($this) {
|
||||
self::MANAGEMENT_FEE => 'info',
|
||||
self::GRATITUDE => 'success',
|
||||
self::SELF_BUSINESS => 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return collect(self::cases())->mapWithKeys(fn ($case) => [
|
||||
$case->value => $case->label(),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
132
app/Filament/Resources/ActivityLogs/ActivityLogResource.php
Normal file
132
app/Filament/Resources/ActivityLogs/ActivityLogResource.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ActivityLogs;
|
||||
|
||||
use App\Filament\Resources\ActivityLogs\Pages\ListActivityLogs;
|
||||
use App\Models\ActivityLog;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ActivityLogResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ActivityLog::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedClock;
|
||||
|
||||
protected static ?int $navigationSort = 7;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'description';
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_activity_logs');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('description')
|
||||
->defaultSort('created_at', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('created_at')
|
||||
->label(__('app.time'))
|
||||
->dateTime('d/m/Y H:i:s')
|
||||
->sortable(),
|
||||
TextColumn::make('user.name')
|
||||
->label(__('app.user'))
|
||||
->placeholder(__('app.system')),
|
||||
TextColumn::make('action')
|
||||
->label(__('app.action'))
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'import' => __('app.action_import'),
|
||||
'export' => __('app.action_export'),
|
||||
'transfer' => __('app.action_transfer'),
|
||||
'close' => __('app.action_close'),
|
||||
'create' => __('app.action_create'),
|
||||
'update' => __('app.action_update'),
|
||||
'delete' => __('app.action_delete'),
|
||||
default => $state,
|
||||
})
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'import' => 'info',
|
||||
'export' => 'success',
|
||||
'transfer' => 'warning',
|
||||
'close' => 'success',
|
||||
'create' => 'info',
|
||||
'update' => 'gray',
|
||||
'delete' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
TextColumn::make('description')
|
||||
->label(__('app.description'))
|
||||
->wrap()
|
||||
->searchable(),
|
||||
TextColumn::make('metadata')
|
||||
->label(__('app.details'))
|
||||
->formatStateUsing(function (?array $state): string {
|
||||
if (empty($state)) return '';
|
||||
$lines = [];
|
||||
foreach ($state as $key => $val) {
|
||||
if (is_scalar($val)) {
|
||||
$lines[] = "<b>{$key}</b>: {$val}";
|
||||
} elseif (is_array($val)) {
|
||||
$nested = [];
|
||||
foreach ($val as $k => $v) {
|
||||
if (is_scalar($v)) {
|
||||
$nested[] = "{$k}: {$v}";
|
||||
}
|
||||
}
|
||||
$lines[] = "<b>{$key}</b>: " . implode(', ', $nested);
|
||||
}
|
||||
}
|
||||
return implode('<br>', $lines);
|
||||
})
|
||||
->html()
|
||||
->wrap()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('action')
|
||||
->label(__('app.action'))
|
||||
->options([
|
||||
'import' => __('app.action_import'),
|
||||
'export' => __('app.action_export'),
|
||||
'transfer' => __('app.action_transfer'),
|
||||
'close' => __('app.action_close'),
|
||||
'create' => __('app.action_create'),
|
||||
'update' => __('app.action_update'),
|
||||
'delete' => __('app.action_delete'),
|
||||
]),
|
||||
SelectFilter::make('user_id')
|
||||
->label(__('app.user'))
|
||||
->relationship('user', 'name'),
|
||||
])
|
||||
->actions([])
|
||||
->bulkActions([]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListActivityLogs::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()
|
||||
->with('user')
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ActivityLogs\Pages;
|
||||
|
||||
use App\Filament\Resources\ActivityLogs\ActivityLogResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListActivityLogs extends ListRecords
|
||||
{
|
||||
protected static string $resource = ActivityLogResource::class;
|
||||
}
|
||||
@@ -12,7 +12,10 @@ class FeedbacksRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'feedbacks';
|
||||
|
||||
protected static ?string $title = 'app.feedback_history';
|
||||
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
||||
{
|
||||
return __('app.feedback_history');
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
|
||||
@@ -49,6 +49,10 @@ class CreateFeedback extends CreateRecord
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($feedback->attachments()->where('path', $path)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$feedback->attachments()->create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'user_id' => auth()->id(),
|
||||
@@ -56,9 +60,27 @@ class CreateFeedback extends CreateRecord
|
||||
'path' => $path,
|
||||
'disk' => 'public',
|
||||
'collection' => $this->pendingCollection,
|
||||
'mime_type' => $disk->mimeType($path),
|
||||
'size' => $disk->size($path),
|
||||
'mime_type' => $this->safeMimeType($disk, $path),
|
||||
'size' => $this->safeFileSize($disk, $path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function safeMimeType($disk, string $path): ?string
|
||||
{
|
||||
try {
|
||||
return $disk->mimeType($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function safeFileSize($disk, string $path): ?int
|
||||
{
|
||||
try {
|
||||
return $disk->size($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ class EditFeedback extends EditRecord
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($feedback->attachments()->where('path', $path)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$feedback->attachments()->create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'user_id' => $sender->id,
|
||||
@@ -97,8 +101,8 @@ class EditFeedback extends EditRecord
|
||||
'path' => $path,
|
||||
'disk' => 'public',
|
||||
'collection' => 'general',
|
||||
'mime_type' => $disk->mimeType($path),
|
||||
'size' => $disk->size($path),
|
||||
'mime_type' => $this->safeMimeType($disk, $path),
|
||||
'size' => $this->safeFileSize($disk, $path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -194,6 +198,10 @@ class EditFeedback extends EditRecord
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($feedback->attachments()->where('path', $path)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$feedback->attachments()->create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'user_id' => auth()->id(),
|
||||
@@ -201,12 +209,30 @@ class EditFeedback extends EditRecord
|
||||
'path' => $path,
|
||||
'disk' => 'public',
|
||||
'collection' => $this->pendingCollection,
|
||||
'mime_type' => $disk->mimeType($path),
|
||||
'size' => $disk->size($path),
|
||||
'mime_type' => $this->safeMimeType($disk, $path),
|
||||
'size' => $this->safeFileSize($disk, $path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function safeMimeType($disk, string $path): ?string
|
||||
{
|
||||
try {
|
||||
return $disk->mimeType($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function safeFileSize($disk, string $path): ?int
|
||||
{
|
||||
try {
|
||||
return $disk->size($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$data['is_general'] = $data['customer_product_id'] === null;
|
||||
|
||||
@@ -27,7 +27,10 @@ class InteractionsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'interactions';
|
||||
|
||||
protected static ?string $title = 'feedback.interaction_history';
|
||||
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
||||
{
|
||||
return __('feedback.interaction_history');
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
@@ -211,6 +214,10 @@ class InteractionsRelationManager extends RelationManager
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($record->attachments()->where('path', $path)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$record->attachments()->create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'feedback_id' => $record->feedback_id,
|
||||
@@ -219,8 +226,8 @@ class InteractionsRelationManager extends RelationManager
|
||||
'path' => $path,
|
||||
'disk' => 'public',
|
||||
'collection' => 'general',
|
||||
'mime_type' => $disk->mimeType($path),
|
||||
'size' => $disk->size($path),
|
||||
'mime_type' => $this->safeMimeType($disk, $path),
|
||||
'size' => $this->safeFileSize($disk, $path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -262,4 +269,22 @@ class InteractionsRelationManager extends RelationManager
|
||||
throw new \Filament\Exceptions\SilentActionException;
|
||||
}
|
||||
}
|
||||
|
||||
protected function safeMimeType($disk, string $path): ?string
|
||||
{
|
||||
try {
|
||||
return $disk->mimeType($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function safeFileSize($disk, string $path): ?int
|
||||
{
|
||||
try {
|
||||
return $disk->size($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Filament\Resources\Products\RelationManagers;
|
||||
use App\Enums\ContractStatus;
|
||||
use App\Enums\ContractType;
|
||||
use App\Filament\Resources\Contracts\ContractResource;
|
||||
use App\Models\Feedback;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filter;
|
||||
@@ -15,7 +16,10 @@ class ContractsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'contracts';
|
||||
|
||||
protected static ?string $title = 'app.contracts';
|
||||
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
||||
{
|
||||
return __('app.contracts');
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
|
||||
@@ -12,7 +12,10 @@ class HandoversRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'handovers';
|
||||
|
||||
protected static ?string $title = 'app.handovers';
|
||||
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
||||
{
|
||||
return __('app.handovers');
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
namespace App\Filament\Resources\Products\RelationManagers;
|
||||
|
||||
use App\Enums\ServiceType;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
@@ -12,7 +19,39 @@ class ProductServicesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'productServices';
|
||||
|
||||
protected static ?string $title = 'app.services';
|
||||
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
||||
{
|
||||
return __('app.services');
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('service_type')
|
||||
->label(__('enums.service_type'))
|
||||
->options(ServiceType::options())
|
||||
->required(),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('enums.status'))
|
||||
->options([
|
||||
'active' => __('app.status_active'),
|
||||
'pending' => __('app.status_pending'),
|
||||
'completed' => __('app.status_completed'),
|
||||
'cancelled' => __('app.status_cancelled'),
|
||||
])
|
||||
->default('active')
|
||||
->required(),
|
||||
|
||||
DatePicker::make('actual_collection_date')
|
||||
->label(__('app.collection_date')),
|
||||
|
||||
Textarea::make('remark')
|
||||
->label(__('app.remark'))
|
||||
->rows(3),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
@@ -20,15 +59,12 @@ class ProductServicesRelationManager extends RelationManager
|
||||
->recordTitleAttribute('service_type')
|
||||
->columns([
|
||||
TextColumn::make('service_type')
|
||||
->label(__('enums.service_type'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'management_fee' => 'info',
|
||||
'gratitude' => 'success',
|
||||
'self_business' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->color(fn (string $state): string => ServiceType::from($state)->color())
|
||||
->formatStateUsing(fn (string $state): string => ServiceType::from($state)->label()),
|
||||
TextColumn::make('status')
|
||||
->label(__('enums.status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'active' => 'success',
|
||||
@@ -52,8 +88,10 @@ class ProductServicesRelationManager extends RelationManager
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('service_type')
|
||||
->options(ServiceType::MANAGEMENT_FEE->options()),
|
||||
->label(__('enums.service_type'))
|
||||
->options(ServiceType::options()),
|
||||
SelectFilter::make('status')
|
||||
->label(__('enums.status'))
|
||||
->options([
|
||||
'active' => __('app.status_active'),
|
||||
'pending' => __('app.status_pending'),
|
||||
@@ -62,7 +100,12 @@ class ProductServicesRelationManager extends RelationManager
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->headerActions([])
|
||||
->recordActions([]);
|
||||
->headerActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Models/ActivityLog.php
Normal file
32
app/Models/ActivityLog.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
#[Fillable(['user_id', 'action', 'description', 'subject_type', 'subject_id', 'metadata', 'created_at'])]
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function subject(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
34
app/Policies/ActivityLogPolicy.php
Normal file
34
app/Policies/ActivityLogPolicy.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
|
||||
class ActivityLogPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('view-activity-logs');
|
||||
}
|
||||
|
||||
public function view(User $user, ActivityLog $activityLog): bool
|
||||
{
|
||||
return $user->hasPermissionTo('view-activity-logs');
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, ActivityLog $activityLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, ActivityLog $activityLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
27
app/Services/ActivityLogger.php
Normal file
27
app/Services/ActivityLogger.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ActivityLogger
|
||||
{
|
||||
public static function log(
|
||||
string $action,
|
||||
string $description,
|
||||
?Model $subject = null,
|
||||
?array $metadata = null,
|
||||
?int $userId = null,
|
||||
): ActivityLog {
|
||||
return ActivityLog::create([
|
||||
'user_id' => $userId ?? auth()->id(),
|
||||
'action' => $action,
|
||||
'description' => $description,
|
||||
'subject_type' => $subject ? get_class($subject) : null,
|
||||
'subject_id' => $subject?->id,
|
||||
'metadata' => $metadata,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,20 @@ class ClosingService
|
||||
]);
|
||||
}
|
||||
|
||||
$previousStatus = $feedback->status;
|
||||
|
||||
$feedback->update(['status' => 'closed']);
|
||||
|
||||
ActivityLogger::log(
|
||||
'close',
|
||||
__('feedback.log_close', ['title' => $feedback->title]),
|
||||
$feedback,
|
||||
[
|
||||
'feedback_id' => $feedback->id,
|
||||
'previous_status' => $previousStatus,
|
||||
],
|
||||
);
|
||||
|
||||
$this->notifyStakeholders($feedback, $actor);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class FileService
|
||||
'video/mov',
|
||||
];
|
||||
|
||||
protected int $maxSize = 20480;
|
||||
protected int $maxSize = 51200;
|
||||
|
||||
protected string $defaultDisk = 'public';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Department;
|
||||
use App\Models\Feedback;
|
||||
use App\Models\TicketTransferLog;
|
||||
use App\Models\User;
|
||||
use App\Services\ActivityLogger;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class TransferService
|
||||
@@ -24,6 +25,7 @@ class TransferService
|
||||
}
|
||||
|
||||
$fromDepartmentId = $feedback->current_department_id;
|
||||
$fromDepartmentName = $feedback->currentDepartment?->name ?? 'N/A';
|
||||
|
||||
$log = TicketTransferLog::create([
|
||||
'feedback_id' => $feedback->id,
|
||||
@@ -40,6 +42,22 @@ class TransferService
|
||||
|
||||
$this->notifyDepartmentManager($toDepartment, $feedback, $log, $sender);
|
||||
|
||||
ActivityLogger::log(
|
||||
'transfer',
|
||||
__('feedback.log_transfer', [
|
||||
'title' => $feedback->title,
|
||||
'from' => $fromDepartmentName,
|
||||
'to' => $toDepartment->name,
|
||||
]),
|
||||
$feedback,
|
||||
[
|
||||
'feedback_id' => $feedback->id,
|
||||
'from_department' => $fromDepartmentName,
|
||||
'to_department' => $toDepartment->name,
|
||||
'reason' => $reason,
|
||||
],
|
||||
);
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user