Updated ERP with new modules and features
This commit is contained in:
42
app/Http/Controllers/ActivityLogController.php
Normal file
42
app/Http/Controllers/ActivityLogController.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
@@ -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([
|
||||
|
||||
@@ -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!']);
|
||||
|
||||
64
app/Http/Controllers/ManagementDashboardController.php
Normal file
64
app/Http/Controllers/ManagementDashboardController.php
Normal 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."]);
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
63
app/Http/Controllers/ServerController.php
Normal file
63
app/Http/Controllers/ServerController.php
Normal 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.']);
|
||||
}
|
||||
}
|
||||
70
app/Http/Controllers/ServerCredentialController.php
Normal file
70
app/Http/Controllers/ServerCredentialController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
166
app/Http/Controllers/VpnConfigController.php
Normal file
166
app/Http/Controllers/VpnConfigController.php
Normal 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}\"");
|
||||
}
|
||||
}
|
||||
30
app/Models/ActivityLog.php
Normal file
30
app/Models/ActivityLog.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
17
app/Models/NetworkOperatorDocument.php
Normal file
17
app/Models/NetworkOperatorDocument.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
22
app/Models/Server.php
Normal 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'];
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
65
app/Traits/LogsActivity.php
Normal file
65
app/Traits/LogsActivity.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('servers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // e.g., "Main PBX Engine" or "Synology NAS"
|
||||
$table->string('ip_address');
|
||||
$table->string('os_version')->nullable(); // e.g., "AlmaLinux 9", "Debian"
|
||||
$table->string('username')->default('root');
|
||||
$table->text('password'); // Will be encrypted via Model casting
|
||||
$table->json('services')->nullable(); // Array of services like ['Docker', 'PostGIS', 'Dovecot']
|
||||
$table->string('status')->default('Active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('servers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('password_access_audits', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('server_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('ip_address');
|
||||
$table->string('user_agent')->nullable();
|
||||
$table->timestamp('accessed_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_access_audits');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('vpn_configs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('partner_name'); // e.g., "MTN Ghana" or "Telecel"
|
||||
$table->string('peer_ip'); // Partner's Gateway
|
||||
$table->string('local_ip'); // Your Gateway
|
||||
$table->string('encryption_domain_local')->nullable(); // Your internal subnets/VLANs
|
||||
$table->string('encryption_domain_remote')->nullable(); // Partner's subnets
|
||||
$table->text('preshared_key'); // Encrypted
|
||||
$table->string('status')->default('Active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('vpn_configs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('vpn_configs', function (Blueprint $table) {
|
||||
// General Info
|
||||
$table->string('local_device')->nullable()->after('peer_ip');
|
||||
$table->string('peer_device')->nullable()->after('local_device');
|
||||
|
||||
// Phase 1 (IKE)
|
||||
$table->integer('ike_version')->default(2)->after('preshared_key');
|
||||
$table->string('phase1_encryption')->default('AES-256')->after('ike_version');
|
||||
$table->string('phase1_hash')->default('SHA-256')->after('phase1_encryption');
|
||||
$table->string('phase1_dh_group')->default('14')->after('phase1_hash');
|
||||
$table->integer('phase1_lifetime')->default(28800)->after('phase1_dh_group');
|
||||
|
||||
// Phase 2 (IPSec)
|
||||
$table->string('phase2_encryption')->default('AES-256')->after('phase1_lifetime');
|
||||
$table->string('phase2_hash')->default('SHA-256')->after('phase2_encryption');
|
||||
$table->string('phase2_dh_group')->nullable()->after('phase2_hash');
|
||||
$table->integer('phase2_lifetime')->default(3600)->after('phase2_dh_group');
|
||||
$table->boolean('pfs_enabled')->default(true)->after('phase2_lifetime');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('vpn_configs', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'local_device', 'peer_device', 'ike_version',
|
||||
'phase1_encryption', 'phase1_hash', 'phase1_dh_group', 'phase1_lifetime',
|
||||
'phase2_encryption', 'phase2_hash', 'phase2_dh_group', 'phase2_lifetime', 'pfs_enabled'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id();$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
|
||||
// This creates `loggable_type` and `loggable_id` to link to ANY model
|
||||
$table->morphs('loggable');
|
||||
|
||||
$table->string('action'); // 'created', 'updated', 'deleted'$table->json('old_data')->nullable(); // State before update
|
||||
$table->json('new_data')->nullable(); // State after update$table->string('ip_address')->nullable();
|
||||
$table->string('user_agent')->nullable();$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('activity_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->json('old_data')->nullable()->after('action');
|
||||
$table->string('ip_address')->nullable()->after('new_data');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->dropColumn(['old_data', 'ip_address']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function upSIde()
|
||||
{
|
||||
Schema::create('network_operator_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('network_operator_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('file_path');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
public function up()
|
||||
{
|
||||
Schema::create('network_operator_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Explicitly define as standard unsigned integer to match older tables
|
||||
$table->unsignedInteger('network_operator_id');
|
||||
|
||||
$table->string('name');
|
||||
$table->string('file_path');
|
||||
$table->timestamps();
|
||||
|
||||
// Explicitly define the constraint
|
||||
$table->foreign('network_operator_id')
|
||||
->references('id')
|
||||
->on('network_operators')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('network_operator_documents');
|
||||
}
|
||||
};
|
||||
33
features.md
Normal file
33
features.md
Normal file
@@ -0,0 +1,33 @@
|
||||
Click ERP
|
||||
|
||||
|
||||
Client Management (CRM)
|
||||
• Client Profiles: Complete tracking of demographics, services, contract types, billing modes, and Account Manager assignments.
|
||||
|
||||
• Entity Record entries: A dedicated view for each client housing sub-records for Short Codes (SMS/Voice/USSD), Payments, and Onboarding Progress.
|
||||
|
||||
• Time-Locked Communications: A Notes system where staff can log client interactions.
|
||||
|
||||
• Client Data Table: A client's view with real-time search, advanced filtering by service/status, and CSV/PDF exports.
|
||||
|
||||
Staff Daily Reporting
|
||||
• Report Submission Portal: An interface for staff to log their daily activities, achievements, and challenges.
|
||||
|
||||
• Time-Window Enforcement: Strict backend validation that automatically locks report submissions 48 hours after the target date.
|
||||
|
||||
• Automated Dispatch: Automatic generation and routing of customized HTML email notifications to management upon successful report submission.
|
||||
|
||||
Global Offices & Holidays
|
||||
• Branch office Management: Centralized directory for managing physical office locations and assigning Country Managers.
|
||||
|
||||
• Public Holiday Management : The ability to enter recurring or localized public holidays
|
||||
|
||||
|
||||
Management & Admin Dashboard
|
||||
• Centralized Approvals: A dedicated hub for reviewing, approving, or rejecting staff Leave Requests.
|
||||
|
||||
• Compliance Tracking: Automated calculations comparing actual submitted reports against expected working days to gauge staff consistency.
|
||||
|
||||
• Report Overrides: A feature that allows management to temporarily unlock the 48-hour submission window for specific staff members and past dates.
|
||||
|
||||
|
||||
274
public/assets/js/mno-show.js
Normal file
274
public/assets/js/mno-show.js
Normal file
@@ -0,0 +1,274 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// ==========================================
|
||||
// 1. CONNECTION DETAILS LOGIC
|
||||
// ==========================================
|
||||
$('#connectionModeSelect').on('change', function() {
|
||||
if ($(this).val() === 'VPN Tunnel') {$('#vpnFormContainer').slideDown();
|
||||
} else {
|
||||
$('#vpnFormContainer').slideUp();
|
||||
$('#vpnFileInput').val('');
|
||||
}
|
||||
});
|
||||
|
||||
$('#addConnectionForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let $form =$(this);
|
||||
let $submitBtn =$('#btnSubmitConnection');
|
||||
let originalText = $submitBtn.html();
|
||||
let $alertBox =$('#connectionModalAlert');
|
||||
let formData = new FormData(this);
|
||||
|
||||
$submitBtn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
|
||||
$alertBox.html('');
|
||||
$form.find('.is-invalid').removeClass('is-invalid');$form.find('.invalid-feedback').remove();
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#addConnectionModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Success!',
|
||||
text: response.message,
|
||||
confirmButtonColor: '#5c4df0'
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
let errors = xhr.responseJSON.errors;
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div>Please check the highlighted fields below.</div>
|
||||
</div>
|
||||
`);
|
||||
$.each(errors, function(key, value) {
|
||||
let $input =$form.find('[name="' + key + '"]');
|
||||
if ($input.length) {
|
||||
$input.addClass('is-invalid');$input.parent().append('<div class="invalid-feedback d-block fw-medium small">' + value[0] + '</div>');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<div>An error occurred. Please try again.</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#addConnectionModal').on('hidden.bs.modal', function () {
|
||||
$('#addConnectionForm')[0].reset();
|
||||
$('#vpnFormContainer').hide();
|
||||
$('#connectionModalAlert').html('');
|
||||
$('#addConnectionForm').find('.is-invalid').removeClass('is-invalid');
|
||||
$('#addConnectionForm').find('.invalid-feedback').remove();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 2. MULTIPLE DOCUMENT UPLOAD LOGIC
|
||||
// ==========================================
|
||||
$('#addDocRowBtn').on('click', function() {
|
||||
let newRow = `
|
||||
<div class="row g-2 mb-3 document-row">
|
||||
<div class="col-md-5">
|
||||
<input type="text" name="document_names[]" class="form-control" placeholder="e.g. Rate Card" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="file" name="documents[]" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-outline-danger w-100 remove-doc-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#documentRowsContainer').append(newRow);
|
||||
updateRemoveButtons();
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-doc-row', function() {$(this).closest('.document-row').remove();
|
||||
updateRemoveButtons();
|
||||
});
|
||||
|
||||
function updateRemoveButtons() {
|
||||
if ($('.document-row').length > 1) {$('.remove-doc-row').prop('disabled', false);
|
||||
} else {
|
||||
$('.remove-doc-row').prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
$('#addDocumentForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let $form =$(this);
|
||||
let $submitBtn =$('#btnSubmitDocuments');
|
||||
let originalText = $submitBtn.html();
|
||||
let $alertBox =$('#documentModalAlert');
|
||||
let formData = new FormData(this);
|
||||
|
||||
$submitBtn.html('<span class="spinner-border spinner-border-sm me-2"></span>Uploading...').prop('disabled', true);
|
||||
$alertBox.html('');$form.find('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#addDocumentModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Uploaded!',
|
||||
text: response.message,
|
||||
confirmButtonColor: '#5c4df0'
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div>Validation failed. Ensure all files are valid formats and under 10MB.</div>
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
complete: function() {
|
||||
$submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#addDocumentModal').on('hidden.bs.modal', function () {
|
||||
$('#addDocumentForm')[0].reset();
|
||||
$('#documentModalAlert').html('');
|
||||
$('.document-row:not(:first)').remove();
|
||||
updateRemoveButtons();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 3. EDIT MNO PARTNER LOGIC
|
||||
// ==========================================
|
||||
$('.btn-edit-operator').on('click', function() {
|
||||
// Fetch data via the existing getJson endpoint
|
||||
$.ajax({
|
||||
url: `${baseUrl}/mnos/json/${operatorId}`, // Adjust URL if your route is named differently
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
$('#edit_mno_id').val(data.id);
|
||||
$('#edit_name').val(data.name);
|
||||
$('#edit_country').val(data.country);
|
||||
$('#edit_connection_status').val(data.connection_status);
|
||||
$('#edit_connection_type').val(data.connection_type);
|
||||
|
||||
$('#edit_mno_account_manager').val(data.mno_account_manager);
|
||||
$('#edit_technical_support_person').val(data.technical_support_person);
|
||||
$('#edit_contact_person').val(data.contact_person);
|
||||
$('#edit_contact_person_phone').val(data.contact_person_phone);
|
||||
$('#edit_contact_person_email').val(data.contact_person_email);
|
||||
$('#edit_support_skype').val(data.support_skype);
|
||||
|
||||
$('#edit_buying_rate').val(data.buying_rate);
|
||||
$('#edit_rate_type').val(data.rate_type);
|
||||
$('#edit_payment_terms').val(data.payment_terms);
|
||||
|
||||
// Reset tabs to Basic Info
|
||||
$('#editOperatorModal .nav-link').removeClass('active');
|
||||
$('#editOperatorModal .tab-pane').removeClass('show active');
|
||||
$('#editOperatorModal .nav-link').first().addClass('active');
|
||||
$('#edit-tab-basic').addClass('show active');
|
||||
|
||||
$('#edit_contract_type').val(data.contract_type);
|
||||
$('#edit_contract_validity').val(data.contract_validity);
|
||||
|
||||
// Handle Boolean for Auto Renew
|
||||
let isAutoRenew = data.contract_auto_renew === false || data.contract_auto_renew === 0 ? '0' : '1';
|
||||
$('#edit_contract_auto_renew').val(isAutoRenew).trigger('change');
|
||||
|
||||
// Format date to YYYY-MM-DD for the HTML date input if it exists
|
||||
if (data.next_renewal_date) {
|
||||
let formattedDate = data.next_renewal_date.split('T')[0];
|
||||
$('#edit_next_renewal_date').val(formattedDate);
|
||||
} else {
|
||||
$('#edit_next_renewal_date').val('');
|
||||
}
|
||||
|
||||
// Reset tabs to Basic Info
|
||||
$('#editOperatorModal .nav-link').removeClass('active');
|
||||
$('#editOperatorModal .tab-pane').removeClass('show active');
|
||||
$('#editOperatorModal .nav-link').first().addClass('active');
|
||||
$('#edit-tab-basic').addClass('show active');
|
||||
|
||||
$('#editOperatorModal').modal('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
// Toggle the Renewal Date field based on Auto Renew selection
|
||||
$('#edit_contract_auto_renew').on('change', function() {
|
||||
if ($(this).val() === '0') {$('#renewal_date_container').slideDown();
|
||||
$('#edit_next_renewal_date').prop('required', true);
|
||||
} else {
|
||||
$('#renewal_date_container').slideUp();
|
||||
$('#edit_next_renewal_date').prop('required', false).val('');
|
||||
}
|
||||
});
|
||||
$('#editOperatorForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form =$(this);
|
||||
let $btn =$('#btnUpdateOperator');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Updating...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');$form.find('.invalid-feedback').remove();
|
||||
|
||||
$.ajax({
|
||||
url: `${baseUrl}/mnos/${operatorId}`, // PUT endpoint
|
||||
type: 'POST', // Laravel uses POST + @method('PUT') for forms
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#editOperatorModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Updated!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input =$form.find('[name="' + key + '"]');
|
||||
$input.addClass('is-invalid');$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
|
||||
});
|
||||
|
||||
// Auto-switch to the tab with the error
|
||||
let $firstError =$form.find('.is-invalid').first();
|
||||
if ($firstError.length) {
|
||||
let tabId = $firstError.closest('.tab-pane').attr('id');$form.find('[data-bs-target="#' + tabId + '"]').tab('show');
|
||||
}
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
183
public/assets/js/vpn-index.js
Normal file
183
public/assets/js/vpn-index.js
Normal file
@@ -0,0 +1,183 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// 1. Add VPN Config
|
||||
$('#addVpnForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let $btn = $('#btnSaveVpn');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/vpns`,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#addVpnModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Added!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
$input.addClass('is-invalid');
|
||||
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
|
||||
});
|
||||
|
||||
let $firstError = $form.find('.is-invalid').first();
|
||||
if ($firstError.length) {
|
||||
let tabId = $firstError.closest('.tab-pane').attr('id');
|
||||
$form.find('[data-bs-target="#' + tabId + '"]').tab('show');
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Error', 'An unexpected error occurred.', 'error');
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Fetch Data for Edit Modal
|
||||
$(document).on('click', '.btn-edit-vpn', function(e) {
|
||||
e.preventDefault();
|
||||
let vpnId = $(this).data('id');
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/vpns/${vpnId}`,
|
||||
type: 'GET',
|
||||
success: function(vpn) {
|
||||
// Basic Info Tab
|
||||
$('#edit_vpn_id').val(vpn.id);
|
||||
$('#edit_partner_name').val(vpn.partner_name);
|
||||
$('#edit_status').val(vpn.status);
|
||||
$('#edit_local_device').val(vpn.local_device);
|
||||
$('#edit_peer_device').val(vpn.peer_device);
|
||||
$('#edit_local_ip').val(vpn.local_ip);
|
||||
$('#edit_peer_ip').val(vpn.peer_ip);
|
||||
$('#edit_encryption_domain_local').val(vpn.encryption_domain_local);
|
||||
$('#edit_encryption_domain_remote').val(vpn.encryption_domain_remote);
|
||||
|
||||
// Phase 1 Tab
|
||||
$('#edit_ike_version').val(vpn.ike_version);
|
||||
$('#edit_phase1_encryption').val(vpn.phase1_encryption);
|
||||
$('#edit_phase1_hash').val(vpn.phase1_hash);
|
||||
$('#edit_phase1_dh_group').val(vpn.phase1_dh_group);
|
||||
$('#edit_phase1_lifetime').val(vpn.phase1_lifetime);
|
||||
|
||||
// Phase 2 Tab
|
||||
$('#edit_phase2_encryption').val(vpn.phase2_encryption);
|
||||
$('#edit_phase2_hash').val(vpn.phase2_hash);
|
||||
$('#edit_phase2_dh_group').val(vpn.phase2_dh_group);
|
||||
$('#edit_phase2_lifetime').val(vpn.phase2_lifetime);
|
||||
|
||||
// Handle Checkbox for PFS
|
||||
if (vpn.pfs_enabled == 1 || vpn.pfs_enabled === true) {
|
||||
$('#edit_pfs_enabled').prop('checked', true);
|
||||
} else {
|
||||
$('#edit_pfs_enabled').prop('checked', false);
|
||||
}
|
||||
|
||||
// Reset tabs so it opens to the first tab by default
|
||||
$('#editVpnModal .nav-link').removeClass('active');
|
||||
$('#editVpnModal .tab-pane').removeClass('show active');
|
||||
$('#editVpnModal .nav-link').first().addClass('active');
|
||||
$('#edit-tab-basic').addClass('show active');
|
||||
|
||||
$('#editVpnModal').modal('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Update VPN Config
|
||||
$('#editVpnForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let vpnId = $('#edit_vpn_id').val();
|
||||
let $btn = $('#btnUpdateVpn');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Updating...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
$form.find('.invalid-feedback').remove();
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/vpns/${vpnId}`,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#editVpnModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Updated!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
$input.addClass('is-invalid');
|
||||
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
|
||||
});
|
||||
|
||||
|
||||
let $firstError = $form.find('.is-invalid').first();
|
||||
if ($firstError.length) {
|
||||
let tabId = $firstError.closest('.tab-pane').attr('id');
|
||||
$form.find('[data-bs-target="#' + tabId + '"]').tab('show');
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Error', 'An unexpected error occurred.', 'error');
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Export VPN Configuration (Trigger Modal)
|
||||
$(document).on('click', '.btn-export-vpn', function(e) {
|
||||
e.preventDefault();
|
||||
let vpnId = $(this).data('id');
|
||||
let $btn = $(this);
|
||||
let originalHtml = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm"></span>').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/vpns/${vpnId}/template`,
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
$('#export_partner_name').val(response.partner_name);
|
||||
$('#export_config_content').val(response.template);
|
||||
$('#exportVpnModal').modal('show');
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire('Error', 'Failed to generate configuration template.', 'error');
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalHtml).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up modals on close
|
||||
$('#addVpnModal, #editVpnModal, #exportVpnModal').on('hidden.bs.modal', function () {
|
||||
$(this).find('form')[0].reset();
|
||||
$(this).find('.is-invalid').removeClass('is-invalid');
|
||||
$(this).find('.invalid-feedback').remove();
|
||||
});
|
||||
});
|
||||
203
resources/views/activity-logs/index.blade.php
Normal file
203
resources/views/activity-logs/index.blade.php
Normal file
@@ -0,0 +1,203 @@
|
||||
@extends('layouts.masterbeta')
|
||||
|
||||
@section('title', 'Click ERP - System Activity Logs')
|
||||
|
||||
@section('breadcrumbs')
|
||||
<a href="{{ url('/') }}" class="text-secondary text-decoration-none"><i class="bi bi-house me-2"></i></a>
|
||||
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
|
||||
<span class="fw-bold" style="font-size: 0.95rem;">Activity Logs</span>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">System Audit Trail</h4>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Monitor user actions, data modifications, and security events.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-card shadow-sm bg-white rounded-3 overflow-hidden">
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="p-3 border-bottom bg-light bg-opacity-50">
|
||||
<form method="GET" action="{{ route('activity-logs.index') }}" class="row g-2 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control bg-white border-start-0 ps-0" name="search" value="{{ request('search') }}" placeholder="Search user or entity (e.g. VpnConfig)...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select form-select-sm" name="action_type">
|
||||
<option value="">All Actions</option>
|
||||
<option value="created" {{ request('action_type') == 'created' ? 'selected' : '' }}>Created</option>
|
||||
<option value="updated" {{ request('action_type') == 'updated' ? 'selected' : '' }}>Updated</option>
|
||||
<option value="deleted" {{ request('action_type') == 'deleted' ? 'selected' : '' }}>Deleted</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control form-control-sm" name="date_from" value="{{ request('date_from') }}" placeholder="From">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control form-control-sm" name="date_to" value="{{ request('date_to') }}" placeholder="To">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-sm btn-dark w-100 fw-bold">Filter</button>
|
||||
<a href="{{ route('activity-logs.index') }}" class="btn btn-sm btn-outline-secondary" title="Clear Filters"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary" style="font-size: 0.85rem;">
|
||||
<tr>
|
||||
<th class="ps-4">Timestamp</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Entity Context</th>
|
||||
<th class="text-end pe-4">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($logs as $log)
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="fw-bold text-dark" style="font-size: 0.9rem;">{{ $log->created_at->format('d M Y') }}</div>
|
||||
<div class="text-secondary small">{{ $log->created_at->format('h:i:s A') }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ $log->user->name ?? 'System/Guest' }}</div>
|
||||
<div class="text-secondary font-monospace" style="font-size: 0.75rem;">{{ $log->ip_address }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$badgeColor = match($log->action) {
|
||||
'created' => 'success',
|
||||
'updated' => 'primary',
|
||||
'deleted' => 'danger',
|
||||
default => 'secondary'
|
||||
};
|
||||
@endphp
|
||||
<span class="badge bg-{{ $badgeColor }} bg-opacity-10 text-{{ $badgeColor }} text-uppercase">
|
||||
{{ $log->action }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-dark fw-medium" style="font-size: 0.85rem;">
|
||||
{{ class_basename($log->loggable_type) }} <span class="text-secondary">#{{ $log->loggable_id }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
@if(!empty($log->old_data) || !empty($log->new_data))
|
||||
<button class="btn btn-sm btn-light text-primary border fw-bold btn-view-changes"
|
||||
data-action="{{ $log->action }}"
|
||||
data-old="{{ json_encode($log->old_data) }}"
|
||||
data-new="{{ json_encode($log->new_data) }}"
|
||||
data-entity="{{ class_basename($log->loggable_type) }} #{{ $log->loggable_id }}">
|
||||
<i class="bi bi-eye me-1"></i> View Data
|
||||
</button>
|
||||
@else
|
||||
<span class="text-muted small">No Context</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-secondary">
|
||||
<i class="bi bi-journal-x fs-1 text-muted mb-2 d-block"></i>
|
||||
No activity records found.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($logs->hasPages())
|
||||
<div class="card-footer bg-white border-top p-3 d-flex justify-content-center">
|
||||
{{ $logs->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('modals')
|
||||
<!-- VIEW CHANGES MODAL -->
|
||||
<div class="modal fade" id="viewChangesModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold" id="modalEntityTitle">Data Context</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 bg-light">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6" id="oldDataCol">
|
||||
<h6 class="fw-bold text-danger mb-2"><i class="bi bi-dash-circle me-1"></i> Previous State</h6>
|
||||
<div class="card border-danger border-opacity-25 shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<pre class="m-0 p-3 text-danger" style="font-size: 0.8rem; overflow-x: auto;" id="oldDataContainer"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" id="newDataCol">
|
||||
<h6 class="fw-bold text-success mb-2"><i class="bi bi-plus-circle me-1"></i> New State</h6>
|
||||
<div class="card border-success border-opacity-25 shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<pre class="m-0 p-3 text-success" style="font-size: 0.8rem; overflow-x: auto;" id="newDataContainer"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.btn-view-changes').on('click', function() {
|
||||
let action = $(this).data('action');
|
||||
let entity = $(this).data('entity');
|
||||
|
||||
// Parse the JSON data passed from the Blade attributes
|
||||
let oldData = $(this).data('old');
|
||||
let newData = $(this).data('new');
|
||||
|
||||
$('#modalEntityTitle').text('Context: ' + entity);
|
||||
|
||||
let $oldCol = $('#oldDataCol');
|
||||
let $newCol = $('#newDataCol');
|
||||
let $oldContainer = $('#oldDataContainer');
|
||||
let $newContainer = $('#newDataContainer');
|
||||
|
||||
// Reset layout
|
||||
$oldCol.show().removeClass('col-md-12').addClass('col-md-6');
|
||||
$newCol.show().removeClass('col-md-12').addClass('col-md-6');
|
||||
|
||||
// Format JSON cleanly, checking if null
|
||||
let formattedOld = oldData && oldData !== "null" ? JSON.stringify(oldData, null, 4) : 'No previous data (or field was empty).';
|
||||
let formattedNew = newData && newData !== "null" ? JSON.stringify(newData, null, 4) : 'No new data recorded.';
|
||||
|
||||
$oldContainer.text(formattedOld);
|
||||
$newContainer.text(formattedNew);
|
||||
|
||||
// Adjust layout based on action type to maximize reading space
|
||||
if (action === 'created') {
|
||||
$oldCol.hide();
|
||||
$newCol.removeClass('col-md-6').addClass('col-md-12');
|
||||
} else if (action === 'deleted') {
|
||||
$newCol.hide();
|
||||
$oldCol.removeClass('col-md-6').addClass('col-md-12');
|
||||
}
|
||||
|
||||
$('#viewChangesModal').modal('show');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -130,10 +130,10 @@
|
||||
<!-- Left Column: Pending Leaves & Documents -->
|
||||
<div class="col-lg-8">
|
||||
<div class="content-card shadow-sm mb-4">
|
||||
<div class="p-3 border-bottom d-flex justify-content-between align-items-center bg-light bg-opacity-50">
|
||||
<!-- <div class="p-3 border-bottom d-flex justify-content-between align-items-center bg-light bg-opacity-50">
|
||||
<h6 class="fw-bold mb-0"><i class="bi bi-clock-history me-2 text-warning"></i> Pending Leave Approvals</h6>
|
||||
<a href="{{ route('leave.index') }}" class="small text-decoration-none fw-bold" style="color: #5c4df0;">View All</a>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-custom mb-0 align-middle">
|
||||
<thead>
|
||||
|
||||
@@ -38,6 +38,13 @@
|
||||
.table-custom th { background-color: #f8f9fc; color: #5a617a; font-weight: 600; font-size: 0.8rem; text-transform: uppercase; border-bottom: 2px solid #eaedf1; padding: 1rem; }
|
||||
.table-custom td { padding: 1rem; vertical-align: middle; font-size: 0.9rem; border-bottom: 1px solid #eaedf1; }
|
||||
.form-control:focus, .form-select:focus { border-color: #5c4df0; box-shadow: 0 0 0 0.25rem rgba(92, 77, 240, 0.25); }
|
||||
|
||||
.sidebar-nav-item .toggle-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.sidebar-nav-item.collapsed .toggle-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@stack('styles')
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<div class="mb-0 fs-6">Click <span class="badge bg-secondary ms-1" style="font-size: 0.8rem;">ERP</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 mb-3">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
||||
@@ -16,6 +15,11 @@
|
||||
<a href="{{ url('/') }}" class="sidebar-nav-item {{ request()->is('/') ? 'active' : '' }}">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||
</a>
|
||||
@if(\Auth::user()->department->name == 'Directors')
|
||||
<a href="{{ url('management') }}" class="sidebar-nav-item {{ request()->is('management') ? 'active' : '' }}">
|
||||
<i class="bi bi-border-center me-2"></i> Management Hub
|
||||
</a>
|
||||
@endif
|
||||
<div class="sidebar-heading mt-2">Core Entities</div>
|
||||
<a href="{{ url('/clients') }}" class="sidebar-nav-item {{ request()->is('clients*') ? 'active' : '' }}">
|
||||
<i class="bi bi-people me-2"></i> Clients
|
||||
@@ -36,16 +40,47 @@
|
||||
<a href="{{ url('/documents') }}" class="sidebar-nav-item {{ request()->is('documents*') ? 'active' : '' }}">
|
||||
<i class="bi bi-folder2-open me-2"></i> Document Vault
|
||||
</a>
|
||||
@if(\Auth::user()->department->name == 'Technical')
|
||||
<div class="sidebar-heading mt-2">Technical</div>
|
||||
|
||||
<!-- Infrastructure Parent Toggle -->
|
||||
<a href="#infrastructureMenu" data-bs-toggle="collapse"
|
||||
aria-expanded="{{ request()->is('servers*') ? 'true' : 'false' }}"
|
||||
class="sidebar-nav-item d-flex justify-content-between align-items-center {{ request()->is('servers*') ? '' : 'collapsed' }}">
|
||||
<span><i class="bi bi-hdd-network me-2"></i> Infrastructure</span>
|
||||
<i class="bi bi-chevron-down toggle-icon" style="font-size: 0.75rem;"></i>
|
||||
</a>
|
||||
|
||||
<!-- Collapsible Sub-links -->
|
||||
<div class="collapse {{ request()->is('servers*') ? 'show' : '' }}" id="infrastructureMenu">
|
||||
<div class="d-flex flex-column ms-3 mt-1 border-start border-secondary ps-2">
|
||||
<a href="{{ url('/servers') }}" class="sidebar-nav-item py-2 {{ request()->is('servers*') ? 'active' : '' }}" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-server me-2"></i> Servers
|
||||
</a>
|
||||
<a href="{{ url('/vpns') }}" class="sidebar-nav-item py-2 {{ request()->is('vpns*') ? 'active' : '' }}" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-shield-lock me-2"></i> VPN Management
|
||||
</a>
|
||||
<!--
|
||||
<a href="{{ url('/networking') }}" class="sidebar-nav-item py-2" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-router me-2"></i> Networking
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="sidebar-heading mt-2">Others</div>
|
||||
<a href="{{ url('/reports') }}" class="sidebar-nav-item {{ request()->is('reports*') ? 'active' : '' }}">
|
||||
<i class="bi bi-buildings me-2"></i> Reports
|
||||
</a>
|
||||
|
||||
<a href="{{ url('/offices') }}" class="sidebar-nav-item {{ request()->is('offices*') ? 'active' : '' }}">
|
||||
<i class="bi bi-buildings me-2"></i> Office Locations
|
||||
</a>
|
||||
<a href="{{ url('/staff') }}" class="sidebar-nav-item {{ request()->is('staff*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge me-2"></i> Staff Directory
|
||||
</a>
|
||||
<div class="sidebar-heading mt-2">System</div>
|
||||
<a href="{{ url('/activity-logs') }}" class="sidebar-nav-item {{ request()->is('activity-logs*') ? 'active' : '' }}">
|
||||
<i class="bi bi-clock-history me-2"></i> Activity Logs
|
||||
</a>
|
||||
</aside>
|
||||
@@ -40,12 +40,14 @@
|
||||
<div class="content-card p-4 h-100 border-start border-4 border-0" style="border-left-color: #10b981 !important;">
|
||||
<h6 class="text-secondary text-uppercase fw-bold mb-3" style="letter-spacing: 1px; font-size: 0.8rem;">Leave Policy Quick Guide</h6>
|
||||
<ul class="text-secondary mb-0 small" style="line-height: 1.8;">
|
||||
<li><strong>Policy 1</strong></li>
|
||||
<li><strong>Pending Requests:</strong> Can be canceled before HR approval. Once approved, contact HR to amend.</li>
|
||||
<li><strong>Policy 2</strong></li>
|
||||
<li><strong>Policy 3</strong></li>
|
||||
<li><strong><!-- Annual Leave:</strong> Deducted from your balance. Weekends are automatically excluded from the calculation.</li>
|
||||
<li><strong>Policy 4</strong></li>
|
||||
|
||||
<!-- <li><strong>Annual Leave:</strong> Deducted from your balance. Weekends are automatically excluded from the calculation.</li>
|
||||
<li><strong>Sick Leave:</strong> Requires a medical certificate for absences exceeding 2 consecutive days.</li>
|
||||
<li><strong> -->Pending Requests:</strong> Can be canceled before HR approval. Once approved, contact HR to amend.</li>
|
||||
-->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
200
resources/views/management/index.blade.php
Normal file
200
resources/views/management/index.blade.php
Normal file
@@ -0,0 +1,200 @@
|
||||
@extends('layouts.masterbeta')
|
||||
|
||||
@section('title', 'Click ERP - Management Dashboard')
|
||||
|
||||
@section('breadcrumbs')
|
||||
<a href="{{ url('/') }}" class="text-secondary text-decoration-none"><i class="bi bi-house me-2"></i></a>
|
||||
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
|
||||
<span class="fw-bold" style="font-size: 0.95rem;">Management Dashboard</span>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">Management Hub</h4>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Centralized oversight for leave and reports</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-light-subtle shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3 border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||
<!-- Tabs Navigation -->
|
||||
<ul class="nav nav-tabs card-header-tabs m-0" id="managementTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold text-dark" id="leaves-tab" data-bs-toggle="tab" data-bs-target="#leaves-pane" type="button">
|
||||
<i class="bi bi-calendar-event text-primary me-1"></i> Leave Approvals
|
||||
@if($pendingLeaves->count() > 0)
|
||||
<span class="badge bg-danger rounded-pill ms-1">{{ $pendingLeaves->count() }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" id="reports-tab" data-bs-toggle="tab" data-bs-target="#reports-pane" type="button">
|
||||
<i class="bi bi-journal-text text-primary me-1"></i> Staff Reports
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body tab-content bg-light p-4" id="managementTabsContent">
|
||||
|
||||
<!-- TAB 1: LEAVE MANAGEMENT -->
|
||||
<div class="tab-pane fade show active" id="leaves-pane" role="tabpanel">
|
||||
<h6 class="fw-bold mb-3 text-secondary">Pending Requests</h6>
|
||||
<div class="table-responsive bg-white border rounded shadow-sm">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary" style="font-size: 0.85rem;">
|
||||
<tr>
|
||||
<th class="ps-3">Staff Member</th>
|
||||
<th>Leave Type</th>
|
||||
<th>Duration</th>
|
||||
<th>Requested On</th>
|
||||
<th class="text-end pe-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($pendingLeaves as $leave)
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ $leave->user->name ?? 'N/A' }}</td>
|
||||
<td><span class="badge bg-info bg-opacity-10 text-info">{{ $leave->leave_type ?? 'Annual' }}</span></td>
|
||||
<td class="text-secondary small">
|
||||
{{ \Carbon\Carbon::parse($leave->start_date)->format('d M') }} - {{ \Carbon\Carbon::parse($leave->end_date)->format('d M Y') }}
|
||||
<br><span class="fw-medium text-dark">{{ $leave->total_days }} days</span>
|
||||
</td>
|
||||
<td class="text-secondary small">{{ $leave->created_at->format('d M Y') }}</td>
|
||||
<td class="text-end pe-3">
|
||||
<button class="btn btn-sm btn-success fw-bold px-3 btn-action-leave" data-id="{{ $leave->id }}" data-status="Approved">Approve</button>
|
||||
<button class="btn btn-sm btn-outline-danger fw-bold px-3 ms-1 btn-action-leave" data-id="{{ $leave->id }}" data-status="Rejected">Reject</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted small">
|
||||
<i class="bi bi-check2-circle fs-3 d-block mb-2 text-success"></i> All caught up. No pending leave requests.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: DAILY REPORTS TRACKING -->
|
||||
<div class="tab-pane fade" id="reports-pane" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-0">Report Submission Metrics</h6>
|
||||
<form class="d-flex gap-2" method="GET" action="{{ route('management.index') }}">
|
||||
<!-- Keeps the tab active on reload -->
|
||||
<input type="hidden" name="active_tab" value="reports-pane">
|
||||
<input type="date" name="start_date" class="form-control form-control-sm" value="{{ $startDate }}" required>
|
||||
<input type="date" name="end_date" class="form-control form-control-sm" value="{{ $endDate }}" required>
|
||||
<button type="submit" class="btn btn-sm btn-primary fw-bold">Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive bg-white border rounded shadow-sm">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary" style="font-size: 0.85rem;">
|
||||
<tr>
|
||||
<th class="ps-3">Staff Member</th>
|
||||
<th>Designation</th>
|
||||
<th>Reports Submitted</th>
|
||||
<th class="text-end pe-3">Performance (Approx)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($staffReports as $staff)
|
||||
@php
|
||||
// Calculate working days assuming 5-day work week roughly (can be refined based on actual business logic)
|
||||
$start = \Carbon\Carbon::parse($startDate);
|
||||
$end = \Carbon\Carbon::parse($endDate);
|
||||
$workDays = max($start->diffInDaysFiltered(function (Carbon\Carbon $date) {
|
||||
return $date->isWeekday();
|
||||
}, $end), 1); // Avoid division by zero
|
||||
|
||||
$completionPercentage = min(round(($staff->daily_reports_count / $workDays) * 100), 100);
|
||||
$barColor = $completionPercentage >= 80 ? 'bg-success' : ($completionPercentage >= 50 ? 'bg-warning' : 'bg-danger');
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">
|
||||
<i class="bi bi-person-circle text-secondary me-2"></i>{{ $staff->name }}
|
||||
</td>
|
||||
<td class="text-secondary small">{{ $staff->designation ?? 'Staff' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-primary rounded-pill px-3">{{ $staff->daily_reports_count }}</span>
|
||||
<span class="text-muted small ms-1">/ ~{{ $workDays }} expected</span>
|
||||
</td>
|
||||
<td class="text-end pe-3" style="width: 25%;">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<span class="fw-bold me-2" style="font-size: 0.8rem;">{{ $completionPercentage }}%</span>
|
||||
<div class="progress flex-grow-1" style="height: 6px; max-width: 100px;">
|
||||
<div class="progress-bar {{ $barColor }}" role="progressbar" style="width: {{ $completionPercentage }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Keep active tab on reload if returning from a filter submission
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTab = urlParams.get('active_tab');
|
||||
if (activeTab) {
|
||||
$('.nav-tabs button[data-bs-target="#' + activeTab + '"]').tab('show');
|
||||
}
|
||||
|
||||
// Handle Leave Approval / Rejection
|
||||
$('.btn-action-leave').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
let leaveId = $(this).data('id');
|
||||
let actionStatus = $(this).data('status');
|
||||
let confirmColor = actionStatus === 'Approved' ? '#198754' : '#dc3545';
|
||||
|
||||
Swal.fire({
|
||||
title: `Mark as ${actionStatus}?`,
|
||||
text: `This will update the staff member's leave request.`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: confirmColor,
|
||||
confirmButtonText: `Yes, ${actionStatus}`
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: "{{ url('management/leaves') }}/" + leaveId,
|
||||
type: 'POST',
|
||||
data: {
|
||||
_token: '{{ csrf_token() }}',
|
||||
_method: 'PUT',
|
||||
status: actionStatus
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Updated',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire('Error', 'Could not update the leave status.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -66,9 +66,10 @@
|
||||
<div class="col-md-3">
|
||||
<select name="connection_status" class="form-select bg-white" onchange="this.form.submit()">
|
||||
<option value="">Any Status</option>
|
||||
<option value="Active" {{ request('link_type') == 'vpn' ? 'selected' : '' }}>Active</option>
|
||||
<option value="Inactive" {{ request('link_type') == 'direct' ? 'selected' : '' }}>Inactive</option>
|
||||
<option value="Pending" {{ request('link_type') == 'direct' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="Active" {{ request('connection_status') == 'Active' ? 'selected' : '' }}>Active</option>
|
||||
<option value="Inactive" {{ request('connection_status') == 'Inactive' ? 'selected' : '' }}>Inactive</option>
|
||||
<option value="Pending" {{ request('connection_status') == 'Pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="Testing" {{ request('connection_status') == 'Testing' ? 'selected' : '' }}>Testing</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
258
resources/views/network_operators/partials/modals.blade.php
Normal file
258
resources/views/network_operators/partials/modals.blade.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<!-- Add Connection Modal -->
|
||||
<!-- Add Connection Modal -->
|
||||
<div class="modal fade" id="addConnectionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold">Add Interconnect Detail</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Added ID to the form -->
|
||||
<form id="addConnectionForm" action="{{ route('mnos.connections.store', $operator->id) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="modal-body p-4">
|
||||
<!-- Alert container for AJAX errors -->
|
||||
<div id="connectionModalAlert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Connection Mode *</label>
|
||||
<select name="connection_mode" id="connectionModeSelect" class="form-select" required>
|
||||
<option value="Whitelisted IP">Whitelisted IP Address</option>
|
||||
<option value="Domain">Domain / Hostname</option>
|
||||
<option value="VPN Tunnel">VPN Tunnel Endpoint</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="vpnFormContainer" style="display: none;">
|
||||
<label class="form-label text-secondary fw-semibold small">Upload VPN Form (PDF, DOCX)</label>
|
||||
<input type="file" name="vpn_file" id="vpnFileInput" class="form-control" accept=".pdf,.doc,.docx,.xlsx,.jpg,.png">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Identifier (IP / Domain / VPN Peer) *</label>
|
||||
<input type="text" name="identifier" class="form-control" required placeholder="e.g. 196.200.15.22 or smpp.operator.com">
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Port</label>
|
||||
<!-- Added type="number", min, and max -->
|
||||
<input type="number" name="port" class="form-control" placeholder="e.g. 2775" min="1" max="65535">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="Active">Active</option>
|
||||
<option value="Testing">Testing</option>
|
||||
<option value="Disabled">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Notes / Description</label>
|
||||
<textarea name="notes" class="form-control" rows="2" placeholder="Optional context or routing details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnSubmitConnection" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">Save Endpoint</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add Document Modal -->
|
||||
<div class="modal fade" id="addDocumentModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold">Upload Documents</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="addDocumentForm" action="{{ route('mnos.documents.store', $operator->id) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="modal-body p-4">
|
||||
<div id="documentModalAlert"></div>
|
||||
|
||||
<div id="documentRowsContainer">
|
||||
<!-- Initial Row -->
|
||||
<div class="row g-2 mb-3 document-row">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label text-secondary fw-semibold small">Document Name *</label>
|
||||
<input type="text" name="document_names[]" class="form-control" placeholder="e.g. Signed NDA" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">File *</label>
|
||||
<input type="file" name="documents[]" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger w-100 remove-doc-row" disabled>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="addDocRowBtn" class="btn btn-sm btn-light text-primary border fw-bold mt-2">
|
||||
<i class="bi bi-plus-lg me-1"></i> Add Another File
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnSubmitDocuments" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">Upload All</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT MNO MODAL -->
|
||||
<div class="modal fade" id="editOperatorModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Edit MNO Partner</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="editOperatorForm" novalidate>
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" id="edit_mno_id">
|
||||
|
||||
<div class="modal-body p-0">
|
||||
<ul class="nav nav-tabs px-4 pt-3 bg-light" role="tablist">
|
||||
<li class="nav-item"><button class="nav-link active fw-bold" data-bs-toggle="tab" data-bs-target="#edit-tab-basic" type="button">Basic & Tech</button></li>
|
||||
<li class="nav-item"><button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#edit-tab-contacts" type="button">Contacts</button></li>
|
||||
<li class="nav-item"><button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#edit-tab-billing" type="button">Billing</button></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content p-4">
|
||||
<!-- TAB 1: BASIC INFO -->
|
||||
<div class="tab-pane fade show active" id="edit-tab-basic">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Name *</label>
|
||||
<input type="text" class="form-control" name="name" id="edit_name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Country *</label>
|
||||
<input type="text" class="form-control" name="country" id="edit_country" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Connection Status</label>
|
||||
<select class="form-select" name="connection_status" id="edit_connection_status">
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
<option value="Testing">Testing</option>
|
||||
<option value="Pending">Pending</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Connection Type</label>
|
||||
<select class="form-select" name="connection_type" id="edit_connection_type">
|
||||
<option value="DIRECT">Direct</option>
|
||||
<option value="VPN">VPN</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: CONTACTS -->
|
||||
<div class="tab-pane fade" id="edit-tab-contacts">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Account Manager</label>
|
||||
<select class="form-select" name="mno_account_manager" id="edit_mno_account_manager">
|
||||
<option value="">Select Account Manager</option>
|
||||
@foreach($staffMembers as $staff)
|
||||
<option value="{{ $staff->name }}">{{ $staff->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Technical Support Lead</label>
|
||||
<input type="text" class="form-control" name="technical_support_person" id="edit_technical_support_person">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Contact Person</label>
|
||||
<input type="text" class="form-control" name="contact_person" id="edit_contact_person">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Contact Phone</label>
|
||||
<input type="text" class="form-control" name="contact_person_phone" id="edit_contact_person_phone">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Contact Email</label>
|
||||
<input type="email" class="form-control" name="contact_person_email" id="edit_contact_person_email">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-secondary fw-semibold small">Support MS Teams / Skype</label>
|
||||
<input type="text" class="form-control" name="support_skype" id="edit_support_skype">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: BILLING -->
|
||||
<div class="tab-pane fade" id="edit-tab-billing">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 mt-3 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-file-earmark-text me-2"></i>Contract Details</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Contract Type</label>
|
||||
<select class="form-select" name="contract_type" id="edit_contract_type">
|
||||
<option value="">Select Type...</option>
|
||||
<option value="Bilateral">Bilateral</option>
|
||||
<option value="Unilateral">Unilateral</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Contract Validity</label>
|
||||
<select class="form-select" name="contract_validity" id="edit_contract_validity">
|
||||
<option value="">Select Validity...</option>
|
||||
<option value="Quarterly">Quarterly</option>
|
||||
<option value="Biannual">Biannual</option>
|
||||
<option value="Yearly">Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Auto Renew?</label>
|
||||
<select class="form-select" name="contract_auto_renew" id="edit_contract_auto_renew">
|
||||
<option value="1">Yes</option>
|
||||
<option value="0">No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4" id="renewal_date_container" style="display: none;">
|
||||
<label class="form-label text-secondary fw-semibold small">Next Renewal Date *</label>
|
||||
<input type="date" class="form-control" name="next_renewal_date" id="edit_next_renewal_date">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Buying Rate</label>
|
||||
<input type="number" step="0.0001" class="form-control" name="buying_rate" id="edit_buying_rate">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Rate Type</label>
|
||||
<input type="text" class="form-control" name="rate_type" id="edit_rate_type" placeholder="e.g. Flat, Tiered">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Payment Terms</label>
|
||||
<input type="text" class="form-control" name="payment_terms" id="edit_payment_terms" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnUpdateOperator" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Update Partner
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,13 +21,16 @@
|
||||
<h3 class="fw-bold mb-1">{{ $operator->name }}</h3>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">
|
||||
<i class="bi bi-geo-alt me-1"></i> {{ $operator->country }} •
|
||||
<span class="badge {{ $operator->connection_status == 'Online' ? 'bg-success' : 'bg-danger' }} ms-1">
|
||||
{{ $operator->connection_status ?? 'Offline' }}
|
||||
<span class="badge {{ $operator->connection_status == 'Active' ? 'bg-success' : 'bg-danger' }} ms-1">
|
||||
{{ $operator->connection_status ?? 'Inactive' }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary btn-sm fw-bold btn-edit-operator" data-id="{{ $operator->id }}">
|
||||
<i class="bi bi-pencil me-1"></i> Edit MNO
|
||||
</button>
|
||||
<a href="{{ route('mnos.index') }}" class="btn btn-outline-secondary btn-sm fw-bold">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to List
|
||||
</a>
|
||||
@@ -66,6 +69,7 @@
|
||||
</h5>
|
||||
<hr class="text-muted opacity-25">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
<span class="text-muted small d-block">Operating Country</span>
|
||||
<strong class="text-dark">{{ $operator->country }}</strong>
|
||||
@@ -84,6 +88,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contract Details -->
|
||||
<div class="content-card p-4 mb-4">
|
||||
<h5 class="fw-bold mb-3 text-secondary" style="font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="bi bi-file-earmark-text me-2 text-primary"></i> Contract Details
|
||||
</h5>
|
||||
<hr class="text-muted opacity-25">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted small d-block">Contract Type</span>
|
||||
<strong class="text-dark">{{ $operator->contract_type ?? 'N/A' }}</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted small d-block">Contract Validity</span>
|
||||
<strong class="text-dark">{{ $operator->contract_validity ?? 'N/A' }}</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted small d-block">Auto Renew</span>
|
||||
@if(is_null($operator->contract_auto_renew))
|
||||
<strong class="text-dark">N/A</strong>
|
||||
@elseif($operator->contract_auto_renew)
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success">Yes</span>
|
||||
@else
|
||||
<span class="badge bg-danger bg-opacity-10 text-danger border border-danger mb-1">No</span>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">
|
||||
Renewal Date: <strong class="text-dark">{{ $operator->next_renewal_date ? $operator->next_renewal_date->format('d M Y') : 'Not Set' }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provisioned Services Badges -->
|
||||
<div class="content-card p-4">
|
||||
@@ -220,182 +254,70 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Documents Section -->
|
||||
<div class="content-card p-4 mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0 text-secondary" style="font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="bi bi-folder me-2 text-primary"></i> Operator Documents
|
||||
</h5>
|
||||
<button type="button" class="btn btn-sm text-white fw-bold" style="background-color: #5c4df0;" data-bs-toggle="modal" data-bs-target="#addDocumentModal">
|
||||
<i class="bi bi-cloud-upload me-1"></i> Upload Documents
|
||||
</button>
|
||||
</div>
|
||||
<hr class="text-muted opacity-25">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-custom align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document Name</th>
|
||||
<th>File Type</th>
|
||||
<th>Uploaded On</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($operator->documents as $doc)
|
||||
<tr>
|
||||
<td class="fw-bold">{{ $doc->name }}</td>
|
||||
<td><span class="badge bg-light text-dark border">{{ strtoupper(pathinfo($doc->file_path, PATHINFO_EXTENSION)) }}</span></td>
|
||||
<td>{{ $doc->created_at->format('d M Y') }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ asset('storage/' . $doc->file_path) }}" target="_blank" class="btn btn-sm btn-light text-primary me-2">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
<form action="{{ route('mnos.documents.destroy', $doc->id) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-light text-danger" onclick="return confirm('Delete this document?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-secondary py-4">No documents uploaded yet.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@push('modals')
|
||||
|
||||
<!-- Add Connection Modal -->
|
||||
<!-- Add Connection Modal -->
|
||||
<div class="modal fade" id="addConnectionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold">Add Interconnect Detail</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Added ID to the form -->
|
||||
<form id="addConnectionForm" action="{{ route('mnos.connections.store', $operator->id) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="modal-body p-4">
|
||||
<!-- Alert container for AJAX errors -->
|
||||
<div id="connectionModalAlert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Connection Mode *</label>
|
||||
<select name="connection_mode" id="connectionModeSelect" class="form-select" required>
|
||||
<option value="Whitelisted IP">Whitelisted IP Address</option>
|
||||
<option value="Domain">Domain / Hostname</option>
|
||||
<option value="VPN Tunnel">VPN Tunnel Endpoint</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="vpnFormContainer" style="display: none;">
|
||||
<label class="form-label text-secondary fw-semibold small">Upload VPN Form (PDF, DOCX)</label>
|
||||
<input type="file" name="vpn_file" id="vpnFileInput" class="form-control" accept=".pdf,.doc,.docx,.xlsx,.jpg,.png">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Identifier (IP / Domain / VPN Peer) *</label>
|
||||
<input type="text" name="identifier" class="form-control" required placeholder="e.g. 196.200.15.22 or smpp.operator.com">
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Port</label>
|
||||
<!-- Added type="number", min, and max -->
|
||||
<input type="number" name="port" class="form-control" placeholder="e.g. 2775" min="1" max="65535">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="Active">Active</option>
|
||||
<option value="Testing">Testing</option>
|
||||
<option value="Disabled">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Notes / Description</label>
|
||||
<textarea name="notes" class="form-control" rows="2" placeholder="Optional context or routing details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnSubmitConnection" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">Save Endpoint</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include('network_operators.partials.modals')
|
||||
@endpush
|
||||
@push('scripts')
|
||||
<script>
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log('foo bar')
|
||||
$('#connectionModeSelect').on('change', function() {
|
||||
if ($(this).val() === 'VPN Tunnel') {
|
||||
$('#vpnFormContainer').slideDown();
|
||||
} else {
|
||||
$('#vpnFormContainer').slideUp();
|
||||
$('#vpnFileInput').val(''); // Clear the file input if they switch away
|
||||
}
|
||||
});
|
||||
});
|
||||
$(document).ready(function() {
|
||||
// Toggle VPN File input visibility
|
||||
$('#connectionModeSelect').on('change', function() {
|
||||
if ($(this).val() === 'VPN Tunnel') {
|
||||
$('#vpnFormContainer').slideDown();
|
||||
} else {
|
||||
$('#vpnFormContainer').slideUp();
|
||||
$('#vpnFileInput').val('');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle AJAX Form Submission
|
||||
$('#addConnectionForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let $form = $(this);
|
||||
let $submitBtn = $('#btnSubmitConnection');
|
||||
let originalText = $submitBtn.html();
|
||||
let $alertBox = $('#connectionModalAlert');
|
||||
|
||||
// Use FormData to handle the file upload payload
|
||||
let formData = new FormData(this);
|
||||
|
||||
$submitBtn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
|
||||
$alertBox.html('');
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
$form.find('.invalid-feedback').remove();
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false, // Prevent jQuery from converting the data into a query string
|
||||
contentType: false, // Prevent jQuery from overriding the multipart header
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#addConnectionModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Success!',
|
||||
text: response.message,
|
||||
confirmButtonColor: '#5c4df0'
|
||||
}).then(() => {
|
||||
// Reload the page to reflect the new connection in the table
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
let errors = xhr.responseJSON.errors;
|
||||
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div>Please check the highlighted fields below.</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Highlight specific fields
|
||||
$.each(errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
if ($input.length) {
|
||||
$input.addClass('is-invalid');
|
||||
$input.parent().append('<div class="invalid-feedback d-block fw-medium small">' + value[0] + '</div>');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<div>An error occurred. Please try again.</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reset the form when the modal is closed
|
||||
$('#addConnectionModal').on('hidden.bs.modal', function () {
|
||||
$('#addConnectionForm')[0].reset();
|
||||
$('#vpnFormContainer').hide();
|
||||
$('#connectionModalAlert').html('');
|
||||
$('#addConnectionForm').find('.is-invalid').removeClass('is-invalid');
|
||||
$('#addConnectionForm').find('.invalid-feedback').remove();
|
||||
});
|
||||
});
|
||||
const baseUrl = "{{ url('/') }}";
|
||||
const operatorId = "{{ $operator->id }}";
|
||||
</script>
|
||||
<script src="{{ asset('public/assets/js/mno-show.js') }}"></script>
|
||||
|
||||
|
||||
@endpush
|
||||
496
resources/views/servers/index.blade.php
Normal file
496
resources/views/servers/index.blade.php
Normal file
@@ -0,0 +1,496 @@
|
||||
@extends('layouts.masterbeta')
|
||||
|
||||
@section('title', 'Click ERP - Server Infrastructure')
|
||||
|
||||
@section('breadcrumbs')
|
||||
<a href="{{ url('/') }}" class="text-secondary text-decoration-none"><i class="bi bi-house me-2"></i></a>
|
||||
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
|
||||
<span class="fw-bold" style="font-size: 0.95rem;">Servers</span>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">Infrastructure Management</h4>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Server list</p>
|
||||
</div>
|
||||
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" data-bs-toggle="modal" data-bs-target="#addServerModal">
|
||||
<i class="bi bi-plus-lg me-2"></i> Add Server
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content-card shadow-sm bg-white rounded-3 overflow-hidden">
|
||||
<!-- Filters Header -->
|
||||
<div class="p-3 border-bottom bg-light bg-opacity-50">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control bg-white border-start-0 ps-0" id="searchServer" placeholder="Search by name, IP, or OS ...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary" style="font-size: 0.85rem;">
|
||||
<tr>
|
||||
<th class="ps-4">Server / IP</th>
|
||||
<th>OS Version</th>
|
||||
<th>Services</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($servers as $server)
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="fw-bold text-dark">{{ $server->name }}</div>
|
||||
<div class="text-secondary font-monospace small"><i class="bi bi-hdd-network me-1"></i>{{ $server->ip_address }}</div>
|
||||
</td>
|
||||
<td class="text-secondary small">{{ $server->os_version ?? 'Unknown' }}</td>
|
||||
<td>
|
||||
@if(is_array($server->services))
|
||||
@foreach($server->services as $service)
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary border me-1">{{ $service }}</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span class="text-muted small">None</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ $server->status == 'Active' ? 'bg-success' : 'bg-warning' }} bg-opacity-10 text-{{ $server->status == 'Active' ? 'success' : 'warning' }}">
|
||||
{{ $server->status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<!-- Secure Access Button -->
|
||||
<button class="btn btn-sm btn-danger fw-bold btn-access-credentials me-2" data-id="{{ $server->id }}" data-name="{{ $server->name }}">
|
||||
<i class="bi bi-shield-lock-fill me-1"></i> View Password
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-light text-primary btn-edit-server" data-id="{{ $server->id }}" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-secondary">
|
||||
<i class="bi bi-server fs-1 text-muted mb-2 d-block"></i>
|
||||
No servers added yet.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('modals')
|
||||
<!-- OTP VERIFICATION MODAL -->
|
||||
<div class="modal fade" id="otpModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-dark text-white border-bottom-0">
|
||||
<h6 class="modal-title fw-bold"><i class="bi bi-shield-lock me-2"></i>Security Check</h6>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="verifyOtpForm">
|
||||
@csrf
|
||||
<div class="modal-body p-4 text-center">
|
||||
<input type="hidden" id="auth_server_id" name="server_id">
|
||||
|
||||
<i class="bi bi-envelope-check text-success" style="font-size: 2.5rem;"></i>
|
||||
<p class="text-secondary small mt-2 mb-3">An authorization code has been sent to your email. Enter it below to access credentials for <strong id="auth_server_name" class="text-dark"></strong>.</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control form-control-lg text-center fw-bold letter-spacing-3" name="otp" id="otp_input" placeholder="000000" maxlength="6" required autocomplete="off" style="letter-spacing: 5px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0 d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-sm btn-link text-secondary text-decoration-none" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnVerifyOtp" class="btn btn-dark btn-sm fw-bold px-4">
|
||||
Unlock
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ADD SERVER MODAL -->
|
||||
<div class="modal fade" id="addServerModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Add New Server</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="addServerForm">
|
||||
@csrf
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Server Name *</label>
|
||||
<input type="text" class="form-control" name="name" required placeholder="e.g. Main PBX Engine">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">IP Address *</label>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" required placeholder="192.168.x.x">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">OS Version</label>
|
||||
<input type="text" class="form-control" name="os_version" placeholder="e.g. AlmaLinux 9, Debian 12">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status *</label>
|
||||
<select class="form-select" name="status" required>
|
||||
<option value="Active" selected>Active</option>
|
||||
<option value="Maintenance">Maintenance</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-shield-lock me-2"></i>Access Credentials</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Username *</label>
|
||||
<input type="text" class="form-control" name="username" required value="root">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Password *</label>
|
||||
<input type="password" class="form-control" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Running Services</label>
|
||||
<select class="form-select select2-services" name="services[]" multiple="multiple" style="width: 100%;">
|
||||
<!-- Services will be entered dynamically as tags -->
|
||||
</select>
|
||||
<div class="form-text text-muted" style="font-size: 0.75rem;">Type a service (e.g., Docker, Asterisk, PostGIS) and press enter.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnSaveServer" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Save Server
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT SERVER MODAL -->
|
||||
<div class="modal fade" id="editServerModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Edit Server Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="editServerForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" id="edit_server_id">
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Server Name *</label>
|
||||
<input type="text" class="form-control" name="name" id="edit_name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">IP Address *</label>
|
||||
<input type="text" class="form-control font-monospace" name="ip_address" id="edit_ip_address" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">OS Version</label>
|
||||
<input type="text" class="form-control" name="os_version" id="edit_os_version">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status *</label>
|
||||
<select class="form-select" name="status" id="edit_status" required>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Maintenance">Maintenance</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-shield-lock me-2"></i>Access Credentials</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Username *</label>
|
||||
<input type="text" class="form-control" name="username" id="edit_username" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Update Password</label>
|
||||
<input type="password" class="form-control" name="password" placeholder="Leave blank to keep current password">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-3">
|
||||
<label class="form-label text-secondary fw-semibold small">Running Services</label>
|
||||
<select class="form-select select2-services" name="services[]" id="edit_services" multiple="multiple" style="width: 100%;"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnUpdateServer" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Update Server
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// 1. Request OTP when clicking "View Password"
|
||||
$('.btn-access-credentials').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
let $btn = $(this);
|
||||
let serverId = $btn.data('id');
|
||||
let serverName = $btn.data('name');
|
||||
let originalHtml = $btn.html();
|
||||
|
||||
// Set loading state on button
|
||||
$btn.html('<span class="spinner-border spinner-border-sm"></span>').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/servers/${serverId}/request-otp`,
|
||||
type: 'POST',
|
||||
data: { _token: '{{ csrf_token() }}' },
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Setup and show modal
|
||||
$('#auth_server_id').val(serverId);
|
||||
$('#auth_server_name').text(serverName);
|
||||
$('#otp_input').val('');
|
||||
$('#otpModal').modal('show');
|
||||
|
||||
// Auto-focus input after modal opens
|
||||
setTimeout(() => $('#otp_input').focus(), 500);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire('Error', 'Failed to send OTP. Please check your email configuration.', 'error');
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalHtml).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Submit OTP and Display Credentials
|
||||
$('#verifyOtpForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let $btn = $('#btnVerifyOtp');
|
||||
let serverId = $('#auth_server_id').val();
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Verifying...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/servers/${serverId}/verify-otp`,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#otpModal').modal('hide');
|
||||
|
||||
// Display credentials securely in a SweetAlert
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Access Granted',
|
||||
html: `
|
||||
<div class="text-start mt-3 p-3 bg-light border rounded">
|
||||
<div class="mb-2"><span class="text-muted small">Username:</span><br> <strong class="fs-5">${response.username}</strong></div>
|
||||
<div><span class="text-muted small">Password:</span><br> <strong class="fs-5 text-primary font-monospace" style="user-select: all;">${response.password}</strong></div>
|
||||
</div>
|
||||
<div class="text-muted small mt-3"><i class="bi bi-info-circle me-1"></i>This event has been logged.</div>
|
||||
`,
|
||||
confirmButtonColor: '#212529',
|
||||
confirmButtonText: 'Close & Clear'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$('#otp_input').addClass('is-invalid');
|
||||
// Shake animation for incorrect code
|
||||
$('#otpModal .modal-content').addClass('animate__animated animate__headShake');
|
||||
setTimeout(() => $('#otpModal .modal-content').removeClass('animate__animated animate__headShake'), 500);
|
||||
} else {
|
||||
Swal.fire('Error', 'A server error occurred.', 'error');
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Initialize Select2 for dynamic tagging
|
||||
$('.select2-services').select2({
|
||||
theme: 'bootstrap-5',
|
||||
tags: true, // Allows typing custom services
|
||||
tokenSeparators: [',', ' '],
|
||||
dropdownParent: function() {
|
||||
if ($('#addServerModal').hasClass('show')) return $('#addServerModal');
|
||||
if ($('#editServerModal').hasClass('show')) return $('#editServerModal');
|
||||
return $(document.body);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Add Server Submission
|
||||
$('#addServerForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let $btn = $('#btnSaveServer');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
$.ajax({
|
||||
url: "{{ route('servers.store') }}",
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#addServerModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Added!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
if (key === 'services') $input = $form.find('[name="services[]"]');
|
||||
|
||||
if ($input.hasClass('select2-hidden-accessible')) {
|
||||
$input.next('.select2-container').addClass('is-invalid');
|
||||
} else {
|
||||
$input.addClass('is-invalid');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', 'An unexpected error occurred.', 'error');
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle Edit Modal Trigger
|
||||
$(document).on('click', '.btn-edit-server', function(e) {
|
||||
e.preventDefault();
|
||||
let serverId = $(this).data('id');
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/servers/${serverId}`,
|
||||
type: 'GET',
|
||||
success: function(server) {
|
||||
$('#edit_server_id').val(server.id);
|
||||
$('#edit_name').val(server.name);
|
||||
$('#edit_ip_address').val(server.ip_address);
|
||||
$('#edit_os_version').val(server.os_version);
|
||||
$('#edit_username').val(server.username);
|
||||
$('#edit_status').val(server.status);
|
||||
|
||||
// Populate Select2 tags
|
||||
let $services = $('#edit_services');
|
||||
$services.val(null).trigger('change');
|
||||
if (server.services && Array.isArray(server.services)) {
|
||||
server.services.forEach(function(val) {
|
||||
if ($services.find("option[value='" + val + "']").length === 0) {
|
||||
$services.append(new Option(val, val, true, true));
|
||||
}
|
||||
});
|
||||
$services.val(server.services).trigger('change');
|
||||
}
|
||||
|
||||
$('#editServerModal').modal('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle Edit Server Submission
|
||||
$('#editServerForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let serverId = $('#edit_server_id').val();
|
||||
let $btn = $('#btnUpdateServer');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Updating...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
$.ajax({
|
||||
url: `${base_url}/servers/${serverId}`,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#editServerModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Updated!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
// Same error handling logic as Add Server
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
if (key === 'services') $input = $form.find('[name="services[]"]');
|
||||
|
||||
if ($input.hasClass('select2-hidden-accessible')) {
|
||||
$input.next('.select2-container').addClass('is-invalid');
|
||||
} else {
|
||||
$input.addClass('is-invalid');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up modals on close
|
||||
$('#addServerModal, #editServerModal').on('hidden.bs.modal', function () {
|
||||
$(this).find('form')[0].reset();
|
||||
$(this).find('.select2-services').val(null).trigger('change');
|
||||
$(this).find('.is-invalid').removeClass('is-invalid');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">Short Codes Management</h4>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage SMS, USSD, and Voice short codes and validity periods.</p>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage SMS, USSD, and Voice short codes and their expiry dates.</p>
|
||||
</div>
|
||||
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" id="btnOpenShortCodeModal">
|
||||
<i class="bi bi-plus-lg me-2"></i> Add Short Code
|
||||
@@ -126,7 +126,7 @@
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">${{ number_format($code->monthly_fee, 2) }}</div>
|
||||
<div class="fw-bold text-dark">{{ number_format($code->monthly_fee, 2) }}</div>
|
||||
<div class="text-secondary" style="font-size: 0.75rem;">Monthly Fee</div>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
<th>Full Name</th>
|
||||
<th>Relationship</th>
|
||||
<th>Phone</th>
|
||||
<th>Medical Details</th>
|
||||
<!-- <th>Medical Details</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="viewStaffDependentsList">
|
||||
@@ -598,9 +598,8 @@
|
||||
dependentsHtml += `
|
||||
<tr>
|
||||
<td class="fw-bold">${dep.fullname}</td>
|
||||
<td><span class="badge bg-secondary bg-opacity-10 text-dark border">${dep.relationship}</span></td>
|
||||
<td><span class="badge bg-warning bg-opacity-10 text-dark border">${dep.relationship}</span></td>
|
||||
<td>${dep.phone || 'N/A'}</td>
|
||||
<td class="text-secondary">${dep.medical_details || 'None'}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
491
resources/views/vpns/index.blade copy.php
Normal file
491
resources/views/vpns/index.blade copy.php
Normal file
@@ -0,0 +1,491 @@
|
||||
@extends('layouts.masterbeta')
|
||||
|
||||
@section('title', 'Click ERP - VPN Management')
|
||||
|
||||
@section('breadcrumbs')
|
||||
<a href="{{ url('/') }}" class="text-secondary text-decoration-none"><i class="bi bi-house me-2"></i></a>
|
||||
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
|
||||
<span class="fw-bold" style="font-size: 0.95rem;">VPN Management</span>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">VPN Management</h4>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage IPsec configurations with partners.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Global Standard Template Export -->
|
||||
<a href="{{ route('vpns.standard-template') }}" class="btn btn-outline-dark fw-bold d-flex align-items-center" style="border-radius: 8px;">
|
||||
<i class="bi bi-file-earmark-arrow-down me-2"></i> Standard Parameters
|
||||
</a>
|
||||
|
||||
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" data-bs-toggle="modal" data-bs-target="#addVpnModal">
|
||||
<i class="bi bi-plus-lg me-2"></i> Add VPN Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-card shadow-sm bg-white rounded-3 overflow-hidden">
|
||||
<!-- Filters Header -->
|
||||
<div class="p-3 border-bottom bg-light bg-opacity-50">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control bg-white border-start-0 ps-0" id="searchVpn" placeholder="Search by partner name or IP...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary" style="font-size: 0.85rem;">
|
||||
<tr>
|
||||
<th class="ps-4">Partner</th>
|
||||
<th>Gateways (Local ➔ Peer)</th>
|
||||
<th>Encryption Domains (Local ➔ Remote)</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($vpns as $vpn)
|
||||
<tr>
|
||||
<td class="ps-4 fw-bold text-dark">{{ $vpn->partner_name }}</td>
|
||||
<td>
|
||||
<div class="font-monospace small">
|
||||
<span class="text-primary">{{ $vpn->local_ip }}</span>
|
||||
<i class="bi bi-arrow-right mx-1 text-secondary"></i>
|
||||
<span class="text-danger">{{ $vpn->peer_ip }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-monospace small text-secondary">
|
||||
{{ $vpn->encryption_domain_local ?? 'Any' }}
|
||||
<i class="bi bi-arrow-right mx-1"></i>
|
||||
{{ $vpn->encryption_domain_remote ?? 'Any' }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ $vpn->status == 'Active' ? 'bg-success' : 'bg-warning' }} bg-opacity-10 text-{{ $vpn->status == 'Active' ? 'success' : 'warning' }}">
|
||||
{{ $vpn->status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<button class="btn btn-sm btn-outline-dark fw-bold btn-export-vpn me-2" data-id="{{ $vpn->id }}">
|
||||
<i class="bi bi-download me-1"></i> Export
|
||||
</button>
|
||||
<button class="btn btn-sm btn-light text-primary btn-edit-vpn" data-id="{{ $vpn->id }}" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-secondary">
|
||||
<i class="bi bi-shield-slash fs-1 text-muted mb-2 d-block"></i>
|
||||
No VPN configurations added yet.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($vpns->hasPages())
|
||||
<div class="card-footer bg-white border-top p-3 d-flex justify-content-center">
|
||||
{{ $vpns->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('modals')
|
||||
<!-- ADD VPN MODAL -->
|
||||
<div class="modal fade" id="addVpnModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Add VPN Configuration</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="addVpnForm">
|
||||
@csrf
|
||||
<div class="modal-body p-0">
|
||||
<ul class="nav nav-tabs px-4 pt-3 bg-light" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold" data-bs-toggle="tab" data-bs-target="#tab-basic" type="button">Basic Info</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#tab-phase1" type="button">Phase 1 (IKE)</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#tab-phase2" type="button">Phase 2 (IPSec)</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content p-4">
|
||||
<!-- TAB 1: BASIC INFO -->
|
||||
<div class="tab-pane fade show active" id="tab-basic">
|
||||
<!-- Place your existing Partner Name, Status, IPs, Subnets, and PSK fields here -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Device</label>
|
||||
<input type="text" class="form-control" name="local_device" placeholder="e.g. Cisco Catalyst 8500">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Peer Device</label>
|
||||
<input type="text" class="form-control" name="peer_device" placeholder="e.g. FortiGate 61E">
|
||||
</div>
|
||||
<!-- ... existing gateway/subnet inputs ... -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: PHASE 1 (IKE) -->
|
||||
<div class="tab-pane fade" id="tab-phase1">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">IKE Version *</label>
|
||||
<select class="form-select" name="ike_version">
|
||||
<option value="1">v1</option>
|
||||
<option value="2" selected>v2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Encryption *</label>
|
||||
<select class="form-select" name="phase1_encryption">
|
||||
<option value="AES-128">AES-128</option>
|
||||
<option value="AES-256" selected>AES-256</option>
|
||||
<option value="3DES">3DES</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Hash *</label>
|
||||
<select class="form-select" name="phase1_hash">
|
||||
<option value="SHA-1">SHA-1</option>
|
||||
<option value="SHA-256" selected>SHA-256</option>
|
||||
<option value="SHA-512">SHA-512</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">DH Group *</label>
|
||||
<select class="form-select" name="phase1_dh_group">
|
||||
<option value="2">Group 2</option>
|
||||
<option value="14" selected>Group 14</option>
|
||||
<option value="21">Group 21</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Lifetime (Seconds) *</label>
|
||||
<input type="number" class="form-control" name="phase1_lifetime" value="28800" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: PHASE 2 (IPSEC) -->
|
||||
<div class="tab-pane fade" id="tab-phase2">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Encryption *</label>
|
||||
<select class="form-select" name="phase2_encryption">
|
||||
<option value="AES-128">AES-128</option>
|
||||
<option value="AES-256" selected>AES-256</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Hash *</label>
|
||||
<select class="form-select" name="phase2_hash">
|
||||
<option value="SHA-1">SHA-1</option>
|
||||
<option value="SHA-256" selected>SHA-256</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">DH Group</label>
|
||||
<select class="form-select" name="phase2_dh_group">
|
||||
<option value="">Match Phase 1</option>
|
||||
<option value="14">Group 14</option>
|
||||
<option value="21">Group 21</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Lifetime (Secs) *</label>
|
||||
<input type="number" class="form-control" name="phase2_lifetime" value="3600" required>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="pfs_enabled" value="1" checked id="pfsSwitch">
|
||||
<label class="form-check-label text-secondary fw-semibold small" for="pfsSwitch">Enable PFS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnSaveVpn" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT VPN MODAL -->
|
||||
<div class="modal fade" id="editVpnModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Edit VPN Configuration</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="editVpnForm">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" id="edit_vpn_id">
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Partner Name *</label>
|
||||
<input type="text" class="form-control" name="partner_name" id="edit_partner_name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status *</label>
|
||||
<select class="form-select" name="status" id="edit_status" required>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-router me-2"></i>Gateway IP Addresses</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Gateway IP *</label>
|
||||
<input type="text" class="form-control font-monospace" name="local_ip" id="edit_local_ip" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Peer Gateway IP *</label>
|
||||
<input type="text" class="form-control font-monospace" name="peer_ip" id="edit_peer_ip" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-diagram-3 me-2"></i>Encryption Domains (Subnets)</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Encryption Domain</label>
|
||||
<input type="text" class="form-control font-monospace" name="encryption_domain_local" id="edit_encryption_domain_local">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Remote Encryption Domain</label>
|
||||
<input type="text" class="form-control font-monospace" name="encryption_domain_remote" id="edit_encryption_domain_remote">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-key me-2"></i>Authentication</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label text-secondary fw-semibold small">Update Pre-Shared Key (PSK)</label>
|
||||
<input type="password" class="form-control font-monospace" name="preshared_key" placeholder="Leave blank to keep current PSK">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnUpdateVpn" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Update Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXPORT VPN CONFIG MODAL -->
|
||||
<div class="modal fade" id="exportVpnModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-dark text-white border-bottom-0">
|
||||
<h5 class="modal-title fw-bold"><i class="bi bi-file-earmark-code me-2"></i>Export Partner Config</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('vpns.download') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="partner_name" id="export_partner_name">
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<div class="alert alert-info py-2 d-flex align-items-center" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-info-circle-fill me-2 fs-5"></i>
|
||||
<div>You can edit the configuration parameters below before downloading the file to share with the partner.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control font-monospace bg-light" name="config_content" id="export_config_content" rows="15" required style="font-size: 0.85rem; white-space: pre;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0 d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success btn-sm fw-bold px-4">
|
||||
<i class="bi bi-download me-1"></i> Download .txt File
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let baseUrl = "{{ url('/') }}";
|
||||
|
||||
// 1. Add VPN Config
|
||||
$('#addVpnForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let $btn = $('#btnSaveVpn');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
$.ajax({
|
||||
url: `${baseUrl}/vpns`,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#addVpnModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Added!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
$input.addClass('is-invalid');
|
||||
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', 'An unexpected error occurred.', 'error');
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Fetch Data for Edit Modal
|
||||
$(document).on('click', '.btn-edit-vpn', function(e) {
|
||||
e.preventDefault();
|
||||
let vpnId = $(this).data('id');
|
||||
|
||||
$.ajax({
|
||||
url: `${baseUrl}/vpns/${vpnId}`,
|
||||
type: 'GET',
|
||||
success: function(vpn) {
|
||||
$('#edit_vpn_id').val(vpn.id);
|
||||
$('#edit_partner_name').val(vpn.partner_name);
|
||||
$('#edit_local_ip').val(vpn.local_ip);
|
||||
$('#edit_peer_ip').val(vpn.peer_ip);
|
||||
$('#edit_encryption_domain_local').val(vpn.encryption_domain_local);
|
||||
$('#edit_encryption_domain_remote').val(vpn.encryption_domain_remote);
|
||||
$('#edit_status').val(vpn.status);
|
||||
|
||||
$('#editVpnModal').modal('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Update VPN Config
|
||||
$('#editVpnForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
let $form = $(this);
|
||||
let vpnId = $('#edit_vpn_id').val();
|
||||
let $btn = $('#btnUpdateVpn');
|
||||
let originalText = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span>Updating...').prop('disabled', true);
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
$form.find('.invalid-feedback').remove();
|
||||
|
||||
$.ajax({
|
||||
url: `${baseUrl}/vpns/${vpnId}`,
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
$('#editVpnModal').modal('hide');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Updated!',
|
||||
text: response.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => location.reload());
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 422) {
|
||||
$.each(xhr.responseJSON.errors, function(key, value) {
|
||||
let $input = $form.find('[name="' + key + '"]');
|
||||
$input.addClass('is-invalid');
|
||||
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
|
||||
});
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Export VPN Configuration (Trigger Modal)
|
||||
$(document).on('click', '.btn-export-vpn', function(e) {
|
||||
e.preventDefault();
|
||||
let vpnId = $(this).data('id');
|
||||
let $btn = $(this);
|
||||
let originalHtml = $btn.html();
|
||||
|
||||
$btn.html('<span class="spinner-border spinner-border-sm"></span>').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: `${baseUrl}/vpns/${vpnId}/template`,
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
$('#export_partner_name').val(response.partner_name);
|
||||
$('#export_config_content').val(response.template);
|
||||
$('#exportVpnModal').modal('show');
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire('Error', 'Failed to generate configuration template.', 'error');
|
||||
},
|
||||
complete: function() {
|
||||
$btn.html(originalHtml).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up modals on close
|
||||
$('#addVpnModal, #editVpnModal, #exportVpnModal').on('hidden.bs.modal', function () {
|
||||
$(this).find('form')[0].reset();
|
||||
$(this).find('.is-invalid').removeClass('is-invalid');
|
||||
$(this).find('.invalid-feedback').remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
505
resources/views/vpns/index.blade.php
Normal file
505
resources/views/vpns/index.blade.php
Normal file
@@ -0,0 +1,505 @@
|
||||
@extends('layouts.masterbeta')
|
||||
|
||||
@section('title', 'Click ERP - VPN Management')
|
||||
|
||||
@section('breadcrumbs')
|
||||
<a href="{{ url('/') }}" class="text-secondary text-decoration-none"><i class="bi bi-house me-2"></i></a>
|
||||
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
|
||||
<span class="fw-bold" style="font-size: 0.95rem;">VPN Management</span>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">VPN Management</h4>
|
||||
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage IPsec configurations with partners.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Global Standard Template Export -->
|
||||
<a href="{{ route('vpns.standard-template') }}" class="btn btn-outline-dark fw-bold d-flex align-items-center" style="border-radius: 8px;">
|
||||
<i class="bi bi-file-earmark-arrow-down me-2"></i> Standard Parameters
|
||||
</a>
|
||||
|
||||
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" data-bs-toggle="modal" data-bs-target="#addVpnModal">
|
||||
<i class="bi bi-plus-lg me-2"></i> Add VPN Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-card shadow-sm bg-white rounded-3 overflow-hidden">
|
||||
<!-- Filters Header -->
|
||||
<div class="p-3 border-bottom bg-light bg-opacity-50">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control bg-white border-start-0 ps-0" id="searchVpn" placeholder="Search by partner name or IP...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary" style="font-size: 0.85rem;">
|
||||
<tr>
|
||||
<th class="ps-4">Partner</th>
|
||||
<th>Gateways (Local ➔ Peer)</th>
|
||||
<th>Encryption Domains (Local ➔ Remote)</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($vpns as $vpn)
|
||||
<tr>
|
||||
<td class="ps-4 fw-bold text-dark">{{ $vpn->partner_name }}</td>
|
||||
<td>
|
||||
<div class="font-monospace small">
|
||||
<span class="text-primary">{{ $vpn->local_ip }}</span>
|
||||
<i class="bi bi-arrow-right mx-1 text-secondary"></i>
|
||||
<span class="text-danger">{{ $vpn->peer_ip }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-monospace small text-secondary">
|
||||
{{ $vpn->encryption_domain_local ?? 'Any' }}
|
||||
<i class="bi bi-arrow-right mx-1"></i>
|
||||
{{ $vpn->encryption_domain_remote ?? 'Any' }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ $vpn->status == 'Active' ? 'bg-success' : 'bg-warning' }} bg-opacity-10 text-{{ $vpn->status == 'Active' ? 'success' : 'warning' }}">
|
||||
{{ $vpn->status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<button class="btn btn-sm btn-outline-dark fw-bold btn-export-vpn me-2" data-id="{{ $vpn->id }}">
|
||||
<i class="bi bi-download me-1"></i> Export
|
||||
</button>
|
||||
<button class="btn btn-sm btn-light text-primary btn-edit-vpn" data-id="{{ $vpn->id }}" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-secondary">
|
||||
<i class="bi bi-shield-slash fs-1 text-muted mb-2 d-block"></i>
|
||||
No VPN configurations added yet.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($vpns->hasPages())
|
||||
<div class="card-footer bg-white border-top p-3 d-flex justify-content-center">
|
||||
{{ $vpns->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('modals')
|
||||
<!-- ADD VPN MODAL -->
|
||||
<!-- ADD VPN MODAL -->
|
||||
<div class="modal fade" id="addVpnModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Add VPN Configuration</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form id="addVpnForm" novalidate>
|
||||
@csrf
|
||||
<div class="modal-body p-0">
|
||||
<!-- Modal Navigation Tabs -->
|
||||
<ul class="nav nav-tabs px-4 pt-3 bg-light" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold" data-bs-toggle="tab" data-bs-target="#add-tab-basic" type="button">Basic Info</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#add-tab-phase1" type="button">Phase 1 (IKE)</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#add-tab-phase2" type="button">Phase 2 (IPSec)</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content p-4">
|
||||
<!-- TAB 1: BASIC INFO -->
|
||||
<div class="tab-pane fade show active" id="add-tab-basic">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Partner Name *</label>
|
||||
<input type="text" class="form-control" name="partner_name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status *</label>
|
||||
<select class="form-select" name="status" required>
|
||||
<option value="Active" selected>Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Device</label>
|
||||
<input type="text" class="form-control" name="local_device" placeholder="e.g. Cisco Catalyst 8500">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Peer Device</label>
|
||||
<input type="text" class="form-control" name="peer_device" placeholder="e.g. FortiGate 61E">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-router me-2"></i>Gateway IP Addresses</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Gateway IP *</label>
|
||||
<input type="text" class="form-control font-monospace" name="local_ip" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Peer Gateway IP *</label>
|
||||
<input type="text" class="form-control font-monospace" name="peer_ip" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-diagram-3 me-2"></i>Encryption Domains (Subnets)</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Encryption Domain</label>
|
||||
<input type="text" class="form-control font-monospace" name="encryption_domain_local" placeholder="e.g. 10.0.0.0/24">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Remote Encryption Domain</label>
|
||||
<input type="text" class="form-control font-monospace" name="encryption_domain_remote">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-key me-2"></i>Authentication</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label text-secondary fw-semibold small">Pre-Shared Key (PSK) *</label>
|
||||
<input type="password" class="form-control font-monospace" name="preshared_key" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: PHASE 1 (IKE) -->
|
||||
<div class="tab-pane fade" id="add-tab-phase1">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">IKE Version *</label>
|
||||
<select class="form-select" name="ike_version">
|
||||
<option value="1">v1</option>
|
||||
<option value="2" selected>v2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Encryption *</label>
|
||||
<select class="form-select" name="phase1_encryption">
|
||||
<option value="AES-128">AES-128</option>
|
||||
<option value="AES-256" selected>AES-256</option>
|
||||
<option value="3DES">3DES</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Hash *</label>
|
||||
<select class="form-select" name="phase1_hash">
|
||||
<option value="SHA-1">SHA-1</option>
|
||||
<option value="SHA-256" selected>SHA-256</option>
|
||||
<option value="SHA-512">SHA-512</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">DH Group *</label>
|
||||
<select class="form-select" name="phase1_dh_group">
|
||||
<option value="2">Group 2</option>
|
||||
<option value="14" selected>Group 14</option>
|
||||
<option value="21">Group 21</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Lifetime (Seconds) *</label>
|
||||
<input type="number" class="form-control" name="phase1_lifetime" value="28800" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: PHASE 2 (IPSEC) -->
|
||||
<div class="tab-pane fade" id="add-tab-phase2">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Encryption *</label>
|
||||
<select class="form-select" name="phase2_encryption">
|
||||
<option value="AES-128">AES-128</option>
|
||||
<option value="AES-256" selected>AES-256</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Hash *</label>
|
||||
<select class="form-select" name="phase2_hash">
|
||||
<option value="SHA-1">SHA-1</option>
|
||||
<option value="SHA-256" selected>SHA-256</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">DH Group</label>
|
||||
<select class="form-select" name="phase2_dh_group">
|
||||
<option value="">Match Phase 1</option>
|
||||
<option value="14">Group 14</option>
|
||||
<option value="21">Group 21</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Lifetime (Secs) *</label>
|
||||
<input type="number" class="form-control" name="phase2_lifetime" value="3600" required>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="pfs_enabled" value="1" checked id="addPfsSwitch">
|
||||
<label class="form-check-label text-secondary fw-semibold small" for="addPfsSwitch">Enable PFS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnSaveVpn" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT VPN MODAL -->
|
||||
<!-- EDIT VPN MODAL -->
|
||||
<div class="modal fade" id="editVpnModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Edit VPN Configuration</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<form id="editVpnForm" novalidate>
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" id="edit_vpn_id">
|
||||
|
||||
<div class="modal-body p-0">
|
||||
<!-- Modal Navigation Tabs -->
|
||||
<ul class="nav nav-tabs px-4 pt-3 bg-light" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold" data-bs-toggle="tab" data-bs-target="#edit-tab-basic" type="button">Basic Info</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#edit-tab-phase1" type="button">Phase 1 (IKE)</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold text-dark" data-bs-toggle="tab" data-bs-target="#edit-tab-phase2" type="button">Phase 2 (IPSec)</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content p-4">
|
||||
<!-- TAB 1: BASIC INFO -->
|
||||
<div class="tab-pane fade show active" id="edit-tab-basic">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Partner Name *</label>
|
||||
<input type="text" class="form-control" name="partner_name" id="edit_partner_name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Status *</label>
|
||||
<select class="form-select" name="status" id="edit_status" required>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Device</label>
|
||||
<input type="text" class="form-control" name="local_device" id="edit_local_device">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Peer Device</label>
|
||||
<input type="text" class="form-control" name="peer_device" id="edit_peer_device">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-router me-2"></i>Gateway IP Addresses</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Gateway IP *</label>
|
||||
<input type="text" class="form-control font-monospace" name="local_ip" id="edit_local_ip" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Peer Gateway IP *</label>
|
||||
<input type="text" class="form-control font-monospace" name="peer_ip" id="edit_peer_ip" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-diagram-3 me-2"></i>Encryption Domains (Subnets)</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Local Encryption Domain</label>
|
||||
<input type="text" class="form-control font-monospace" name="encryption_domain_local" id="edit_encryption_domain_local">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Remote Encryption Domain</label>
|
||||
<input type="text" class="form-control font-monospace" name="encryption_domain_remote" id="edit_encryption_domain_remote">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-2 border-bottom pb-2">
|
||||
<h6 class="fw-bold text-dark mb-0"><i class="bi bi-key me-2"></i>Authentication</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label text-secondary fw-semibold small">Update Pre-Shared Key (PSK)</label>
|
||||
<input type="password" class="form-control font-monospace" name="preshared_key" placeholder="Leave blank to keep current PSK">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: PHASE 1 (IKE) -->
|
||||
<div class="tab-pane fade" id="edit-tab-phase1">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">IKE Version *</label>
|
||||
<select class="form-select" name="ike_version" id="edit_ike_version">
|
||||
<option value="1">v1</option>
|
||||
<option value="2">v2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Encryption *</label>
|
||||
<select class="form-select" name="phase1_encryption" id="edit_phase1_encryption">
|
||||
<option value="AES-128">AES-128</option>
|
||||
<option value="AES-256">AES-256</option>
|
||||
<option value="3DES">3DES</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Hash *</label>
|
||||
<select class="form-select" name="phase1_hash" id="edit_phase1_hash">
|
||||
<option value="SHA-1">SHA-1</option>
|
||||
<option value="SHA-256">SHA-256</option>
|
||||
<option value="SHA-512">SHA-512</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">DH Group *</label>
|
||||
<select class="form-select" name="phase1_dh_group" id="edit_phase1_dh_group">
|
||||
<option value="2">Group 2</option>
|
||||
<option value="14">Group 14</option>
|
||||
<option value="21">Group 21</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Lifetime (Seconds) *</label>
|
||||
<input type="number" class="form-control" name="phase1_lifetime" id="edit_phase1_lifetime" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: PHASE 2 (IPSEC) -->
|
||||
<div class="tab-pane fade" id="edit-tab-phase2">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Encryption *</label>
|
||||
<select class="form-select" name="phase2_encryption" id="edit_phase2_encryption">
|
||||
<option value="AES-128">AES-128</option>
|
||||
<option value="AES-256">AES-256</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-secondary fw-semibold small">Hash *</label>
|
||||
<select class="form-select" name="phase2_hash" id="edit_phase2_hash">
|
||||
<option value="SHA-1">SHA-1</option>
|
||||
<option value="SHA-256">SHA-256</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">DH Group</label>
|
||||
<select class="form-select" name="phase2_dh_group" id="edit_phase2_dh_group">
|
||||
<option value="">Match Phase 1</option>
|
||||
<option value="14">Group 14</option>
|
||||
<option value="21">Group 21</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary fw-semibold small">Lifetime (Secs) *</label>
|
||||
<input type="number" class="form-control" name="phase2_lifetime" id="edit_phase2_lifetime" required>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="pfs_enabled" value="1" id="edit_pfs_enabled">
|
||||
<label class="form-check-label text-secondary fw-semibold small" for="edit_pfs_enabled">Enable PFS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="btnUpdateVpn" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">
|
||||
Update Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXPORT VPN CONFIG MODAL -->
|
||||
<div class="modal fade" id="exportVpnModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-dark text-white border-bottom-0">
|
||||
<h5 class="modal-title fw-bold"><i class="bi bi-file-earmark-code me-2"></i>Export Partner Config</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('vpns.download') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="partner_name" id="export_partner_name">
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<div class="alert alert-info py-2 d-flex align-items-center" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-info-circle-fill me-2 fs-5"></i>
|
||||
<div>You can edit the configuration parameters below before downloading the file to share with the partner.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control font-monospace bg-light" name="config_content" id="export_config_content" rows="15" required style="font-size: 0.85rem; white-space: pre;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0 d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success btn-sm fw-bold px-4">
|
||||
<i class="bi bi-download me-1"></i> Download .txt File
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="{{ asset('public/assets/js/vpn-index.js') }}"></script>
|
||||
@endpush
|
||||
@@ -55,6 +55,10 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::resource('clients', App\Http\Controllers\ClientsController::class);
|
||||
|
||||
#MNOs mnos.store
|
||||
Route::get('/mnos/json/{id}', [App\Http\Controllers\NetworkOperatorsController::class, 'getJson'])->name('mnos.json');
|
||||
Route::post('/mnos/{operatorId}/documents', [App\Http\Controllers\NetworkOperatorsController::class, 'storeDocument'])->name('mnos.documents.store');
|
||||
Route::delete('/mnos/documents/{id}', [App\Http\Controllers\NetworkOperatorsController::class, 'destroyDocument'])->name('mnos.documents.destroy');
|
||||
|
||||
Route::post('/mnos/{id}/connections', [App\Http\Controllers\NetworkOperatorsController::class, 'storeConnection'])->name('mnos.connections.store');
|
||||
Route::delete('/mnos/connections/{id}', [App\Http\Controllers\NetworkOperatorsController::class, 'destroyConnection'])->name('mnos.connections.destroy');
|
||||
Route::get('/mnos/{id}', [App\Http\Controllers\NetworkOperatorsController::class, 'show'])->name('mnos.show');
|
||||
@@ -134,7 +138,29 @@ Route::middleware(['auth'])->group(function () {
|
||||
// Add a manual/custom holiday
|
||||
Route::post('holidays', [App\Http\Controllers\PublicHolidaysController::class, 'store']);
|
||||
|
||||
|
||||
// Management Dashboard
|
||||
Route::get('/management', [App\Http\Controllers\ManagementDashboardController::class, 'index'])->name('management.index');
|
||||
Route::put('/management/leaves/{id}', [App\Http\Controllers\ManagementDashboardController::class, 'updateLeaveStatus'])->name('management.leaves.update');
|
||||
|
||||
// Delete a holiday
|
||||
Route::delete('holidays/{holiday}', [App\Http\Controllers\PublicHolidaysController::class, 'destroy']);
|
||||
|
||||
Route::resource('servers', App\Http\Controllers\ServerController::class);
|
||||
|
||||
// OTP Security Routes
|
||||
Route::post('/servers/{server}/request-otp', [App\Http\Controllers\ServerCredentialController::class, 'requestOtp'])->name('servers.request-otp');
|
||||
Route::post('/servers/{server}/verify-otp', [App\Http\Controllers\ServerCredentialController::class, 'verifyOtp'])->name('servers.verify-otp');
|
||||
|
||||
|
||||
Route::get('/vpns/standard-template', [App\Http\Controllers\VpnConfigController::class, 'downloadStandardTemplate'])->name('vpns.standard-template');
|
||||
Route::resource('vpns', App\Http\Controllers\VpnConfigController::class);
|
||||
Route::get('/vpns/{vpn}/template', [App\Http\Controllers\VpnConfigController::class, 'generateTemplate'])->name('vpns.template');
|
||||
Route::post('/vpns/download', [App\Http\Controllers\VpnConfigController::class, 'download'])->name('vpns.download');
|
||||
|
||||
|
||||
|
||||
Route::get('/activity-logs', [App\Http\Controllers\ActivityLogController::class, 'index'])->name('activity-logs.index');
|
||||
|
||||
Route::post('/logout', [App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
|
||||
|
||||
|
||||
-- contract_validity is now next_renewal_date _-- next_renewal_date | contract_validity
|
||||
|
||||
-- next_renewal_date column type is supposed to be DATE
|
||||
|
||||
contract_auto_renew should be changed from YES/NO to 0/1
|
||||
|
||||
`name` varchar(200) NOT NULL DEFAULT '',
|
||||
`country` varchar(200) NOT NULL DEFAULT '',
|
||||
`contact_person` varchar(191) DEFAULT NULL,
|
||||
|
||||
Reference in New Issue
Block a user