Updated ERP with new modules and features

This commit is contained in:
Kwesi Banson
2026-09-22 08:06:31 +00:00
parent 71bc103fee
commit 5174e82e51
53 changed files with 3706 additions and 203 deletions

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers;
use App\Models\ActivityLog;
use Illuminate\Http\Request;
class ActivityLogController extends Controller
{
public function index(Request $request)
{
$query = ActivityLog::with(['user', 'loggable'])->latest();
// 1. Filter by Action
if ($request->filled('action_type')) {
$query->where('action', $request->action_type);
}
// 2. Filter by Date Range
if ($request->filled('date_from')) {
$query->whereDate('created_at', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->whereDate('created_at', '<=', $request->date_to);
}
// 3. Search by User or Entity Type
if ($request->filled('search')) {
$searchTerm = $request->search;
$query->where(function ($q) use ($searchTerm) {
$q->whereHas('user', function ($userQuery) use ($searchTerm) {
$userQuery->where('name', 'like', '%' . $searchTerm . '%');
})
->orWhere('loggable_type', 'like', '%' . $searchTerm . '%');
});
}
$logs = $query->paginate(20)->withQueryString();
return view('activity-logs.index', compact('logs'));
}
}

View File

@@ -31,7 +31,10 @@ class DailyReportController extends Controller
->where('report_date', $request->report_date)
->delete();
$managementEmails = ['samuel@click-mobile.com'];
$managementEmails = ['kwesi@click-mobile.com'];
// $managementEmails = User::whereIn('group', ['directors'])
// ->pluck('email')
// ->toArray();
Mail::to($managementEmails)->send(new DailyReportSubmitted($report, auth()->user()));
return response()->json([

View File

@@ -135,7 +135,7 @@ class LeaveRequestController extends Controller
$leaveRequest->update([
'status' => $validated['status'],
'admin_remarks' => $validated['admin_remarks'],
'approved_by' => auth()->id(), // Track who approved it
'approved_by' => auth()->id(),
]);
return response()->json(['success' => true, 'message' => 'Leave status updated successfully!']);

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use App\Models\StaffMember;
use App\Models\LeaveRequest;
use Illuminate\Http\Request;
use Carbon\Carbon;
class ManagementDashboardController extends Controller
{
public function index(Request $request)
{
// 1. Fetch Pending Leaves (Adjust status/model names to match your schema)
$pendingLeaves = LeaveRequest::with('staff')
->where('status', 'Pending')
->orderBy('created_at', 'asc')
->get();
$startDate = $request->input('start_date', Carbon::now()->startOfMonth()->format('Y-m-d'));
$endDate = $request->input('end_date', Carbon::now()->endOfMonth()->format('Y-m-d'));
$staffReports = StaffMember::withCount(['dailyReports' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('report_date', [$startDate, $endDate]);
}])
->orderBy('name')
->get();
return view('management.index', compact('pendingLeaves', 'staffReports', 'startDate', 'endDate'));
}
public function updateLeaveStatus(Request $request, $id)
{
$request->validate(['status' => 'required|in:Approved,Rejected']);
$leave = LeaveRequest::findOrFail($id);
if ($request['status'] === 'Approved' && $leave->status !== 'Approved') {
if ($leave->leave_type === 'Annual') {
$staff = $leave->staff;
$staff->annual_leave_balance -= $leave->total_days;
$staff->save();
}
}
if ($request['status'] === 'Rejected' && $leave->status === 'Rejected') {
if ($leave->leave_type === 'Annual') {
$staff = $leave->staff;
$staff->annual_leave_balance += $leave->total_days;
$staff->save();
}
}
$leave->update([
'status' => strtoupper($request['status']),
'admin_remarks' => 'N/A',
'approved_by' => auth()->id(),
]);
// $leave->update(['status' => $request->status]);
return response()->json(['success' => true, 'message' => "Leave request {$request->status} successfully."]);
}
}

View File

@@ -79,6 +79,8 @@ class NetworkOperatorsController extends Controller
$request->validate([
'name' => 'required|string|max:200',
'country' => 'required|string',
'contract_auto_renew' => 'required|boolean',
'next_renewal_date' => 'nullable|required_if:contract_auto_renew,0|date',
]);
$operator->update([
@@ -98,6 +100,10 @@ class NetworkOperatorsController extends Controller
'rate_type' => $request->rate_type,
'payment_terms' => $request->payment_terms,
'connection_type' => $request->connection_type,
'contract_type' => $request->contract_type,
'contract_validity' => $request->contract_validity,
'contract_auto_renew' => $request->contract_auto_renew,
'next_renewal_date' => $request->contract_auto_renew ? null : $request->next_renewal_date,
]);
return response()->json(['message' => 'MNO Partner updated successfully!']);
@@ -112,7 +118,8 @@ class NetworkOperatorsController extends Controller
public function show($id)
{
$operator = NetworkOperator::findOrFail($id);
return view('network_operators.show', compact('operator'));
$staffMembers = \App\Models\StaffMember::orderBy('name')->get();
return view('network_operators.show', compact('operator', 'staffMembers'));
}
public function storeConnection(Request $request, $operatorId)
@@ -171,5 +178,45 @@ class NetworkOperatorsController extends Controller
return redirect()->back()->with('success', 'Connection detail removed.');
}
public function storeDocument(Request $request, $operatorId)
{
$request->validate([
'document_names' => 'required|array',
'document_names.*' => 'required|string|max:255',
'documents' => 'required|array',
'documents.*' => 'required|file|mimes:pdf,doc,docx,xlsx,xls,png,jpg,jpeg|max:10240', // 10MB limit
]);
foreach ($request->document_names as $index => $name) {
if ($request->hasFile("documents.$index")) {
$filePath = $request->file("documents.$index")->store('mno_documents', 'public');
\App\Models\NetworkOperatorDocument::create([
'network_operator_id' => $operatorId,
'name' => $name,
'file_path' => $filePath,
]);
}
}
return response()->json([
'success' => true,
'message' => 'Documents uploaded successfully!'
]);
}
public function destroyDocument($id)
{
$document = \App\Models\NetworkOperatorDocument::findOrFail($id);
// Optionally delete the file from storage
if (\Illuminate\Support\Facades\Storage::disk('public')->exists($document->file_path)) {
\Illuminate\Support\Facades\Storage::disk('public')->delete($document->file_path);
}
$document->delete();
return redirect()->back()->with('success', 'Document deleted successfully.');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use Illuminate\Http\Request;
class ServerController extends Controller
{
public function index()
{
$servers = Server::orderBy('name', 'asc')->paginate(15);
return view('servers.index', compact('servers'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'ip_address' => 'required|ip',
'os_version' => 'nullable|string|max:255',
'username' => 'required|string|max:255',
'password' => 'required|string',
'services' => 'nullable|array',
'status' => 'required|in:Active,Inactive,Maintenance',
]);
Server::create($validated);
return response()->json(['success' => true, 'message' => 'Server added successfully.']);
}
// Return server data for the edit modal
public function show($id)
{
$server = Server::findOrFail($id);
return response()->json($server);
}
public function update(Request $request, $id)
{
$server = Server::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:255',
'ip_address' => 'required|ip',
'os_version' => 'nullable|string|max:255',
'username' => 'required|string|max:255',
'password' => 'nullable|string',
'services' => 'nullable|array',
'status' => 'required|in:Active,Inactive,Maintenance',
]);
// If password field is empty, remove it from the array so we don't overwrite the existing one
if (empty($validated['password'])) {
unset($validated['password']);
}
$server->update($validated);
return response()->json(['success' => true, 'message' => 'Server updated successfully.']);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\DB;
class ServerCredentialController extends Controller
{
// Step 1: Generate and Email OTP
public function requestOtp(Request $request, $serverId)
{
$server = Server::findOrFail($serverId);
$user = auth()->user();
// Generate a 6-digit OTP
$otp = random_int(100000, 999999);
// Store in cache for 5 minutes tied specifically to this user and server
$cacheKey = "otp_{$user->id}_server_{$server->id}";
Cache::put($cacheKey, $otp, now()->addMinutes(5));
// Dispatch Email (You can replace this with a proper Mailable later)
Mail::raw("Your OTP to view the credentials for {$server->name} ({$server->ip_address}) is: {$otp}. This code expires in 5 minutes.", function ($message) use ($user) {
$message->to($user->email)
->subject('Server Access Verification Code');
});
return response()->json([
'success' => true,
'message' => 'An OTP has been sent to your email.'
]);
}
// Step 2: Verify OTP and Release Password
public function verifyOtp(Request $request, $serverId)
{
$request->validate(['otp' => 'required|numeric']);
$user = auth()->user();
$server = Server::findOrFail($serverId);
$cacheKey = "otp_{$user->id}_server_{$server->id}";
if (Cache::get($cacheKey) == $request->otp) {
// Clear the OTP so it cannot be reused
Cache::forget($cacheKey);
// Log the access in the audit trail
DB::table('password_access_audits')->insert([
'user_id' => $user->id,
'server_id' => $server->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'accessed_at' => now(),
]);
// Return the decrypted password (Laravel handles decryption automatically via the model cast)
return response()->json([
'success' => true,
'username' => $server->username,
'password' => $server->password
]);
}
return response()->json(['success' => false, 'message' => 'Invalid or expired OTP.'], 422);
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Http\Controllers;
use App\Models\VpnConfig;
use Illuminate\Http\Request;
class VpnConfigController extends Controller
{
public function index()
{
$vpns = VpnConfig::orderBy('partner_name')->paginate(15);
return view('vpns.index', compact('vpns'));
}
// Handle Add Form Submission
public function store(Request $request)
{
$validated = $request->validate([
'partner_name' => 'required|string|max:255',
'status' => 'required|in:Active,Inactive',
'local_ip' => 'required|ip',
'peer_ip' => 'required|ip',
'encryption_domain_local' => 'nullable|string|max:255',
'encryption_domain_remote' => 'nullable|string|max:255',
'preshared_key' => 'required|string',
'local_device' => 'nullable|string|max:255',
'peer_device' => 'nullable|string|max:255',
'ike_version' => 'required|integer',
'phase1_encryption' => 'required|string|max:50',
'phase1_hash' => 'required|string|max:50',
'phase1_dh_group' => 'required|string|max:50',
'phase1_lifetime' => 'required|integer',
'phase2_encryption' => 'required|string|max:50',
'phase2_hash' => 'required|string|max:50',
'phase2_dh_group' => 'nullable|string|max:50',
'phase2_lifetime' => 'required|integer',
'pfs_enabled' => 'boolean',
]);
VpnConfig::create($validated);
return response()->json(['success' => true, 'message' => 'VPN configuration added successfully.']);
}
// Fetch data for the Edit Modal
public function show($id)
{
$vpn = VpnConfig::findOrFail($id);
// Hide the pre-shared key from being sent back in the JSON response for security
$vpn->makeHidden('preshared_key');
return response()->json($vpn);
}
// Handle Edit Form Submission
public function update(Request $request, $id)
{
$vpn = VpnConfig::findOrFail($id);
$validated = $request->validate([
'partner_name' => 'required|string|max:255',
'status' => 'required|in:Active,Inactive',
'local_ip' => 'required|ip',
'peer_ip' => 'required|ip',
'encryption_domain_local' => 'nullable|string|max:255',
'encryption_domain_remote' => 'nullable|string|max:255',
'preshared_key' => 'nullable|string',
'local_device' => 'nullable|string|max:255',
'peer_device' => 'nullable|string|max:255',
'ike_version' => 'required|integer',
'phase1_encryption' => 'required|string|max:50',
'phase1_hash' => 'required|string|max:50',
'phase1_dh_group' => 'required|string|max:50',
'phase1_lifetime' => 'required|integer',
'phase2_encryption' => 'required|string|max:50',
'phase2_hash' => 'required|string|max:50',
'phase2_dh_group' => 'nullable|string|max:50',
'phase2_lifetime' => 'required|integer',
'pfs_enabled' => 'boolean',
]);
// If the PSK field is empty, remove it so we don't overwrite the existing key
if (empty($validated['preshared_key'])) {
unset($validated['preshared_key']);
}
$vpn->update($validated);
return response()->json(['success' => true, 'message' => 'VPN configuration updated successfully.']);
}
// Export the company's global standard VPN parameters
public function downloadStandardTemplate()
{
$template = "=================================================\n";
$template .= " Click Mobile VPN Configuration Parameters\n";
$template .= "=================================================\n\n";
$template .= "! Phase 1 (IKE / ISAKMP)\n";
$template .= " Encryption Algorithm: AES-256\n";
$template .= " Hash Algorithm: SHA-256\n";
$template .= " Authentication Method: Pre-Shared Key (PSK)\n";
$template .= " Diffie-Hellman Group: Group 14\n";
$template .= " Lifetime: 86400 seconds\n\n";
$template .= "! Phase 2 (IPsec)\n";
$template .= " Protocol: ESP\n";
$template .= " Encryption Algorithm: AES-256\n";
$template .= " Hash Algorithm: SHA-256\n";
$template .= " Perfect Forward Secrecy (PFS): Group 14\n";
$template .= " Lifetime: 3600 seconds\n\n";
$template .= "! Gateway Details\n";
$template .= " Our Public Gateway IP: [216.55.137.19]\n";
$template .= " Our Encryption Domain: [10.199.0.0/24]\n";
return response($template)
->header('Content-Type', 'text/plain')
->header('Content-Disposition', 'attachment; filename="standard_vpn_parameters.txt"');
}
// Fetch data to populate the Partner Export Modal
public function generateTemplate($id)
{
$vpn = VpnConfig::findOrFail($id);
$template = "=================================================\n";
$template .= " VPN Configuration for Partner: {$vpn->partner_name}\n";
$template .= "=================================================\n\n";
$template .= "! Phase 1 (IKE)\n";
$template .= "crypto isakmp policy 10\n";
$template .= " encr aes 256\n";
$template .= " hash sha256\n";
$template .= " authentication pre-share\n";
$template .= " group 14\n";
$template .= "crypto isakmp key {$vpn->preshared_key} address {$vpn->local_ip}\n\n";
$template .= "! Phase 2 (IPsec)\n";
$template .= "crypto ipsec transform-set TS-AES256-SHA256 esp-aes 256 esp-sha256-hmac\n";
$template .= "crypto map VPNMAP 10 ipsec-isakmp\n";
$template .= " set peer {$vpn->local_ip}\n";
$template .= " set transform-set TS-AES256-SHA256\n";
$template .= " match address 100\n\n";
$template .= "! Access Control List (Encryption Domains)\n";
$template .= "access-list 100 permit ip {$vpn->encryption_domain_remote} {$vpn->encryption_domain_local}\n";
return response()->json(['template' => $template, 'partner_name' => $vpn->partner_name]);
}
// Process the edited text and force a file download
public function download(Request $request)
{
$request->validate([
'config_content' => 'required|string',
'partner_name' => 'required|string',
]);
$filename = str_replace(' ', '_', strtolower($request->partner_name)) . '_vpn_config.txt';
return response($request->config_content)
->header('Content-Type', 'text/plain')
->header('Content-Disposition', "attachment; filename=\"{$filename}\"");
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ActivityLog extends Model
{
protected $fillable = [
'user_id', 'loggable_type', 'loggable_id', 'action',
'old_data', 'new_data', 'ip_address', 'user_agent'
];
protected $casts = [
'old_data' => 'array',
'new_data' => 'array',
];
// Links back to the User who made the change
public function user()
{
return $this->belongsTo(StaffMember::class);
}
// Links back to the affected Model (Client, Server, VpnConfig, etc.)
public function loggable()
{
return $this->morphTo();
}
}

View File

@@ -3,9 +3,10 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class Client extends Model
{
use LogsActivity;
protected $guarded = [
'id'
];

View File

@@ -3,9 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class ClientFile extends Model
{
use LogsActivity;
protected $guarded = array('id');
public function client_info(){

View File

@@ -5,9 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
use App\Traits\LogsActivity;
class ClientNote extends Model
{
use LogsActivity;
protected $guarded = array('id');
protected static $logUnguarded = true;

View File

@@ -3,9 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class DailyReport extends Model
{
use LogsActivity;
protected $fillable = ['user_id', 'report_date', 'content'];
protected $casts = [
'report_date' => 'date',

View File

@@ -3,9 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class LeaveManagement extends Model
{
use LogsActivity;
protected $guarded = array('id');
public $table = "leave_management";

View File

@@ -15,10 +15,4 @@ class LeaveRequest extends Model
{
return $this->belongsTo(StaffMember::class, 'staff_member_id');
}
// In StaffMember.php
public function leaveRequests()
{
return $this->hasMany(LeaveRequest::class, 'staff_member_id');
}
}

View File

@@ -4,9 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use App\Traits\LogsActivity;
class NetworkOperator extends Model
{
use LogsActivity;
protected $guarded = array('id');
public $table = "network_operators";
@@ -15,6 +17,8 @@ class NetworkOperator extends Model
'support_emails' => 'array',
'finance_emails' => 'array',
'bind_specifics' => 'array',
'contract_auto_renew' => 'boolean',
'next_renewal_date' => 'date',
];
public function account_manager_info(){
return $this->hasOne('App\Models\StaffMember', 'id', 'account_manager_id');
@@ -23,6 +27,10 @@ class NetworkOperator extends Model
{
return $this->hasMany(NetworkOperatorConnection::class, 'network_operator_id');
}
public function documents()
{
return $this->hasMany(NetworkOperatorDocument::class);
}
/*
public function setBuyingRateAttribute($value)
{

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class NetworkOperatorDocument extends Model
{
use LogsActivity;
protected $fillable = ['network_operator_id', 'name', 'file_path'];
public function networkOperator()
{
return $this->belongsTo(NetworkOperator::class);
}
}

View File

@@ -3,9 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class OfficeLocation extends Model
{
use LogsActivity;
protected $guarded = array('id');
protected $table = 'office_locations';

View File

@@ -3,9 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class OfficeLocationDocument extends Model
{
use LogsActivity;
protected $table = 'office_location_documents';
protected $guarded = [];

View File

@@ -3,13 +3,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
// use Spatie\Activitylog\Traits\LogsActivity;
// use Spatie\Activitylog\LogOptions;
use App\Traits\LogsActivity;
class SenderId extends Model{
// use LogsActivity;
use LogsActivity;
protected $guarded = array('id');
public $table = "sender_ids";
protected static $logName = 'senderid_log';

View File

@@ -3,10 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class Senderidold extends Model{
use LogsActivity;
protected $guarded = array('id');
public $table = "sender_ids_copy";

22
app/Models/Server.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class Server extends Model
{
use LogsActivity;
protected $fillable = [
'name', 'ip_address', 'os_version', 'username', 'password', 'services', 'status'
];
protected $casts = [
'password' => 'encrypted', // Automatically encrypts/decrypts
'services' => 'array', // Casts JSON to PHP array
];
// Hide password from standard array/JSON serialization for safety
protected $hidden = ['password'];
}

View File

@@ -3,9 +3,12 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class ShortCode extends Model
{
use LogsActivity;
protected $guarded = array('id');
public $table = "client_short_codes";

View File

@@ -3,9 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class StaffDependent extends Model
{
use LogsActivity;
protected $table = 'staff_dependents';
protected $guarded = [];

View File

@@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Traits\LogsActivity;
class StaffMember extends Authenticatable
{
use HasFactory, Notifiable;
use HasFactory, Notifiable, LogsActivity;
// 1. Explicitly define the custom table name
protected $table = 'staff_members';
@@ -34,6 +35,7 @@ class StaffMember extends Authenticatable
public function department(){
return $this->hasOne('App\Models\Department', 'id', 'department_id');
}
// 4. Cast dates correctly
protected $casts = [
// 'dob' => 'date',

View File

@@ -3,10 +3,23 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\LogsActivity;
class VpnConfig extends Model
{
protected $guarded = ['id'];
//protected $guarded = ['id'];
use LogsActivity;
protected $fillable = [
'partner_name', 'peer_ip', 'local_ip',
'local_device', 'peer_device',
'encryption_domain_local', 'encryption_domain_remote',
'preshared_key', 'status',
'ike_version', 'phase1_encryption', 'phase1_hash', 'phase1_dh_group', 'phase1_lifetime',
'phase2_encryption', 'phase2_hash', 'phase2_dh_group', 'phase2_lifetime', 'pfs_enabled'
];
protected $casts = [
'preshared_key' => 'encrypted',
];
public function client_info(){
return $this->hasOne('App\Models\Client', 'id', 'client_id');
@@ -15,6 +28,6 @@ class VpnConfig extends Model
return $this->hasOne('App\Models\NetworkOps', 'id', 'mno_id');
}
public function userInfo(){
return $this->hasOne('App\Models\SystemUser', 'id', 'user_id');
return $this->hasOne('App\Models\StaffMember', 'id', 'user_id');
}
}

View File

@@ -0,0 +1,65 @@
<?php namespace App\Traits;
use App\Models\ActivityLog;
use Illuminate\Support\Facades\Auth;
trait LogsActivity
{
// Laravel automatically calls boot[TraitName] when booting the model
protected static function bootLogsActivity()
{
static::created(function ($model) {
$model->recordActivity('created', null, $model->getAttributes());
});
static::updated(function ($model) {
// Get only the fields that actually changed
$newAttributes = $model->getChanges();
// Ignore the 'updated_at' timestamp noise
unset($newAttributes['updated_at']);
if (!empty($newAttributes)) {
$oldAttributes = array_intersect_key($model->getOriginal(), $newAttributes);
$model->recordActivity('updated', $oldAttributes, $newAttributes);
}
});
static::deleted(function ($model) {
$model->recordActivity('deleted', $model->getAttributes(), null);
});
}
protected function recordActivity($action, $oldData = null, $newData = null)
{
// Define sensitive fields to NEVER log
$hiddenFields = ['password', 'preshared_key', 'remember_token'];
// Allow models to define custom fields to ignore via a protected $ignoreLogs array
if (property_exists($this, 'ignoreLogs')) {
$hiddenFields = array_merge($hiddenFields, $this->ignoreLogs);
}
$cleanOld = $oldData ? collect($oldData)->except($hiddenFields)->toArray() : null;
$cleanNew = $newData ? collect($newData)->except($hiddenFields)->toArray() : null;
ActivityLog::create([
'user_id' => Auth::id(),
'loggable_type' => get_class($this),
'loggable_id' => $this->id,
'action' => $action,
'old_data' => empty($cleanOld) ? null : $cleanOld,
'new_data' => empty($cleanNew) ? null : $cleanNew,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent()
]);
}
// Helper to fetch logs for a specific model instance
public function activityLogs()
{
return $this->morphMany(ActivityLog::class, 'loggable')->latest();
}
}
?>