them log, chinh upload
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
> - `CODEBASE_SNAPSHOT.md` — Toàn bộ cấu trúc codebase (thay vì scan lại code)
|
> - `CODEBASE_SNAPSHOT.md` — Toàn bộ cấu trúc codebase (thay vì scan lại code)
|
||||||
> - `TASKS_ROADMAP.md` — Roadmap: đã làm, đang làm, sẽ làm
|
> - `TASKS_ROADMAP.md` — Roadmap: đã làm, đang làm, sẽ làm
|
||||||
> - `PROGRESS.md` — Lịch sử tiến độ hoàn thành
|
> - `PROGRESS.md` — Lịch sử tiến độ hoàn thành
|
||||||
|
> - `docs/I18N_GUIDE.md` — Hướng dẫn đa ngôn ngữ
|
||||||
|
> - `docs/ENUM_GUIDE.md` — Hướng dẫn thêm Enum mới
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
AfterSales CRM is a real-estate after-sales customer care system built with **Laravel 13.6** + **Filament 5.6**.
|
AfterSales CRM is a real-estate after-sales customer care system built with **Laravel 13.6** + **Filament 5.6**.
|
||||||
|
|||||||
13
PROGRESS.md
13
PROGRESS.md
@@ -152,6 +152,17 @@ Tham chiếu: `SOP_des.md` — Bản đặc tả hệ thống After-Sale CRM
|
|||||||
- [x] Filament vendor translations (57 Vietnamese files) auto-activated for UI chrome
|
- [x] Filament vendor translations (57 Vietnamese files) auto-activated for UI chrome
|
||||||
- [x] Tests: 8/8 pass (21 assertions)
|
- [x] Tests: 8/8 pass (21 assertions)
|
||||||
|
|
||||||
|
### UI Polish & Bug Fixes
|
||||||
|
- [x] File upload: Fixed disk('local') → disk('public') across all components
|
||||||
|
- [x] File upload: Added exists() check before getting metadata
|
||||||
|
- [x] File upload: Fixed dehydrated(false) issue - use property to store paths
|
||||||
|
- [x] Tailwind CSS: Added ~200 utility classes to filament-custom.css
|
||||||
|
- [x] SVG icons: Use inline width/height attributes instead of Tailwind classes
|
||||||
|
- [x] GlobalSearch: Customized for all 6 Resources with multi-field search
|
||||||
|
- [x] RelationManager titles: Override getTitle() for proper translation
|
||||||
|
- [x] Enum refactoring: Added color() method, static options(), developer comments
|
||||||
|
- [x] Documentation: Created docs/I18N_GUIDE.md and docs/ENUM_GUIDE.md
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database Schema (final — 23 migrations)
|
## Database Schema (final — 23 migrations)
|
||||||
@@ -187,3 +198,5 @@ php artisan test # 8 tests (21 assertions)
|
|||||||
- `CODEBASE_SNAPSHOT.md` — Toàn bộ cấu trúc codebase (đọc thay vì scan code)
|
- `CODEBASE_SNAPSHOT.md` — Toàn bộ cấu trúc codebase (đọc thay vì scan code)
|
||||||
- `TASKS_ROADMAP.md` — Roadmap: đã làm, đang làm, sẽ làm
|
- `TASKS_ROADMAP.md` — Roadmap: đã làm, đang làm, sẽ làm
|
||||||
- `AGENTS.md` — Hướng dẫn chạy, workflow, RBAC, gotchas
|
- `AGENTS.md` — Hướng dẫn chạy, workflow, RBAC, gotchas
|
||||||
|
- `docs/I18N_GUIDE.md` — Hướng dẫn đa ngôn ngữ chi tiết
|
||||||
|
- `docs/ENUM_GUIDE.md` — Hướng dẫn thêm Enum mới
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
@@ -71,6 +72,17 @@ class ExportBackup extends Command
|
|||||||
|
|
||||||
$this->printSummary($backupDir, $totalRows);
|
$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;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
use App\Models\Contract;
|
use App\Models\Contract;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Models\CustomerProduct;
|
use App\Models\CustomerProduct;
|
||||||
@@ -52,6 +53,8 @@ class ImportCskh extends Command
|
|||||||
$this->warn('=== DRY RUN MODE — No data will be written ===');
|
$this->warn('=== DRY RUN MODE — No data will be written ===');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$startTime = now();
|
||||||
|
|
||||||
$rows = SimpleExcelReader::create($filePath)->getRows();
|
$rows = SimpleExcelReader::create($filePath)->getRows();
|
||||||
|
|
||||||
$total = iterator_count($rows);
|
$total = iterator_count($rows);
|
||||||
@@ -94,6 +97,29 @@ class ImportCskh extends Command
|
|||||||
$this->newLine(2);
|
$this->newLine(2);
|
||||||
$this->printSummary($rowCount, $dryRun);
|
$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;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Enums;
|
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
|
enum ContractStatus: string
|
||||||
{
|
{
|
||||||
case ACTIVE = 'active';
|
case ACTIVE = 'active';
|
||||||
@@ -17,12 +25,19 @@ enum ContractStatus: string
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function options(): array
|
public function color(): string
|
||||||
{
|
{
|
||||||
return [
|
return match ($this) {
|
||||||
self::ACTIVE->value => self::ACTIVE->label(),
|
self::ACTIVE => 'success',
|
||||||
self::EXPIRED->value => self::EXPIRED->label(),
|
self::EXPIRED => 'warning',
|
||||||
self::LIQUIDATED->value => self::LIQUIDATED->label(),
|
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;
|
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
|
enum ContractType: string
|
||||||
{
|
{
|
||||||
case SALE = 'sale';
|
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
|
public static function options(): array
|
||||||
{
|
{
|
||||||
return [
|
return collect(self::cases())->mapWithKeys(fn ($case) => [
|
||||||
self::SALE->value => self::SALE->label(),
|
$case->value => $case->label(),
|
||||||
self::LEASE->value => self::LEASE->label(),
|
])->toArray();
|
||||||
self::BCC->value => self::BCC->label(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Enums;
|
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
|
enum HandoverStatus: string
|
||||||
{
|
{
|
||||||
case NOT_READY = 'chua_du_dieu_kien';
|
case NOT_READY = 'chua_du_dieu_kien';
|
||||||
@@ -19,13 +27,20 @@ enum HandoverStatus: string
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function options(): array
|
public function color(): string
|
||||||
{
|
{
|
||||||
return [
|
return match ($this) {
|
||||||
self::NOT_READY->value => self::NOT_READY->label(),
|
self::NOT_READY => 'danger',
|
||||||
self::READY->value => self::READY->label(),
|
self::READY => 'warning',
|
||||||
self::SCHEDULED->value => self::SCHEDULED->label(),
|
self::HANDED_OVER => 'success',
|
||||||
self::HANDED_OVER->value => self::HANDED_OVER->label(),
|
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;
|
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
|
enum ServiceType: string
|
||||||
{
|
{
|
||||||
case MANAGEMENT_FEE = 'management_fee';
|
case MANAGEMENT_FEE = 'management_fee';
|
||||||
@@ -17,12 +25,19 @@ enum ServiceType: string
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function options(): array
|
public function color(): string
|
||||||
{
|
{
|
||||||
return [
|
return match ($this) {
|
||||||
self::MANAGEMENT_FEE->value => self::MANAGEMENT_FEE->label(),
|
self::MANAGEMENT_FEE => 'info',
|
||||||
self::GRATITUDE->value => self::GRATITUDE->label(),
|
self::GRATITUDE => 'success',
|
||||||
self::SELF_BUSINESS->value => self::SELF_BUSINESS->label(),
|
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 $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
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class CreateFeedback extends CreateRecord
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($feedback->attachments()->where('path', $path)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$feedback->attachments()->create([
|
$feedback->attachments()->create([
|
||||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||||
'user_id' => auth()->id(),
|
'user_id' => auth()->id(),
|
||||||
@@ -56,9 +60,27 @@ class CreateFeedback extends CreateRecord
|
|||||||
'path' => $path,
|
'path' => $path,
|
||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
'collection' => $this->pendingCollection,
|
'collection' => $this->pendingCollection,
|
||||||
'mime_type' => $disk->mimeType($path),
|
'mime_type' => $this->safeMimeType($disk, $path),
|
||||||
'size' => $disk->size($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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($feedback->attachments()->where('path', $path)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$feedback->attachments()->create([
|
$feedback->attachments()->create([
|
||||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||||
'user_id' => $sender->id,
|
'user_id' => $sender->id,
|
||||||
@@ -97,8 +101,8 @@ class EditFeedback extends EditRecord
|
|||||||
'path' => $path,
|
'path' => $path,
|
||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
'collection' => 'general',
|
'collection' => 'general',
|
||||||
'mime_type' => $disk->mimeType($path),
|
'mime_type' => $this->safeMimeType($disk, $path),
|
||||||
'size' => $disk->size($path),
|
'size' => $this->safeFileSize($disk, $path),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,6 +198,10 @@ class EditFeedback extends EditRecord
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($feedback->attachments()->where('path', $path)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$feedback->attachments()->create([
|
$feedback->attachments()->create([
|
||||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||||
'user_id' => auth()->id(),
|
'user_id' => auth()->id(),
|
||||||
@@ -201,12 +209,30 @@ class EditFeedback extends EditRecord
|
|||||||
'path' => $path,
|
'path' => $path,
|
||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
'collection' => $this->pendingCollection,
|
'collection' => $this->pendingCollection,
|
||||||
'mime_type' => $disk->mimeType($path),
|
'mime_type' => $this->safeMimeType($disk, $path),
|
||||||
'size' => $disk->size($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
|
protected function mutateFormDataBeforeFill(array $data): array
|
||||||
{
|
{
|
||||||
$data['is_general'] = $data['customer_product_id'] === null;
|
$data['is_general'] = $data['customer_product_id'] === null;
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ class InteractionsRelationManager extends RelationManager
|
|||||||
{
|
{
|
||||||
protected static string $relationship = 'interactions';
|
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
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
@@ -211,6 +214,10 @@ class InteractionsRelationManager extends RelationManager
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($record->attachments()->where('path', $path)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$record->attachments()->create([
|
$record->attachments()->create([
|
||||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||||
'feedback_id' => $record->feedback_id,
|
'feedback_id' => $record->feedback_id,
|
||||||
@@ -219,8 +226,8 @@ class InteractionsRelationManager extends RelationManager
|
|||||||
'path' => $path,
|
'path' => $path,
|
||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
'collection' => 'general',
|
'collection' => 'general',
|
||||||
'mime_type' => $disk->mimeType($path),
|
'mime_type' => $this->safeMimeType($disk, $path),
|
||||||
'size' => $disk->size($path),
|
'size' => $this->safeFileSize($disk, $path),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,4 +269,22 @@ class InteractionsRelationManager extends RelationManager
|
|||||||
throw new \Filament\Exceptions\SilentActionException;
|
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\ContractStatus;
|
||||||
use App\Enums\ContractType;
|
use App\Enums\ContractType;
|
||||||
use App\Filament\Resources\Contracts\ContractResource;
|
use App\Filament\Resources\Contracts\ContractResource;
|
||||||
|
use App\Models\Feedback;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Filter;
|
use Filament\Tables\Filter;
|
||||||
@@ -15,7 +16,10 @@ class ContractsRelationManager extends RelationManager
|
|||||||
{
|
{
|
||||||
protected static string $relationship = 'contracts';
|
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
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ class HandoversRelationManager extends RelationManager
|
|||||||
{
|
{
|
||||||
protected static string $relationship = 'handovers';
|
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
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
namespace App\Filament\Resources\Products\RelationManagers;
|
namespace App\Filament\Resources\Products\RelationManagers;
|
||||||
|
|
||||||
use App\Enums\ServiceType;
|
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\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -12,7 +19,39 @@ class ProductServicesRelationManager extends RelationManager
|
|||||||
{
|
{
|
||||||
protected static string $relationship = 'productServices';
|
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
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
@@ -20,15 +59,12 @@ class ProductServicesRelationManager extends RelationManager
|
|||||||
->recordTitleAttribute('service_type')
|
->recordTitleAttribute('service_type')
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('service_type')
|
TextColumn::make('service_type')
|
||||||
|
->label(__('enums.service_type'))
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (string $state): string => match ($state) {
|
->color(fn (string $state): string => ServiceType::from($state)->color())
|
||||||
'management_fee' => 'info',
|
|
||||||
'gratitude' => 'success',
|
|
||||||
'self_business' => 'warning',
|
|
||||||
default => 'gray',
|
|
||||||
})
|
|
||||||
->formatStateUsing(fn (string $state): string => ServiceType::from($state)->label()),
|
->formatStateUsing(fn (string $state): string => ServiceType::from($state)->label()),
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
|
->label(__('enums.status'))
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (string $state): string => match ($state) {
|
->color(fn (string $state): string => match ($state) {
|
||||||
'active' => 'success',
|
'active' => 'success',
|
||||||
@@ -52,8 +88,10 @@ class ProductServicesRelationManager extends RelationManager
|
|||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('service_type')
|
SelectFilter::make('service_type')
|
||||||
->options(ServiceType::MANAGEMENT_FEE->options()),
|
->label(__('enums.service_type'))
|
||||||
|
->options(ServiceType::options()),
|
||||||
SelectFilter::make('status')
|
SelectFilter::make('status')
|
||||||
|
->label(__('enums.status'))
|
||||||
->options([
|
->options([
|
||||||
'active' => __('app.status_active'),
|
'active' => __('app.status_active'),
|
||||||
'pending' => __('app.status_pending'),
|
'pending' => __('app.status_pending'),
|
||||||
@@ -62,7 +100,12 @@ class ProductServicesRelationManager extends RelationManager
|
|||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->headerActions([])
|
->headerActions([
|
||||||
->recordActions([]);
|
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']);
|
$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);
|
$this->notifyStakeholders($feedback, $actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class FileService
|
|||||||
'video/mov',
|
'video/mov',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected int $maxSize = 20480;
|
protected int $maxSize = 51200;
|
||||||
|
|
||||||
protected string $defaultDisk = 'public';
|
protected string $defaultDisk = 'public';
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Models\Department;
|
|||||||
use App\Models\Feedback;
|
use App\Models\Feedback;
|
||||||
use App\Models\TicketTransferLog;
|
use App\Models\TicketTransferLog;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class TransferService
|
class TransferService
|
||||||
@@ -24,6 +25,7 @@ class TransferService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$fromDepartmentId = $feedback->current_department_id;
|
$fromDepartmentId = $feedback->current_department_id;
|
||||||
|
$fromDepartmentName = $feedback->currentDepartment?->name ?? 'N/A';
|
||||||
|
|
||||||
$log = TicketTransferLog::create([
|
$log = TicketTransferLog::create([
|
||||||
'feedback_id' => $feedback->id,
|
'feedback_id' => $feedback->id,
|
||||||
@@ -40,6 +42,22 @@ class TransferService
|
|||||||
|
|
||||||
$this->notifyDepartmentManager($toDepartment, $feedback, $log, $sender);
|
$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;
|
return $log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('activity_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->string('action');
|
||||||
|
$table->text('description');
|
||||||
|
$table->nullableMorphs('subject');
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->timestamp('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('activity_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -56,6 +56,9 @@ class PermissionSeeder extends Seeder
|
|||||||
// Dashboard & Export
|
// Dashboard & Export
|
||||||
'view-dashboard-widgets',
|
'view-dashboard-widgets',
|
||||||
'export-data',
|
'export-data',
|
||||||
|
|
||||||
|
// Activity Logs
|
||||||
|
'view-activity-logs',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
@@ -92,6 +95,7 @@ class PermissionSeeder extends Seeder
|
|||||||
'create-tag',
|
'create-tag',
|
||||||
'update-tag',
|
'update-tag',
|
||||||
'view-dashboard-widgets',
|
'view-dashboard-widgets',
|
||||||
|
'view-activity-logs',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Staff: read + limited write
|
// Staff: read + limited write
|
||||||
|
|||||||
233
docs/ENUM_GUIDE.md
Normal file
233
docs/ENUM_GUIDE.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# Hướng dẫn thêm Enum mới
|
||||||
|
|
||||||
|
## Tổng quan
|
||||||
|
|
||||||
|
Hệ thống sử dụng PHP Enums với i18n support. Khi cần thêm giá trị enum mới (Service Type, Contract Type, v.v.), chỉ cần cập nhật 3 chỗ.
|
||||||
|
|
||||||
|
## Cấu trúc Enum hiện có
|
||||||
|
|
||||||
|
```
|
||||||
|
app/Enums/
|
||||||
|
├── ContractStatus.php # Trạng thái hợp đồng
|
||||||
|
├── ContractType.php # Loại hợp đồng
|
||||||
|
├── HandoverStatus.php # Trạng thái bàn giao
|
||||||
|
└── ServiceType.php # Loại dịch vụ
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ví dụ: Thêm Service Type mới
|
||||||
|
|
||||||
|
Giả sử cần thêm loại dịch vụ "Bảo trì định kỳ" (maintenance).
|
||||||
|
|
||||||
|
### Bước 1: Thêm case vào Enum
|
||||||
|
|
||||||
|
**File:** `app/Enums/ServiceType.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
enum ServiceType: string
|
||||||
|
{
|
||||||
|
case MANAGEMENT_FEE = 'management_fee';
|
||||||
|
case GRATITUDE = 'gratitude';
|
||||||
|
case SELF_BUSINESS = 'self_business';
|
||||||
|
case MAINTENANCE = 'maintenance'; // ← THÊM MỚI
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::MANAGEMENT_FEE => __('enums.service.management_fee'),
|
||||||
|
self::GRATITUDE => __('enums.service.gratitude'),
|
||||||
|
self::SELF_BUSINESS => __('enums.service.self_business'),
|
||||||
|
self::MAINTENANCE => __('enums.service.maintenance'), // ← THÊM MỚI
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::MANAGEMENT_FEE => 'info',
|
||||||
|
self::GRATITUDE => 'success',
|
||||||
|
self::SELF_BUSINESS => 'warning',
|
||||||
|
self::MAINTENANCE => 'primary', // ← THÊM MỚI
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// options() tự động lấy tất cả cases - KHÔNG cần sửa
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bước 2: Thêm translation tiếng Việt
|
||||||
|
|
||||||
|
**File:** `lang/vi/enums.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
// ... existing keys ...
|
||||||
|
|
||||||
|
'service' => [
|
||||||
|
'management_fee' => 'Phí quản lý',
|
||||||
|
'gratitude' => 'Tri ân',
|
||||||
|
'self_business' => 'Tự doanh',
|
||||||
|
'maintenance' => 'Bảo trì định kỳ', // ← THÊM MỚI
|
||||||
|
],
|
||||||
|
|
||||||
|
// ... existing keys ...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bước 3: Thêm translation tiếng Anh
|
||||||
|
|
||||||
|
**File:** `lang/en/enums.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
// ... existing keys ...
|
||||||
|
|
||||||
|
'service' => [
|
||||||
|
'management_fee' => 'Management fee',
|
||||||
|
'gratitude' => 'Gratitude',
|
||||||
|
'self_business' => 'Self business',
|
||||||
|
'maintenance' => 'Periodic maintenance', // ← THÊM MỚI
|
||||||
|
],
|
||||||
|
|
||||||
|
// ... existing keys ...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bước 4: Reload browser
|
||||||
|
|
||||||
|
Không cần chạy lệnh nào, chỉ cần reload trang.
|
||||||
|
|
||||||
|
## Ví dụ: Thêm Contract Type mới
|
||||||
|
|
||||||
|
### Bước 1: Thêm case
|
||||||
|
|
||||||
|
**File:** `app/Enums/ContractType.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
enum ContractType: string
|
||||||
|
{
|
||||||
|
case SALE = 'sale';
|
||||||
|
case LEASE = 'lease';
|
||||||
|
case BCC = 'bcc';
|
||||||
|
case CONSORTIUM = 'consortium'; // ← THÊM MỚI
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::SALE => __('enums.contract_type.sale'),
|
||||||
|
self::LEASE => __('enums.contract_type.lease'),
|
||||||
|
self::BCC => __('enums.contract_type.bcc'),
|
||||||
|
self::CONSORTIUM => __('enums.contract_type.consortium'), // ← THÊM MỚI
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::SALE => 'success',
|
||||||
|
self::LEASE => 'info',
|
||||||
|
self::BCC => 'warning',
|
||||||
|
self::CONSORTIUM => 'primary', // ← THÊM MỚI
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bước 2-3: Thêm translations
|
||||||
|
|
||||||
|
```php
|
||||||
|
// lang/vi/enums.php
|
||||||
|
'contract_type' => [
|
||||||
|
'sale' => 'HĐ Mua bán',
|
||||||
|
'lease' => 'HĐ Thuê',
|
||||||
|
'bcc' => 'HĐ Hợp tác kinh doanh (BCC)',
|
||||||
|
'consortium' => 'HĐ Liên doanh', // ← THÊM MỚI
|
||||||
|
],
|
||||||
|
|
||||||
|
// lang/en/enums.php
|
||||||
|
'contract_type' => [
|
||||||
|
'sale' => 'Sale contract',
|
||||||
|
'lease' => 'Lease contract',
|
||||||
|
'bcc' => 'Business cooperation contract (BCC)',
|
||||||
|
'consortium' => 'Consortium contract', // ← THÊM MỚI
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Enum (Copy-paste)
|
||||||
|
|
||||||
|
Khi tạo enum mới, copy template sau:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Tên] Enum
|
||||||
|
*
|
||||||
|
* Để thêm giá trị 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 [EnumName]: string
|
||||||
|
{
|
||||||
|
case [CASE_1] = '[value_1]';
|
||||||
|
case [CASE_2] = '[value_2]';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::[CASE_1] => __('enums.[enum_name].[value_1]'),
|
||||||
|
self::[CASE_2] => __('enums.[enum_name].[value_2]'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::[CASE_1] => 'success',
|
||||||
|
self::[CASE_2] => 'info',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function options(): array
|
||||||
|
{
|
||||||
|
return collect(self::cases())->mapWithKeys(fn ($case) => [
|
||||||
|
$case->value => $case->label(),
|
||||||
|
])->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cách sử dụng Enum trong code
|
||||||
|
|
||||||
|
### Trong Filament Form
|
||||||
|
```php
|
||||||
|
Select::make('service_type')
|
||||||
|
->label(__('enums.service_type'))
|
||||||
|
->options(ServiceType::options())
|
||||||
|
->required(),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trong Filament Table
|
||||||
|
```php
|
||||||
|
TextColumn::make('service_type')
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state): string => ServiceType::from($state)->color())
|
||||||
|
->formatStateUsing(fn (string $state): string => ServiceType::from($state)->label()),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trong PHP code
|
||||||
|
```php
|
||||||
|
$type = ServiceType::MANAGEMENT_FEE;
|
||||||
|
echo $type->label(); // "Phí quản lý"
|
||||||
|
echo $type->value; // "management_fee"
|
||||||
|
echo $type->color(); // "info"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lưu ý
|
||||||
|
|
||||||
|
1. **`options()` method** - PHẢI là `static` (không phải instance method)
|
||||||
|
2. **Translation keys** - Phải có trong CẢ `lang/vi/enums.php` VÀ `lang/en/enums.php`
|
||||||
|
3. **Color values** - Sử dụng Filament colors: `primary`, `success`, `warning`, `danger`, `info`, `gray`
|
||||||
|
4. **Case values** - Nên dùng snake_case (ví dụ: `management_fee`, không phải `ManagementFee`)
|
||||||
162
docs/I18N_GUIDE.md
Normal file
162
docs/I18N_GUIDE.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Hướng dẫn Đa ngôn ngữ (i18n)
|
||||||
|
|
||||||
|
## Tổng quan
|
||||||
|
|
||||||
|
Hệ thống hỗ trợ 2 ngôn ngữ:
|
||||||
|
- **Tiếng Việt (vi)** - Ngôn ngữ chính
|
||||||
|
- **Tiếng Anh (en)** - Ngôn ngữ dự phòng
|
||||||
|
|
||||||
|
Cấu hình trong `.env`:
|
||||||
|
```
|
||||||
|
APP_LOCALE=vi
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cấu trúc thư mục
|
||||||
|
|
||||||
|
```
|
||||||
|
lang/
|
||||||
|
├── en/
|
||||||
|
│ ├── app.php # UI chung: navigation, labels, status, widgets
|
||||||
|
│ ├── feedback.php # Domain feedback: services, notifications, interactions
|
||||||
|
│ └── enums.php # Labels cho enums
|
||||||
|
└── vi/
|
||||||
|
├── app.php
|
||||||
|
├── feedback.php
|
||||||
|
└── enums.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quy tắc đặt tên Translation Keys
|
||||||
|
|
||||||
|
### 1. Resource Labels
|
||||||
|
```php
|
||||||
|
// Pattern: app.resource_{name}
|
||||||
|
'resource_feedbacks' => 'Phản ánh',
|
||||||
|
'resource_products' => 'Sản phẩm',
|
||||||
|
'resource_customers' => 'Khách hàng',
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Status Labels
|
||||||
|
```php
|
||||||
|
// Pattern: app.status_{name}
|
||||||
|
'status_pending' => 'Chờ xử lý',
|
||||||
|
'status_processing' => 'Đang xử lý',
|
||||||
|
'status_resolved' => 'Đã xử lý',
|
||||||
|
'status_closed' => 'Đã đóng',
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Form Labels
|
||||||
|
```php
|
||||||
|
// Pattern: app.{field_name}
|
||||||
|
'name' => 'Tên',
|
||||||
|
'email' => 'Email',
|
||||||
|
'phone' => 'Điện thoại',
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Enum Labels
|
||||||
|
```php
|
||||||
|
// Pattern: enums.{enum_name}.{case_value}
|
||||||
|
'service.management_fee' => 'Phí quản lý',
|
||||||
|
'contract_status.active' => 'Đang hiệu lực',
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Feedback Domain
|
||||||
|
```php
|
||||||
|
// Pattern: feedback.{action}_{context}
|
||||||
|
'already_closed' => 'Phiếu này đã được đóng.',
|
||||||
|
'close_permission_denied' => 'Chỉ lãnh đạo cấp phòng mới có quyền đóng phiếu.',
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cách sử dụng trong code
|
||||||
|
|
||||||
|
### Resource Labels
|
||||||
|
```php
|
||||||
|
// Phải override getPluralLabel() - KHÔNG tự dịch
|
||||||
|
public static function getPluralLabel(): ?string
|
||||||
|
{
|
||||||
|
return __('app.resource_feedbacks');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phải override getNavigationGroup() - KHÔNG tự dịch
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('app.nav_customer_care');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Fields
|
||||||
|
```php
|
||||||
|
// PHẢI thêm ->label() explicit
|
||||||
|
TextInput::make('name')->label(__('app.name'))
|
||||||
|
Select::make('status')->label(__('app.status'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### RelationManager Title
|
||||||
|
```php
|
||||||
|
// Phải override getTitle() - KHÔNG dùng $title property
|
||||||
|
public static function getTitle(Model $ownerRecord, string $pageClass): string
|
||||||
|
{
|
||||||
|
return __('app.services');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget Heading/Description
|
||||||
|
```php
|
||||||
|
// Phải override getHeading()/getDescription() - KHÔNG dùng property
|
||||||
|
public function getHeading(): string | Htmlable | null
|
||||||
|
{
|
||||||
|
return __('app.widget_avg_time_by_handler');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enum Labels
|
||||||
|
```php
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::ACTIVE => __('enums.contract_status.active'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cách thêm Translation Key mới
|
||||||
|
|
||||||
|
### Bước 1: Thêm vào `lang/vi/app.php`
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
// ... existing keys ...
|
||||||
|
'new_key' => 'Giá trị tiếng Việt',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bước 2: Thêm vào `lang/en/app.php`
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
// ... existing keys ...
|
||||||
|
'new_key' => 'English value',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bước 3: Sử dụng trong code
|
||||||
|
```php
|
||||||
|
echo __('app.new_key');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lưu ý quan trọng
|
||||||
|
|
||||||
|
1. **`$pluralLabel` property** - KHÔNG tự dịch, phải override `getPluralLabel()`
|
||||||
|
2. **`getNavigationGroup()`** - KHÔNG tự dịch, phải gọi `__()`
|
||||||
|
3. **`$heading`/`$description`** trong ChartWidget - KHÔNG tự dịch, phải override methods
|
||||||
|
4. **Form fields** - PHẢI thêm `->label()` explicit
|
||||||
|
5. **`$title`** trong RelationManager - KHÔNG tự dịch, phải override `getTitle()`
|
||||||
|
6. **Filament vendor** - Có sẵn 57 file dịch tiếng Việt, tự kích hoạt khi `APP_LOCALE=vi`
|
||||||
|
|
||||||
|
## Kiểm tra Translation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Kiểm tra key có tồn tại không
|
||||||
|
php artisan tinker --execute="echo __('app.new_key');"
|
||||||
|
|
||||||
|
# Kiểm tra locale hiện tại
|
||||||
|
php artisan tinker --execute="echo app()->getLocale();"
|
||||||
|
```
|
||||||
@@ -14,6 +14,7 @@ return [
|
|||||||
'resource_tags' => 'Tags',
|
'resource_tags' => 'Tags',
|
||||||
'resource_roles' => 'Roles',
|
'resource_roles' => 'Roles',
|
||||||
'resource_users' => 'Users',
|
'resource_users' => 'Users',
|
||||||
|
'resource_activity_logs' => 'Activity Logs',
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
'status_pending' => 'Pending',
|
'status_pending' => 'Pending',
|
||||||
@@ -89,6 +90,7 @@ return [
|
|||||||
'signed_status' => 'Signed Status',
|
'signed_status' => 'Signed Status',
|
||||||
'contract_info' => 'Contract Information',
|
'contract_info' => 'Contract Information',
|
||||||
'feedback_history' => 'Feedback History',
|
'feedback_history' => 'Feedback History',
|
||||||
|
'service_type' => 'Service Type',
|
||||||
'tab_all' => 'All',
|
'tab_all' => 'All',
|
||||||
'min' => 'min',
|
'min' => 'min',
|
||||||
'na' => 'N/A',
|
'na' => 'N/A',
|
||||||
@@ -130,4 +132,16 @@ return [
|
|||||||
'blade_file_count' => ':count file(s)',
|
'blade_file_count' => ':count file(s)',
|
||||||
'blade_proof' => 'Proof',
|
'blade_proof' => 'Proof',
|
||||||
'blade_evidence' => 'Evidence',
|
'blade_evidence' => 'Evidence',
|
||||||
|
|
||||||
|
// Activity Logs
|
||||||
|
'system' => 'System',
|
||||||
|
'time' => 'Time',
|
||||||
|
'action' => 'Action',
|
||||||
|
'action_import' => 'Import',
|
||||||
|
'action_export' => 'Export',
|
||||||
|
'action_transfer' => 'Transfer',
|
||||||
|
'action_close' => 'Close',
|
||||||
|
'action_create' => 'Create',
|
||||||
|
'action_update' => 'Update',
|
||||||
|
'action_delete' => 'Delete',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum Translations (English)
|
||||||
|
*
|
||||||
|
* To add new enum:
|
||||||
|
* 1. Add new array here (e.g., 'service_type' => [...])
|
||||||
|
* 2. Add corresponding translation in lang/vi/enums.php
|
||||||
|
* 3. Reload browser
|
||||||
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
// Handover Status
|
||||||
'handover' => [
|
'handover' => [
|
||||||
'not_ready' => 'Not ready',
|
'not_ready' => 'Not ready',
|
||||||
'ready' => 'Ready',
|
'ready' => 'Ready',
|
||||||
@@ -8,21 +18,30 @@ return [
|
|||||||
'scheduled' => 'Scheduled',
|
'scheduled' => 'Scheduled',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Service Type
|
||||||
'service' => [
|
'service' => [
|
||||||
'management_fee' => 'Management fee',
|
'management_fee' => 'Management fee',
|
||||||
'gratitude' => 'Gratitude',
|
'gratitude' => 'Gratitude',
|
||||||
'self_business' => 'Self business',
|
'self_business' => 'Self business',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Contract Status
|
||||||
'contract_status' => [
|
'contract_status' => [
|
||||||
'active' => 'Active',
|
'active' => 'Active',
|
||||||
'expired' => 'Expired',
|
'expired' => 'Expired',
|
||||||
'liquidated' => 'Liquidated',
|
'liquidated' => 'Liquidated',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Contract Type
|
||||||
'contract_type' => [
|
'contract_type' => [
|
||||||
'sale' => 'Sale contract',
|
'sale' => 'Sale contract',
|
||||||
'lease' => 'Lease contract',
|
'lease' => 'Lease contract',
|
||||||
'bcc' => 'Business cooperation contract (BCC)',
|
'bcc' => 'Business cooperation contract (BCC)',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Labels for Select filters
|
||||||
|
'service_type' => 'Service Type',
|
||||||
|
'status' => 'Status',
|
||||||
|
'handover_status' => 'Handover Status',
|
||||||
|
'contract_type_label' => 'Contract Type',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -54,4 +54,10 @@ return [
|
|||||||
// CreateFeedback
|
// CreateFeedback
|
||||||
'created_via' => 'Feedback created via :channel',
|
'created_via' => 'Feedback created via :channel',
|
||||||
'unknown_channel' => 'unknown channel',
|
'unknown_channel' => 'unknown channel',
|
||||||
|
|
||||||
|
// Activity Log
|
||||||
|
'log_import_done' => 'Import completed: :file - :rows rows',
|
||||||
|
'log_export_done' => 'Exported :tables tables, :rows rows',
|
||||||
|
'log_transfer' => ':title transferred from :from to :to',
|
||||||
|
'log_close' => 'Ticket closed: :title',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ return [
|
|||||||
'resource_tags' => 'Thẻ',
|
'resource_tags' => 'Thẻ',
|
||||||
'resource_roles' => 'Vai trò',
|
'resource_roles' => 'Vai trò',
|
||||||
'resource_users' => 'Người dùng',
|
'resource_users' => 'Người dùng',
|
||||||
|
'resource_activity_logs' => 'Nhật ký hoạt động',
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
'status_pending' => 'Chờ xử lý',
|
'status_pending' => 'Chờ xử lý',
|
||||||
@@ -89,6 +90,7 @@ return [
|
|||||||
'signed_status' => 'Tình trạng ký',
|
'signed_status' => 'Tình trạng ký',
|
||||||
'contract_info' => 'Thông tin hợp đồng',
|
'contract_info' => 'Thông tin hợp đồng',
|
||||||
'feedback_history' => 'Lịch sử phản ánh',
|
'feedback_history' => 'Lịch sử phản ánh',
|
||||||
|
'service_type' => 'Loại dịch vụ',
|
||||||
'tab_all' => 'Tất cả',
|
'tab_all' => 'Tất cả',
|
||||||
'min' => 'phút',
|
'min' => 'phút',
|
||||||
'na' => 'N/A',
|
'na' => 'N/A',
|
||||||
@@ -130,4 +132,16 @@ return [
|
|||||||
'blade_file_count' => ':count tệp',
|
'blade_file_count' => ':count tệp',
|
||||||
'blade_proof' => 'Bằng chứng',
|
'blade_proof' => 'Bằng chứng',
|
||||||
'blade_evidence' => 'Chứng cứ',
|
'blade_evidence' => 'Chứng cứ',
|
||||||
|
|
||||||
|
// Activity Logs
|
||||||
|
'system' => 'Hệ thống',
|
||||||
|
'time' => 'Thời gian',
|
||||||
|
'action' => 'Hành động',
|
||||||
|
'action_import' => 'Nhập dữ liệu',
|
||||||
|
'action_export' => 'Xuất dữ liệu',
|
||||||
|
'action_transfer' => 'Chuyển phòng ban',
|
||||||
|
'action_close' => 'Đóng ticket',
|
||||||
|
'action_create' => 'Tạo mới',
|
||||||
|
'action_update' => 'Cập nhật',
|
||||||
|
'action_delete' => 'Xóa',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum Translations (Vietnamese)
|
||||||
|
*
|
||||||
|
* Để thêm enum mới:
|
||||||
|
* 1. Thêm mảng mới vào đây (ví dụ: 'service_type' => [...])
|
||||||
|
* 2. Thêm bản dịch tương ứng trong lang/en/enums.php
|
||||||
|
* 3. Reload browser
|
||||||
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
// Handover Status
|
||||||
'handover' => [
|
'handover' => [
|
||||||
'not_ready' => 'Chưa đủ điều kiện',
|
'not_ready' => 'Chưa đủ điều kiện',
|
||||||
'ready' => 'Đủ điều kiện',
|
'ready' => 'Đủ điều kiện',
|
||||||
@@ -8,21 +18,30 @@ return [
|
|||||||
'scheduled' => 'Đã lên lịch',
|
'scheduled' => 'Đã lên lịch',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Service Type
|
||||||
'service' => [
|
'service' => [
|
||||||
'management_fee' => 'Phí quản lý',
|
'management_fee' => 'Phí quản lý',
|
||||||
'gratitude' => 'Tri ân',
|
'gratitude' => 'Tri ân',
|
||||||
'self_business' => 'Tự doanh',
|
'self_business' => 'Tự doanh',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Contract Status
|
||||||
'contract_status' => [
|
'contract_status' => [
|
||||||
'active' => 'Đang hiệu lực',
|
'active' => 'Đang hiệu lực',
|
||||||
'expired' => 'Hết hạn',
|
'expired' => 'Hết hạn',
|
||||||
'liquidated' => 'Đã thanh lý',
|
'liquidated' => 'Đã thanh lý',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Contract Type
|
||||||
'contract_type' => [
|
'contract_type' => [
|
||||||
'sale' => 'HĐ Mua bán',
|
'sale' => 'HĐ Mua bán',
|
||||||
'lease' => 'HĐ Thuê',
|
'lease' => 'HĐ Thuê',
|
||||||
'bcc' => 'HĐ Hợp tác kinh doanh (BCC)',
|
'bcc' => 'HĐ Hợp tác kinh doanh (BCC)',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Labels cho Select filters
|
||||||
|
'service_type' => 'Loại dịch vụ',
|
||||||
|
'status' => 'Trạng thái',
|
||||||
|
'handover_status' => 'Trạng thái bàn giao',
|
||||||
|
'contract_type_label' => 'Loại hợp đồng',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -54,4 +54,10 @@ return [
|
|||||||
// CreateFeedback
|
// CreateFeedback
|
||||||
'created_via' => 'Phản ánh được tạo qua :channel',
|
'created_via' => 'Phản ánh được tạo qua :channel',
|
||||||
'unknown_channel' => 'kênh không xác định',
|
'unknown_channel' => 'kênh không xác định',
|
||||||
|
|
||||||
|
// Activity Log
|
||||||
|
'log_import_done' => 'Nhập xong :file - :rows dòng',
|
||||||
|
'log_export_done' => 'Xuất :tables bảng, :rows dòng',
|
||||||
|
'log_transfer' => ':title: chuyển từ :from sang :to',
|
||||||
|
'log_close' => 'Đóng ticket: :title',
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user