Skip to content

Commit 3803562

Browse files
committed
feat(phase-20/23): audit log models, notification controller, traits (in-progress)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent f1c56cd commit 3803562

15 files changed

Lines changed: 315 additions & 60 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
namespace App\Http\Controllers\Api\V1;
3+
4+
use Illuminate\Http\Request;
5+
use Illuminate\Http\JsonResponse;
6+
use App\Modules\Core\Models\AuditLog;
7+
8+
class AuditLogController extends ApiController
9+
{
10+
public function index(Request $request): JsonResponse
11+
{
12+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
13+
14+
$logs = AuditLog::where('tenant_id', $tenantId)
15+
->with('user:id,name')
16+
->when($request->action, fn($q) => $q->where('action', $request->action))
17+
->when($request->type, fn($q) => $q->where('auditable_type', 'like', "%{$request->type}%"))
18+
->latest()
19+
->paginate(50);
20+
21+
return $this->paginated($logs);
22+
}
23+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
namespace App\Http\Controllers\Api\V1;
3+
4+
use Illuminate\Http\Request;
5+
use Illuminate\Http\JsonResponse;
6+
use App\Modules\Core\Models\ErpNotification;
7+
8+
class NotificationController extends ApiController
9+
{
10+
public function index(Request $request): JsonResponse
11+
{
12+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
13+
$userId = $request->user()->id;
14+
15+
$notifications = ErpNotification::where('tenant_id', $tenantId)
16+
->where('user_id', $userId)
17+
->latest()
18+
->paginate(20);
19+
20+
return $this->paginated($notifications);
21+
}
22+
23+
public function unreadCount(Request $request): JsonResponse
24+
{
25+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
26+
$userId = $request->user()->id;
27+
28+
$count = ErpNotification::where('tenant_id', $tenantId)
29+
->where('user_id', $userId)
30+
->whereNull('read_at')
31+
->count();
32+
33+
return $this->success(['count' => $count]);
34+
}
35+
36+
public function markRead(Request $request, int $id): JsonResponse
37+
{
38+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
39+
$userId = $request->user()->id;
40+
41+
$notification = ErpNotification::where('tenant_id', $tenantId)
42+
->where('user_id', $userId)
43+
->findOrFail($id);
44+
45+
$notification->markAsRead();
46+
47+
return $this->success(['message' => 'Notification marked as read']);
48+
}
49+
50+
public function markAllRead(Request $request): JsonResponse
51+
{
52+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
53+
$userId = $request->user()->id;
54+
55+
ErpNotification::where('tenant_id', $tenantId)
56+
->where('user_id', $userId)
57+
->whereNull('read_at')
58+
->update(['read_at' => now()]);
59+
60+
return $this->success(['message' => 'All notifications marked as read']);
61+
}
62+
}

erp/app/Modules/Core/Models/AuditLog.php

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,26 @@
44

55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7-
use Illuminate\Database\Eloquent\Relations\MorphTo;
87

98
class AuditLog extends Model
109
{
11-
public const UPDATED_AT = null;
12-
13-
public $timestamps = false;
14-
1510
protected $fillable = [
16-
'user_id',
1711
'tenant_id',
18-
'event',
12+
'user_id',
1913
'action',
2014
'auditable_type',
2115
'auditable_id',
22-
'auditable_label',
2316
'old_values',
2417
'new_values',
2518
'ip_address',
2619
'user_agent',
27-
'url',
28-
'module',
29-
'created_at',
3020
];
3121

3222
protected $casts = [
33-
'old_values' => 'array',
34-
'new_values' => 'array',
35-
'created_at' => 'datetime',
23+
'old_values' => 'array',
24+
'new_values' => 'array',
3625
];
3726

38-
protected $dates = ['created_at'];
39-
40-
public function auditable(): MorphTo
41-
{
42-
return $this->morphTo();
43-
}
44-
4527
public function user(): BelongsTo
4628
{
4729
return $this->belongsTo(\App\Models\User::class);
@@ -55,15 +37,11 @@ public function tenant(): BelongsTo
5537
/**
5638
* Record an audit log entry.
5739
*
58-
* Supports two call styles:
59-
* record(string $action, $model, array $old, array $new, string $module) -- new style
60-
* record(string $event, $model, array $old, array $new, int $tenantId) -- legacy style
61-
*
62-
* @param string $action
63-
* @param Model|null $model
64-
* @param array $oldValues
65-
* @param array $newValues
66-
* @param string|int|null $moduleOrTenantId
40+
* @param string $action
41+
* @param Model|null $model
42+
* @param array $oldValues
43+
* @param array $newValues
44+
* @param mixed $moduleOrTenantId ignored (kept for backward compat)
6745
* @return static
6846
*/
6947
public static function record(
@@ -74,33 +52,25 @@ public static function record(
7452
$moduleOrTenantId = null
7553
): static {
7654
$tenantId = null;
77-
$module = '';
7855

7956
if (is_int($moduleOrTenantId)) {
8057
$tenantId = $moduleOrTenantId;
81-
} elseif (is_string($moduleOrTenantId)) {
82-
$module = $moduleOrTenantId;
8358
}
8459

8560
if ($tenantId === null) {
86-
$tenantId = auth()->user()?->tenant_id ?? 0;
61+
$tenantId = $model->tenant_id ?? auth()->user()?->tenant_id ?? 0;
8762
}
8863

8964
return static::create([
90-
'tenant_id' => $tenantId,
91-
'user_id' => auth()->id(),
92-
'event' => $action,
93-
'action' => $action,
94-
'auditable_type' => $model ? get_class($model) : null,
95-
'auditable_id' => $model?->getKey(),
96-
'auditable_label' => $model?->name ?? $model?->title ?? $model?->subject ?? null,
97-
'old_values' => $oldValues ?: null,
98-
'new_values' => $newValues ?: null,
99-
'ip_address' => request()?->ip(),
100-
'user_agent' => request()?->userAgent(),
101-
'url' => request()?->fullUrl(),
102-
'module' => $module,
103-
'created_at' => now(),
65+
'tenant_id' => $tenantId,
66+
'user_id' => auth()->id(),
67+
'action' => $action,
68+
'auditable_type' => $model ? get_class($model) : null,
69+
'auditable_id' => $model?->getKey(),
70+
'old_values' => $oldValues ?: null,
71+
'new_values' => $newValues ?: null,
72+
'ip_address' => request()?->ip(),
73+
'user_agent' => request()?->userAgent(),
10474
]);
10575
}
10676

@@ -122,6 +92,6 @@ public function getChangeSummaryAttribute(): string
12292
return implode(', ', $changedKeys) . ' changed';
12393
}
12494

125-
return $this->action ?? $this->event ?? '';
95+
return $this->action ?? '';
12696
}
12797
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
namespace App\Modules\Core\Models;
3+
4+
use Illuminate\Database\Eloquent\Model;
5+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
6+
use App\Models\User;
7+
8+
class ErpNotification extends Model
9+
{
10+
protected $table = 'erp_notifications';
11+
12+
protected $fillable = [
13+
'tenant_id', 'user_id', 'type', 'title', 'message', 'data', 'read_at',
14+
];
15+
16+
protected $casts = [
17+
'data' => 'array',
18+
'read_at' => 'datetime',
19+
];
20+
21+
public function user(): BelongsTo
22+
{
23+
return $this->belongsTo(User::class);
24+
}
25+
26+
public function markAsRead(): void
27+
{
28+
$this->update(['read_at' => now()]);
29+
}
30+
31+
public function isRead(): bool
32+
{
33+
return $this->read_at !== null;
34+
}
35+
}

erp/app/Modules/Core/Observers/AuditLogObserver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ public function deleted(Model $model): void
3030
$this->log('deleted', $model, $model->getOriginal(), []);
3131
}
3232

33-
private function log(string $event, Model $model, array $old, array $new): void
33+
private function log(string $action, Model $model, array $old, array $new): void
3434
{
3535
$tenantId = $this->resolveTenantId($model);
3636

3737
AuditLog::create([
3838
'user_id' => Auth::id(),
3939
'tenant_id' => $tenantId,
40-
'event' => $event,
40+
'action' => $action,
4141
'auditable_type' => get_class($model),
4242
'auditable_id' => $model->getKey(),
4343
'old_values' => $old ?: null,

erp/app/Modules/Finance/Models/Contact.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Modules\Core\Traits\BelongsToTenant;
66
use App\Modules\Core\Traits\HasAuditLog;
7+
use App\Traits\LogsActivity;
78
use Illuminate\Database\Eloquent\Model;
89
use Illuminate\Database\Eloquent\Relations\BelongsTo;
910
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -14,6 +15,7 @@ class Contact extends Model
1415
{
1516
use BelongsToTenant;
1617
use HasAuditLog;
18+
use LogsActivity;
1719
use SoftDeletes;
1820

1921
protected $fillable = [

erp/app/Modules/Finance/Models/Invoice.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Models\User;
66
use App\Modules\Core\Traits\BelongsToTenant;
77
use App\Modules\Core\Traits\HasAuditLog;
8+
use App\Traits\LogsActivity;
89
use App\Modules\Finance\Traits\HasLineItemTotals;
910
use App\Modules\Finance\Traits\HasAttachments;
1011
use App\Modules\Finance\Traits\HasStatusTransitions;
@@ -17,6 +18,7 @@ class Invoice extends Model
1718
{
1819
use BelongsToTenant;
1920
use HasAuditLog;
21+
use LogsActivity;
2022
use SoftDeletes;
2123
use HasLineItemTotals;
2224
use HasAttachments;

erp/app/Modules/HR/Models/Employee.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Models\User;
66
use App\Modules\Core\Traits\BelongsToTenant;
7+
use App\Traits\LogsActivity;
78
use Illuminate\Database\Eloquent\Model;
89
use Illuminate\Database\Eloquent\Relations\BelongsTo;
910
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -12,6 +13,7 @@
1213
class Employee extends Model
1314
{
1415
use BelongsToTenant;
16+
use LogsActivity;
1517
use SoftDeletes;
1618

1719
protected $fillable = [

erp/app/Modules/Inventory/Models/Product.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Modules\Core\Traits\BelongsToTenant;
66
use App\Modules\Core\Traits\HasAuditLog;
7+
use App\Traits\LogsActivity;
78
use Illuminate\Database\Eloquent\Model;
89
use Illuminate\Database\Eloquent\Relations\BelongsTo;
910
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -14,6 +15,7 @@ class Product extends Model
1415
{
1516
use BelongsToTenant;
1617
use HasAuditLog;
18+
use LogsActivity;
1719
use SoftDeletes;
1820

1921
protected $fillable = [

erp/app/Services/NotificationService.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,37 @@
22

33
namespace App\Services;
44

5+
use App\Modules\Core\Models\ErpNotification;
56
use App\Modules\Finance\Models\Invoice;
67
use App\Modules\HR\Models\LeaveRequest;
78
use App\Modules\Inventory\Models\Product;
9+
use App\Events\Notifications\ErpNotification as ErpNotificationEvent;
810
use Illuminate\Support\Facades\Cache;
911

1012
class NotificationService
1113
{
14+
/**
15+
* Send a persistent in-app notification and broadcast via WebSocket.
16+
*/
17+
public static function send(int $tenantId, int $userId, string $type, string $title, string $message, array $data = []): ErpNotification
18+
{
19+
$notification = ErpNotification::create([
20+
'tenant_id' => $tenantId,
21+
'user_id' => $userId,
22+
'type' => $type,
23+
'title' => $title,
24+
'message' => $message,
25+
'data' => $data,
26+
]);
27+
28+
broadcast(new ErpNotificationEvent($tenantId, $type, $title, $message, $data));
29+
30+
return $notification;
31+
}
32+
33+
/**
34+
* Get summary notifications for sidebar bell (cached, page-load driven).
35+
*/
1236
public static function forUser(\App\Models\User $user): array
1337
{
1438
$tenantId = $user->tenant_id;

0 commit comments

Comments
 (0)