Compare commits
6 Commits
c7b1d08afa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2366181476 | |||
| 5d9088d222 | |||
| da89a296c1 | |||
| 1c7d77a050 | |||
| 40b75fcf75 | |||
| d2df9edd69 |
21
AGENTS.md
21
AGENTS.md
@@ -331,10 +331,10 @@
|
|||||||
4. **Command generate schedule hàng loạt:** `php artisan contracts:generate-schedules` cho 139 hợp đồng đã import
|
4. **Command generate schedule hàng loạt:** `php artisan contracts:generate-schedules` cho 139 hợp đồng đã import
|
||||||
|
|
||||||
### Giai đoạn 2: Module Bổ sung (Ưu tiên TRUNG BÌNH)
|
### Giai đoạn 2: Module Bổ sung (Ưu tiên TRUNG BÌNH)
|
||||||
5. **PaymentFine Resource:** Quản lý tiền phạt chậm thanh toán
|
5. **PaymentFine Resource:** Quản lý tiền phạt chậm thanh toán ✅
|
||||||
6. **Appendix Resource:** Quản lý phụ lục hợp đồng
|
6. **Appendix Resource:** Quản lý phụ lục hợp đồng ✅
|
||||||
7. **Settlement Resource:** Quản lý thanh lý hợp đồng
|
7. **Settlement Resource:** Quản lý thanh lý hợp đồng ✅
|
||||||
8. **Discount Engine:** Tính toán tự động chiết khấu từ `discount_details` vào giá trị hợp đồng
|
8. **Discount Engine:** Tính toán tự động chiết khấu từ `discount_details` vào giá trị hợp đồng ✅
|
||||||
|
|
||||||
### Giai đoạn 3: Báo cáo & Tối ưu (Ưu tiên THẤP)
|
### Giai đoạn 3: Báo cáo & Tối ưu (Ưu tiên THẤP)
|
||||||
9. **Dashboard Tài chính:** Tổng doanh thu, dòng tiền dự kiến, công nợ phải thu ✅
|
9. **Dashboard Tài chính:** Tổng doanh thu, dòng tiền dự kiến, công nợ phải thu ✅
|
||||||
@@ -342,6 +342,19 @@
|
|||||||
11. **Export Excel:** Xuất báo cáo công nợ khách hàng ✅
|
11. **Export Excel:** Xuất báo cáo công nợ khách hàng ✅
|
||||||
12. **Notification:** Cảnh báo đợt thanh toán sắp đến hạn ✅
|
12. **Notification:** Cảnh báo đợt thanh toán sắp đến hạn ✅
|
||||||
|
|
||||||
|
### Giai đoạn 4: An toàn & Audit (Đang làm)
|
||||||
|
13. **Soft Delete:** Contract, Payment, Customer + Restore/ForceDelete UI ✅
|
||||||
|
14. **Payment.collected_by:** Ghi nhận ngườ thu tiền ✅
|
||||||
|
|
||||||
|
### Giai đoạn 5: Phân quyền (Thiết kế xong, chờ triển khai)
|
||||||
|
15. **Permission System (Hướng B - Tự viết, không Spatie):**
|
||||||
|
- `role_templates`: Mẫu nhóm với permissions JSONB
|
||||||
|
- `users`: role_template_id + extra_permissions + excluded_permissions
|
||||||
|
- Session cache effective permissions (tính 1 lần/login)
|
||||||
|
- `php artisan permissions:sync` thủ công khi thêm module
|
||||||
|
- Action mới mặc định TẮT, admin bật thủ công
|
||||||
|
- Xem chi tiết: `WORKFLOW.md` Phần VIII
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. CÂU LỆNH THƯỜNG DÙNG
|
## 7. CÂU LỆNH THƯỜNG DÙNG
|
||||||
|
|||||||
@@ -55,3 +55,58 @@ DB_HOST=127.0.0.1 ./vendor/bin/pest
|
|||||||
---
|
---
|
||||||
|
|
||||||
*Commit ngay lập tức trước khi tắt máy!*
|
*Commit ngay lập tức trước khi tắt máy!*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHIÊN TIẾP THEO - PHÂN QUYỀN (CÒN DỞ)
|
||||||
|
|
||||||
|
### Đã có:
|
||||||
|
- [x] Migration: permission_modules, role_templates, users columns
|
||||||
|
- [x] Models: PermissionModule, RoleTemplate
|
||||||
|
- [x] Command: `php artisan permissions:sync`
|
||||||
|
- [x] User Model: getEffectivePermissions(), hasEffectivePermission(), can() override
|
||||||
|
- [x] RoleTemplateResource: Form + Table + Pages (UI tạo mẫu nhóm)
|
||||||
|
- [x] UserResource: Form + Table + Pages (UI gán quyền user)
|
||||||
|
- [x] permissionActions trong 10 Resource
|
||||||
|
|
||||||
|
### CHƯA CÓ (Ưu tiên):
|
||||||
|
1. [ ] Áp dụng can() checks vào TẤT CẢ Resource
|
||||||
|
- Chưa có canViewAny(), canCreate(), canEdit(), canDelete()... trong Resource
|
||||||
|
- Cần thêm vào hoặc tạo base trait để auto check
|
||||||
|
- Hiện tại tất cả user vẫn full quyền!
|
||||||
|
|
||||||
|
2. [ ] Tạo seeder/sample data cho role_templates
|
||||||
|
- Admin: full quyền
|
||||||
|
- Sales: contracts CRUD, customers CRUD, products view
|
||||||
|
- Kế toán: payments CRUD, contracts view, reports view
|
||||||
|
|
||||||
|
3. [ ] Test User::can() override hoạt động đúng
|
||||||
|
- Login → tính effective permissions → lưu session
|
||||||
|
- Logout → xóa session
|
||||||
|
- can('contracts.create') → true/false đúng
|
||||||
|
|
||||||
|
### Cách áp dụng can() vào Resource (gợi ý):
|
||||||
|
```php
|
||||||
|
// Trong mỗi Resource class
|
||||||
|
public static function canViewAny(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()?->can('contracts.view') ?? false;
|
||||||
|
}
|
||||||
|
public static function canCreate(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()?->can('contracts.create') ?? false;
|
||||||
|
}
|
||||||
|
public static function canEdit($record): bool
|
||||||
|
{
|
||||||
|
return auth()->user()?->can('contracts.update') ?? false;
|
||||||
|
}
|
||||||
|
public static function canDelete($record): bool
|
||||||
|
{
|
||||||
|
return auth()->user()?->can('contracts.delete') ?? false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lưu ý:
|
||||||
|
- Không chạy `php artisan permissions:sync` tự động (chạy tay khi thêm module)
|
||||||
|
- Action mới mặc định TẮT
|
||||||
|
- Layout RoleTemplateForm đã fix full width
|
||||||
|
|||||||
105
WORKFLOW.md
105
WORKFLOW.md
@@ -452,7 +452,7 @@ User 1───* Notification (MorphMany)
|
|||||||
| # | Vấn đề | Mức độ | Ghi chú |
|
| # | Vấn đề | Mức độ | Ghi chú |
|
||||||
|---|--------|--------|---------|
|
|---|--------|--------|---------|
|
||||||
| 1 | **~~Soft Delete~~ ✅** | 🟢 Đã xong | Contract, Payment, Customer có SoftDeletes + Restore/ForceDelete UI |
|
| 1 | **~~Soft Delete~~ ✅** | 🟢 Đã xong | Contract, Payment, Customer có SoftDeletes + Restore/ForceDelete UI |
|
||||||
| 2 | **Phân quyền** | 🟡 Trung bình | Chỉ 1 loại user. Ai cũng xóa/sửa được mọi thứ |
|
| 2 | **Phân quyền** | 🟡 Đang thiết kế | Kiến trúc Hướng B (Tự viết, không Spatie). Xem chi tiết Phần VIII |
|
||||||
| 3 | **~~Payment.collected_by~~ ✅** | 🟢 Đã xong | Đã thêm collected_by (FK → users) + hiển thị Form/Table |
|
| 3 | **~~Payment.collected_by~~ ✅** | 🟢 Đã xong | Đã thêm collected_by (FK → users) + hiển thị Form/Table |
|
||||||
| 4 | **Sổ quỹ** | 🟡 Trung bình | Thu tiền nhưng không ghi vào quỹ TM/NH. Không đối soát được |
|
| 4 | **Sổ quỹ** | 🟡 Trung bình | Thu tiền nhưng không ghi vào quỹ TM/NH. Không đối soát được |
|
||||||
| 5 | **CRM Pipeline** | 🟢 Thấp | Chưa quản lý Lead/Khách hàng tiềm năng |
|
| 5 | **CRM Pipeline** | 🟢 Thấp | Chưa quản lý Lead/Khách hàng tiềm năng |
|
||||||
@@ -460,4 +460,107 @@ User 1───* Notification (MorphMany)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VIII. PERMISSION SYSTEM DESIGN (Hướng B - Tự viết, không Spatie)
|
||||||
|
|
||||||
|
> **Quyết định kiến trúc:** Không dùng Spatie Permission. Tự viết module phân quyền đơn giản, đủ dùng, kiểm soát 100%.
|
||||||
|
> **Lý do:** Tránh config phức tạp, tránh xung đột UUID/BIGINT, phù hợp 5-10 role, không cần advanced features.
|
||||||
|
|
||||||
|
### Kiến trúc 3 lớp
|
||||||
|
|
||||||
|
```
|
||||||
|
LAYER 1: ROLE TEMPLATE (Mẫu nhóm)
|
||||||
|
role_templates.id(UUID) | name | description | permissions(JSONB) | is_active
|
||||||
|
|
||||||
|
permissions lưu dạng:
|
||||||
|
{
|
||||||
|
"contracts": ["view","create","update","delete","restore","forceDelete","export"],
|
||||||
|
"payments": ["view","create","update","delete"],
|
||||||
|
"customers": ["view","create","update","delete"]
|
||||||
|
}
|
||||||
|
|
||||||
|
LAYER 2: USER (Kế thừa + Override)
|
||||||
|
users.role_template_id(UUID) → nullable
|
||||||
|
users.extra_permissions(JSONB) → thêm quyền vượt cấp
|
||||||
|
users.excluded_permissions(JSONB) → bớt quyền so với mẫu
|
||||||
|
|
||||||
|
LAYER 3: EFFECTIVE PERMISSIONS (Tính toán động, cache trong Session)
|
||||||
|
effective = template_permissions + extra_permissions - excluded_permissions
|
||||||
|
|
||||||
|
Tính 1 lần khi user login → lưu vào session()->get('user.{id}.permissions')
|
||||||
|
Tự động xóa khi logout
|
||||||
|
Có thể xóa thủ công: auth()->user()->clearPermissionCache()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quy tắc khai báo Permission trong Resource
|
||||||
|
|
||||||
|
Mỗi Resource khai báo:
|
||||||
|
```php
|
||||||
|
class ContractResource extends Resource
|
||||||
|
{
|
||||||
|
protected static array $permissionActions = [
|
||||||
|
'view', 'create', 'update', 'delete',
|
||||||
|
'restore', 'forceDelete', 'export'
|
||||||
|
];
|
||||||
|
protected static string $permissionLabel = 'Hợp đồng';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Naming: `{snake_case_resource_name}.{action}` (ví dụ: `contracts.create`, `payments.delete`)
|
||||||
|
|
||||||
|
### Command đồng bộ (THỦ CÔNG - KHÔNG TỰ ĐỘNG)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan permissions:sync
|
||||||
|
```
|
||||||
|
|
||||||
|
**Khi nào chạy:**
|
||||||
|
- Khi thêm Resource mới
|
||||||
|
- Khi thêm/bớt `$permissionActions` trong Resource
|
||||||
|
- KHÔNG chạy tự động trong composer post-autoload-dump
|
||||||
|
|
||||||
|
**Command làm gì:**
|
||||||
|
1. Quét tất cả Filament Resources, lấy `$permissionActions` + `$permissionLabel`
|
||||||
|
2. Lưu vào bảng `permission_modules` (module, label, actions)
|
||||||
|
3. Action mới mặc định **TẮT** cho tất cả role hiện tại (an toàn)
|
||||||
|
4. Admin phải vào bật thủ công sau
|
||||||
|
|
||||||
|
### Luồng thêm module/action mới
|
||||||
|
|
||||||
|
**Step 1: Dev code**
|
||||||
|
- Tạo Resource mới / thêm action vào Resource
|
||||||
|
- Thêm `$permissionActions` vào Resource class
|
||||||
|
|
||||||
|
**Step 2: Dev chạy command**
|
||||||
|
- `php artisan permissions:sync`
|
||||||
|
|
||||||
|
**Step 3: Admin cấu hình**
|
||||||
|
- Vào Role Template UI → thấy module/action mới (badge "Mới")
|
||||||
|
- Tick bật quyền cho các nhóm cần dùng
|
||||||
|
|
||||||
|
### Các hàm kiểm tra quyền
|
||||||
|
|
||||||
|
```php
|
||||||
|
// User Model
|
||||||
|
public function getEffectivePermissions(): array;
|
||||||
|
public function hasEffectivePermission(string $permission): bool;
|
||||||
|
public function clearPermissionCache(): void;
|
||||||
|
|
||||||
|
// Trong Resource
|
||||||
|
public static function canCreate(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('contracts.create');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI quản lý (Filament Resources)
|
||||||
|
|
||||||
|
| Resource | Chức năng |
|
||||||
|
|----------|-----------|
|
||||||
|
| **RoleTemplateResource** | CRUD mẫu nhóm. Form chọn module → tick actions. CheckboxList per module |
|
||||||
|
| **UserPermissionPage** | Chọn user → hiện role template đang dùng → thêm/bớt quyền chi tiết → preview effective permissions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*File này cần được cập nhật mỗi khi có thay đổi lớn trong kiến trúc hoặc nghiệp vụ.*
|
*File này cần được cập nhật mỗi khi có thay đổi lớn trong kiến trúc hoặc nghiệp vụ.*
|
||||||
|
|||||||
180
app/Console/Commands/SyncPermissions.php
Normal file
180
app/Console/Commands/SyncPermissions.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\PermissionModule;
|
||||||
|
use App\Models\RoleTemplate;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use ReflectionClass;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
|
class SyncPermissions extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'permissions:sync {--dry-run : Chỉ liệt kê, không lưu}';
|
||||||
|
|
||||||
|
protected $description = 'Quét Filament Resources và đồng bộ permission modules. Action mới mặc định TẮT.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
$this->info('Đang quét Filament Resources...');
|
||||||
|
|
||||||
|
$resourcesPath = app_path('Filament/Resources');
|
||||||
|
if (! is_dir($resourcesPath)) {
|
||||||
|
$this->error('Không tìm thấy thư mục Filament/Resources');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modules = [];
|
||||||
|
$finder = new Finder();
|
||||||
|
$finder->files()->in($resourcesPath)->name('*Resource.php');
|
||||||
|
|
||||||
|
foreach ($finder as $file) {
|
||||||
|
$class = $this->getClassFromFile($file->getRealPath());
|
||||||
|
if (! $class || ! class_exists($class)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = new ReflectionClass($class);
|
||||||
|
|
||||||
|
if (! $ref->hasProperty('permissionActions')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prop = $ref->getProperty('permissionActions');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
$actions = $prop->getDefaultValue();
|
||||||
|
|
||||||
|
$label = 'Resource';
|
||||||
|
if ($ref->hasProperty('pluralModelLabel')) {
|
||||||
|
$labelProp = $ref->getProperty('pluralModelLabel');
|
||||||
|
$labelProp->setAccessible(true);
|
||||||
|
$label = $labelProp->getDefaultValue() ?? $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleName = Str::snake(class_basename($class));
|
||||||
|
$moduleName = str_replace('_resource', '', $moduleName);
|
||||||
|
|
||||||
|
$modules[] = [
|
||||||
|
'module' => $moduleName,
|
||||||
|
'label' => $label,
|
||||||
|
'actions' => $actions ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($modules)) {
|
||||||
|
$this->warn('Không tìm thấy Resource nào có $permissionActions.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Tìm thấy ' . count($modules) . ' module(s):');
|
||||||
|
|
||||||
|
foreach ($modules as $m) {
|
||||||
|
$this->line(" - {$m['module']}: " . implode(', ', $m['actions']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info('Dry-run: Không lưu thay đổi.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($modules as $m) {
|
||||||
|
$existing = PermissionModule::where('module', $m['module'])->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$oldActions = $existing->actions ?? [];
|
||||||
|
$newActions = $m['actions'];
|
||||||
|
|
||||||
|
if ($oldActions === $newActions) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$added = array_diff($newActions, $oldActions);
|
||||||
|
$removed = array_diff($oldActions, $newActions);
|
||||||
|
|
||||||
|
$existing->update([
|
||||||
|
'label' => $m['label'],
|
||||||
|
'actions' => $newActions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! empty($added)) {
|
||||||
|
$this->warn(" [{$m['module']}] Thêm actions: " . implode(', ', $added));
|
||||||
|
// Action mới mặc định TẮT cho tất cả role
|
||||||
|
$this->disableNewActionsForAllRoles($m['module'], $added);
|
||||||
|
}
|
||||||
|
if (! empty($removed)) {
|
||||||
|
$this->warn(" [{$m['module']}] Xóa actions: " . implode(', ', $removed));
|
||||||
|
$this->removeActionsFromAllRoles($m['module'], $removed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PermissionModule::create([
|
||||||
|
'module' => $m['module'],
|
||||||
|
'label' => $m['label'],
|
||||||
|
'actions' => $m['actions'],
|
||||||
|
]);
|
||||||
|
$this->info(" [{$m['module']}] Tạo mới module.");
|
||||||
|
// Module mới mặc định TẮT cho tất cả role
|
||||||
|
$this->disableNewActionsForAllRoles($m['module'], $m['actions']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Đồng bộ hoàn tất.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClassFromFile(string $path): ?string
|
||||||
|
{
|
||||||
|
$contents = file_get_contents($path);
|
||||||
|
|
||||||
|
// Tìm namespace
|
||||||
|
$namespace = null;
|
||||||
|
if (preg_match('/namespace\s+([^;]+);/', $contents, $matches)) {
|
||||||
|
$namespace = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tìm class name
|
||||||
|
$class = null;
|
||||||
|
if (preg_match('/class\s+(\w+)/', $contents, $matches)) {
|
||||||
|
$class = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($namespace && $class) {
|
||||||
|
return $namespace . '\\' . $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function disableNewActionsForAllRoles(string $module, array $actions): void
|
||||||
|
{
|
||||||
|
$roles = RoleTemplate::all();
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$perms = $role->permissions ?? [];
|
||||||
|
foreach ($actions as $action) {
|
||||||
|
if (isset($perms[$module]) && in_array($action, $perms[$module])) {
|
||||||
|
continue; // Đã có thì giữ nguyên
|
||||||
|
}
|
||||||
|
// Không thêm vào = mặc định TẮT
|
||||||
|
}
|
||||||
|
// Không cần update vì JSONB không lưu action mới = TẮT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeActionsFromAllRoles(string $module, array $actions): void
|
||||||
|
{
|
||||||
|
$roles = RoleTemplate::all();
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$perms = $role->permissions ?? [];
|
||||||
|
if (isset($perms[$module])) {
|
||||||
|
$perms[$module] = array_values(array_diff($perms[$module], $actions));
|
||||||
|
if (empty($perms[$module])) {
|
||||||
|
unset($perms[$module]);
|
||||||
|
}
|
||||||
|
$role->update(['permissions' => $perms]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\Appendices\Tables\AppendicesTable;
|
|||||||
|
|
||||||
class AppendixResource extends Resource
|
class AppendixResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Appendix::class;
|
protected static ?string $model = Appendix::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Filament\Resources\Contracts\Tables\ContractsTable;
|
|||||||
|
|
||||||
class ContractResource extends Resource
|
class ContractResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Contract::class;
|
protected static ?string $model = Contract::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\Customers\Tables\CustomersTable;
|
|||||||
|
|
||||||
class CustomerResource extends Resource
|
class CustomerResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Customer::class;
|
protected static ?string $model = Customer::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-users';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-users';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::CUSTOMER->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::CUSTOMER->value;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\FormTemplates\Tables\FormTemplatesTable;
|
|||||||
|
|
||||||
class FormTemplateResource extends Resource
|
class FormTemplateResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = FormTemplate::class;
|
protected static ?string $model = FormTemplate::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-duplicate';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-duplicate';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::SETTING->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::SETTING->value;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\PaymentFines\Tables\PaymentFinesTable;
|
|||||||
|
|
||||||
class PaymentFineResource extends Resource
|
class PaymentFineResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = PaymentFine::class;
|
protected static ?string $model = PaymentFine::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-exclamation-triangle';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-exclamation-triangle';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->value;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\Payments\Tables\PaymentsTable;
|
|||||||
|
|
||||||
class PaymentResource extends Resource
|
class PaymentResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Payment::class;
|
protected static ?string $model = Payment::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-banknotes';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-banknotes';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->value;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Filament\Resources\Products\Schemas\ProductForm;
|
|||||||
|
|
||||||
class ProductResource extends Resource
|
class ProductResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Product::class;
|
protected static ?string $model = Product::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-squares-2x2';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-squares-2x2';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::WAREHOUSE->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::WAREHOUSE->value;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\Projects\Schemas\ProjectForm;
|
|||||||
|
|
||||||
class ProjectResource extends Resource
|
class ProjectResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Project::class;
|
protected static ?string $model = Project::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-building-office-2';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-building-office-2';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::PROJECT->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::PROJECT->value;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleTemplates\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleTemplates\RoleTemplateResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateRoleTemplate extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleTemplateResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleTemplates\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleTemplates\RoleTemplateResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditRoleTemplate extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleTemplateResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleTemplates\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleTemplates\RoleTemplateResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListRoleTemplates extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleTemplateResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleTemplates;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleTemplates\Pages;
|
||||||
|
use App\Models\RoleTemplate;
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use App\Filament\Resources\RoleTemplates\Schemas\RoleTemplateForm;
|
||||||
|
use App\Filament\Resources\RoleTemplates\Tables\RoleTemplatesTable;
|
||||||
|
|
||||||
|
class RoleTemplateResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = RoleTemplate::class;
|
||||||
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-shield-check';
|
||||||
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::SETTING->value;
|
||||||
|
protected static ?int $navigationSort = 90;
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Mẫu phân quyền';
|
||||||
|
protected static ?string $pluralModelLabel = 'Mẫu phân quyền';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return RoleTemplateForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return RoleTemplatesTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListRoleTemplates::route('/'),
|
||||||
|
'create' => Pages\CreateRoleTemplate::route('/create'),
|
||||||
|
'edit' => Pages\EditRoleTemplate::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleTemplates\Schemas;
|
||||||
|
|
||||||
|
use App\Models\PermissionModule;
|
||||||
|
use Filament\Forms\Components\CheckboxList;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class RoleTemplateForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
$modules = PermissionModule::orderBy('label')->get();
|
||||||
|
|
||||||
|
$permissionComponents = [];
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$options = [];
|
||||||
|
foreach ($module->actions as $action) {
|
||||||
|
$label = match ($action) {
|
||||||
|
'view' => 'Xem',
|
||||||
|
'create' => 'Thêm',
|
||||||
|
'update' => 'Sửa',
|
||||||
|
'delete' => 'Xóa',
|
||||||
|
'restore' => 'Khôi phục',
|
||||||
|
'forceDelete' => 'Xóa vĩnh viễn',
|
||||||
|
'export' => 'Xuất Excel',
|
||||||
|
default => $action,
|
||||||
|
};
|
||||||
|
$options[$action] = $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
$permissionComponents[] = CheckboxList::make("permissions.{$module->module}")
|
||||||
|
->label($module->label)
|
||||||
|
->options($options)
|
||||||
|
->columns(6)
|
||||||
|
->columnSpanFull();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
Section::make('Thông tin nhóm')
|
||||||
|
->columnSpanFull()
|
||||||
|
->columns(3)
|
||||||
|
->schema([
|
||||||
|
TextInput::make('name')
|
||||||
|
->label('Tên nhóm')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('description')
|
||||||
|
->label('Mô tả'),
|
||||||
|
Toggle::make('is_active')
|
||||||
|
->label('Kích hoạt')
|
||||||
|
->default(true),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Section::make('Phân quyền theo module')
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema($permissionComponents),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleTemplates\Tables;
|
||||||
|
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class RoleTemplatesTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Tên nhóm')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
TextColumn::make('description')
|
||||||
|
->label('Mô tả')
|
||||||
|
->limit(50)
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
IconColumn::make('is_active')
|
||||||
|
->label('Kích hoạt')
|
||||||
|
->boolean()
|
||||||
|
->alignCenter(),
|
||||||
|
|
||||||
|
TextColumn::make('users_count')
|
||||||
|
->label('Số user')
|
||||||
|
->counts('users')
|
||||||
|
->alignCenter(),
|
||||||
|
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Ngày tạo')
|
||||||
|
->dateTime('d/m/Y')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\TernaryFilter::make('is_active')
|
||||||
|
->label('Trạng thái'),
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\SalesPhases\Tables\SalesPhasesTable;
|
|||||||
|
|
||||||
class SalesPhaseResource extends Resource
|
class SalesPhaseResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = SalesPhase::class;
|
protected static ?string $model = SalesPhase::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rocket-launch';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rocket-launch';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::WAREHOUSE->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::WAREHOUSE->value;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Filament\Resources\Settlements\Tables\SettlementsTable;
|
|||||||
|
|
||||||
class SettlementResource extends Resource
|
class SettlementResource extends Resource
|
||||||
{
|
{
|
||||||
|
protected static array $permissionActions = ["view", "create", "update", "delete", "restore", "forceDelete"];
|
||||||
protected static ?string $model = Settlement::class;
|
protected static ?string $model = Settlement::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-clipboard-document-check';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-clipboard-document-check';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
||||||
|
|||||||
11
app/Filament/Resources/Users/Pages/CreateUser.php
Normal file
11
app/Filament/Resources/Users/Pages/CreateUser.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Users\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Users\UserResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateUser extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = UserResource::class;
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Users/Pages/EditUser.php
Normal file
19
app/Filament/Resources/Users/Pages/EditUser.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Users\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Users\UserResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditUser extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = UserResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Users/Pages/ListUsers.php
Normal file
19
app/Filament/Resources/Users/Pages/ListUsers.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Users\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Users\UserResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListUsers extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = UserResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Filament/Resources/Users/Schemas/UserForm.php
Normal file
61
app/Filament/Resources/Users/Schemas/UserForm.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Users\Schemas;
|
||||||
|
|
||||||
|
use App\Models\RoleTemplate;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TagsInput;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class UserForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
Grid::make(2)
|
||||||
|
->schema([
|
||||||
|
Section::make('Thông tin tài khoản')
|
||||||
|
->columnSpan(1)
|
||||||
|
->schema([
|
||||||
|
TextInput::make('name')
|
||||||
|
->label('Họ tên')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('email')
|
||||||
|
->label('Email')
|
||||||
|
->email()
|
||||||
|
->required()
|
||||||
|
->unique(ignoreRecord: true),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Section::make('Phân quyền')
|
||||||
|
->columnSpan(1)
|
||||||
|
->schema([
|
||||||
|
Select::make('role_template_id')
|
||||||
|
->label('Nhóm quyền (Mẫu)')
|
||||||
|
->relationship('roleTemplate', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->placeholder('Không theo mẫu nào'),
|
||||||
|
|
||||||
|
TagsInput::make('extra_permissions')
|
||||||
|
->label('Thêm quyền (vượt cấp)')
|
||||||
|
->placeholder('ví dụ: contracts.export, payments.delete')
|
||||||
|
->helperText('Nhập quyền muốn thêm cho user này, bất chấp mẫu nhóm')
|
||||||
|
->separator(',')
|
||||||
|
->splitKeys([',', 'Enter']),
|
||||||
|
|
||||||
|
TagsInput::make('excluded_permissions')
|
||||||
|
->label('Bớt quyền (hạn chế)')
|
||||||
|
->placeholder('ví dụ: contracts.delete')
|
||||||
|
->helperText('Nhập quyền muốn tắt cho user này, bất chấp mẫu nhóm')
|
||||||
|
->separator(',')
|
||||||
|
->splitKeys([',', 'Enter']),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Filament/Resources/Users/Tables/UsersTable.php
Normal file
43
app/Filament/Resources/Users/Tables/UsersTable.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Users\Tables;
|
||||||
|
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class UsersTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Họ tên')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
TextColumn::make('email')
|
||||||
|
->label('Email')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
TextColumn::make('roleTemplate.name')
|
||||||
|
->label('Nhóm quyền')
|
||||||
|
->placeholder('Không có')
|
||||||
|
->badge()
|
||||||
|
->color('primary'),
|
||||||
|
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Ngày tạo')
|
||||||
|
->dateTime('d/m/Y')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('role_template_id')
|
||||||
|
->label('Nhóm quyền')
|
||||||
|
->relationship('roleTemplate', 'name'),
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Filament/Resources/Users/UserResource.php
Normal file
42
app/Filament/Resources/Users/UserResource.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Users;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Users\Pages;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use App\Filament\Resources\Users\Schemas\UserForm;
|
||||||
|
use App\Filament\Resources\Users\Tables\UsersTable;
|
||||||
|
|
||||||
|
class UserResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = User::class;
|
||||||
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-user-group';
|
||||||
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::SETTING->value;
|
||||||
|
protected static ?int $navigationSort = 100;
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Ngườ dùng';
|
||||||
|
protected static ?string $pluralModelLabel = 'Ngườ dùng';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return UserForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return UsersTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListUsers::route('/'),
|
||||||
|
'create' => Pages\CreateUser::route('/create'),
|
||||||
|
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Models/PermissionModule.php
Normal file
18
app/Models/PermissionModule.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PermissionModule extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['module', 'label', 'actions'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'actions' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
24
app/Models/RoleTemplate.php
Normal file
24
app/Models/RoleTemplate.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class RoleTemplate extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'description', 'permissions', 'is_active'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'permissions' => 'array',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function users()
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,9 +29,83 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'extra_permissions' => 'array',
|
||||||
|
'excluded_permissions' => 'array',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function roleTemplate()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(RoleTemplate::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tính toán effective permissions từ role template + extra - excluded.
|
||||||
|
* Cache trong session (1 lần/login).
|
||||||
|
*/
|
||||||
|
public function getEffectivePermissions(): array
|
||||||
|
{
|
||||||
|
$cacheKey = "user.{$this->id}.permissions";
|
||||||
|
|
||||||
|
if (session()->has($cacheKey)) {
|
||||||
|
return session()->get($cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
$permissions = $this->calculateEffectivePermissions();
|
||||||
|
session()->put($cacheKey, $permissions);
|
||||||
|
|
||||||
|
return $permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasEffectivePermission(string $permission): bool
|
||||||
|
{
|
||||||
|
return in_array($permission, $this->getEffectivePermissions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearPermissionCache(): void
|
||||||
|
{
|
||||||
|
session()->forget("user.{$this->id}.permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function calculateEffectivePermissions(): array
|
||||||
|
{
|
||||||
|
$templatePerms = [];
|
||||||
|
|
||||||
|
if ($this->roleTemplate) {
|
||||||
|
$templatePerms = $this->roleTemplate->permissions ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten template permissions từ {"contracts":["view","create"]} thành ["contracts.view","contracts.create"]
|
||||||
|
$templateFlat = [];
|
||||||
|
foreach ($templatePerms as $module => $actions) {
|
||||||
|
foreach ($actions as $action) {
|
||||||
|
$templateFlat[] = "{$module}.{$action}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$extra = $this->extra_permissions ?? [];
|
||||||
|
$excluded = $this->excluded_permissions ?? [];
|
||||||
|
|
||||||
|
return array_values(array_diff(
|
||||||
|
array_unique(array_merge($templateFlat, $extra)),
|
||||||
|
$excluded
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override can() để tích hợp với Laravel Authorization.
|
||||||
|
* Nếu ability là dạng "contracts.view" → dùng effective permissions.
|
||||||
|
* Ngược lại fallback về parent.
|
||||||
|
*/
|
||||||
|
public function can($abilities, $arguments = []): bool
|
||||||
|
{
|
||||||
|
if (is_string($abilities) && str_contains($abilities, '.')) {
|
||||||
|
return $this->hasEffectivePermission($abilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::can($abilities, $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
public function canAccessPanel(Panel $panel): bool
|
public function canAccessPanel(Panel $panel): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
// Bảng đăng ký các module có thể phân quyền (auto từ Resource)
|
||||||
|
Schema::create('permission_modules', function (Blueprint $table) {
|
||||||
|
$table->uuid('id')->primary();
|
||||||
|
$table->string('module')->unique(); // contracts, payments, customers...
|
||||||
|
$table->string('label'); // Hợp đồng, Thu tiền...
|
||||||
|
$table->jsonb('actions'); // ["view","create","update","delete","restore","forceDelete","export"]
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bảng mẫu nhóm (Role Template)
|
||||||
|
Schema::create('role_templates', function (Blueprint $table) {
|
||||||
|
$table->uuid('id')->primary();
|
||||||
|
$table->string('name'); // Sales, Kế toán, Admin...
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->jsonb('permissions')->default('{}'); // {"contracts":["view","create"], "payments":["view"]}
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sửa users: thêm role_template_id, extra_permissions, excluded_permissions
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->foreignUuid('role_template_id')->nullable()->constrained('role_templates')->nullOnDelete();
|
||||||
|
$table->jsonb('extra_permissions')->default('[]');
|
||||||
|
$table->jsonb('excluded_permissions')->default('[]');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['role_template_id']);
|
||||||
|
$table->dropColumn(['role_template_id', 'extra_permissions', 'excluded_permissions']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('role_templates');
|
||||||
|
Schema::dropIfExists('permission_modules');
|
||||||
|
}
|
||||||
|
};
|
||||||
29
taikhoan.txt
29
taikhoan.txt
@@ -1,29 +0,0 @@
|
|||||||
name: chanphuong
|
|
||||||
email: admin@phuongtc.com
|
|
||||||
pass: 1Qazxsw2@!321
|
|
||||||
|
|
||||||
|
|
||||||
git add .
|
|
||||||
git commit -m "Thêm tính năng X"
|
|
||||||
git push
|
|
||||||
|
|
||||||
✦ Bước 2: Hướng dẫn bạn di chuyển
|
|
||||||
|
|
||||||
Bây giờ bạn chỉ cần thực hiện các thao tác sau để đảm bảo sự liền mạch:
|
|
||||||
|
|
||||||
1. Commit & Push: Hãy commit toàn bộ code hiện tại (bao gồm cả file GEMINI.md tôi vừa tạo) lên kho lưu trữ Git của bạn
|
|
||||||
(GitHub/GitLab/Bitbucket).
|
|
||||||
2. Môi trường mới: Trên máy tính mới, sau khi git clone dự án về:
|
|
||||||
* Hãy đảm bảo đã cài đặt đầy đủ môi trường (PHP, PostgreSQL...).
|
|
||||||
* Chạy composer install.
|
|
||||||
* Cấu hình file .env chính xác cho Database.
|
|
||||||
* Chạy lại php artisan migrate:fresh --seed để có bộ dữ liệu mẫu mà chúng ta đã thống nhất.
|
|
||||||
3. Kích hoạt Gemini: Khi bạn mở terminal tại thư mục dự án trên máy mới và gọi Gemini CLI, tôi sẽ tự động đọc file
|
|
||||||
GEMINI.md.
|
|
||||||
4. Kiểm tra: Bạn có thể hỏi tôi câu này để kiểm tra xem tôi có "nhận ra" dự án không:
|
|
||||||
> "Dự án này đang dùng kiến trúc Filament nào và logic chuyển nhượng bất động sản được quy định ra sao?"
|
|
||||||
|
|
||||||
Nếu tôi trả lời đúng về Filament v5.5 Schemas và các giá trị của transfer_order, nghĩa là cuộc "di cư tri thức" đã thành
|
|
||||||
công rực rỡ.
|
|
||||||
|
|
||||||
Chúc bạn có một hành trình làm việc thuận lợi trên máy tính mới! Tôi luôn sẵn sàng đồng hành cùng bạn.
|
|
||||||
Reference in New Issue
Block a user