bug fixes, leave managment refactoring, report points

This commit is contained in:
Kwesi Banson
2026-11-03 12:05:59 +00:00
parent 965892ac62
commit b393dcdf0e
14 changed files with 144 additions and 117 deletions

View File

@@ -1,63 +0,0 @@
<?php
namespace App\Http\Controllers;
use Session;
use App\Models;
use Illuminate\Http\Request;
class CommentsController extends Controller
{
public function index(){
$user_id = \Auth::user()->id;
$result = Models\Comment::with('projectStatus')
->where('assignee_id', $user_id)
->orderBy('project_id', 'DESC')
->get();
$data = [
'page_title' => 'Projects Status List',
'project_statuses' => $result
];
return view('comments.index', $data);
}
public function add_comment($id){
$result = Models\ProjectStatus::with('comments', 'project')->where('id', $id)->firstOrFail();
// dd($result);
$data = [
'page_title' => 'Projects Status Comment',
'project_arr' => $result
];
// dump($data);
return view('comments.create', $data);
}
public function store(Request $request) {
// Save a new post
$this->validate($request, [
'description' => 'required',
'status' => 'required',
'project_id' => 'required',
'assignee_id' => 'required'
]);
$project_status_arr = $request->except('_token');
$result = Models\ProjectStatus::create($project_status_arr);
Session::flash('success_message', 'Project status added successfully!');
return redirect(url('project-status'));
}
public function show($id) {
// Show a specific post
}
public function edit($id) {
// Show form to edit a post
}
public function update(Request $request, $id) {
// Update a specific post
return redirect(url('project-status'));
}
public function destroy($id) {
// Delete a specific post
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\DailyReport;
use App\Models\ReportOverride;
use App\Models\StaffMember;
use App\Http\Requests\StoreDailyReportRequest;
use Illuminate\Support\Facades\Mail;
use App\Mail\DailyReportSubmitted;
@@ -18,6 +19,7 @@ class DailyReportController extends Controller
return view('reports.index', compact('reports'));
}
public function store(StoreDailyReportRequest $request)
{
$report = DailyReport::create([
@@ -26,20 +28,25 @@ class DailyReportController extends Controller
'content' => $request->content,
]);
// If they used an override, optionally delete or mark it as used so it can't be reused
// Reward the staff member with points (e.g., 10 points per report)
$staff = auth()->user();
$staff->increment('report_points', 10);
ReportOverride::where('user_id', auth()->id())
->where('report_date', $request->report_date)
->delete();
$managementEmails = ['kwesi@click-mobile.com'];
// $managementEmails = User::whereIn('group', ['directors'])
// ->pluck('email')
// ->toArray();
/// modify this to make it dynamic
$managementEmails = StaffMember::with('department')->whereIn('department_id', [5])->pluck('email')->toArray();
if (env('APP_ENV') == 'production') {
Mail::to($managementEmails)->send(new DailyReportSubmitted($report, auth()->user()));
}
return response()->json([
'success' => true,
'message' => 'Daily report submitted successfully.'
'message' => 'Daily report submitted successfully! You earned 10 points.',
'new_points_total' => $staff->report_points // Optional: send back to update UI dynamically
]);
}
}

View File

@@ -16,7 +16,7 @@ class LeaveRequestController extends Controller
{
$query = LeaveRequest::with('staff');
// Search by Staff Name
if ($request->filled('search')) {
$search = $request->search;
$query->whereHas('staff', function($q) use ($search) {
@@ -24,18 +24,17 @@ class LeaveRequestController extends Controller
});
}
// Filter by Status
if ($request->filled('status')) {
$query->where('status', $request->status);
} else {
// Default to showing pending requests first if no filter is applied
}
else {
$query->orderByRaw("FIELD(status, 'PENDING', 'APPROVED', 'REJECTED')");
}
$leaveRequests = $query->orderBy('created_at', 'desc')->paginate(15);
$leaveRequests->appends($request->query());
// KPIs for HR
$pendingCount = LeaveRequest::where('status', 'PENDING')->count();
$approvedThisMonth = LeaveRequest::where('status', 'APPROVED')
->whereMonth('start_date', Carbon::now()->month)
@@ -52,11 +51,8 @@ class LeaveRequestController extends Controller
}
public function myLeave()
{
// Find the staff profile of the currently logged-in user
// Adjust this logic if you link Users to StaffMembers differently (e.g., via a staff_id foreign key)
$staff = StaffMember::where('email', auth()->user()->email)->firstOrFail();
// Fetch their leave history, latest first
$staff = StaffMember::where('email', auth()->user()->email)->firstOrFail();
$leaveRequests = LeaveRequest::where('staff_member_id', $staff->id)
->orderBy('created_at', 'desc')
->paginate(10);
@@ -82,7 +78,6 @@ class LeaveRequestController extends Controller
$start = Carbon::parse($validated['start_date']);
$end = Carbon::parse($validated['end_date']);
// Calculate total days excluding weekends
$totalDays = $start->diffInDaysFiltered(function (Carbon $date) {
return !$date->isWeekend();
}, $end) + 1; // +1 to include both the start and end day
@@ -107,7 +102,7 @@ class LeaveRequestController extends Controller
}
public function updateStatus(Request $request, $id)
public function updateStatusOld(Request $request, $id)
{
$leaveRequest = LeaveRequest::findOrFail($id);
@@ -138,6 +133,44 @@ class LeaveRequestController extends Controller
'approved_by' => auth()->id(),
]);
return response()->json(['success' => true, 'message' => 'Leave status updated successfully!']);
}
public function updateStatus(Request $request, $id)
{
$leaveRequest = LeaveRequest::findOrFail($id);
$incomingStatus = strtoupper($request->status);
$request->merge(['status' => $incomingStatus]);
$validated = $request->validate([
'status' => 'required|in:APPROVED,REJECTED',
'admin_remarks' => 'nullable|string',
]);
$currentStatus = strtoupper($leaveRequest->status);
$newStatus = $validated['status'];
$staff = $leaveRequest->staff;
if ($staff && strtolower($leaveRequest->leave_type) === 'annual') { // only annual leave affects leave balance
if ($newStatus === 'APPROVED' && $currentStatus !== 'APPROVED') {
$staff->annual_leave_balance -= $leaveRequest->total_days;
$staff->save();
}
if ($newStatus === 'REJECTED' && $currentStatus === 'APPROVED') {
$staff->annual_leave_balance += $leaveRequest->total_days;
$staff->save();
}
}
$leaveRequest->update([
'status' => $newStatus,
'admin_remarks' => $validated['admin_remarks'] ?? null,
'approved_by' => auth()->id(),
]);
return response()->json(['success' => true, 'message' => 'Leave status updated successfully!']);
}
}

View File

@@ -11,17 +11,17 @@ 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) {
$staffReports = StaffMember::where('department_id', '!=', 5)
->withCount(['dailyReports' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('report_date', [$startDate, $endDate]);
}])
->orderBy('name')
@@ -29,7 +29,6 @@ class ManagementDashboardController extends Controller
return view('management.index', compact('pendingLeaves', 'staffReports', 'startDate', 'endDate'));
}
public function updateLeaveStatus(Request $request, $id)
{
$request->validate(['status' => 'required|in:Approved,Rejected']);

View File

@@ -79,8 +79,9 @@ 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',
'connection_status' => 'required|string',
// 'contract_auto_renew' => 'required|boolean',
// 'next_renewal_date' => 'nullable|required_if:contract_auto_renew,0|date',
]);
$operator->update([
@@ -109,7 +110,7 @@ class NetworkOperatorsController extends Controller
return response()->json(['message' => 'MNO Partner updated successfully!']);
}
// Fetch single record JSON for editing modal
public function getJson($id)
{
$operator = NetworkOperator::findOrFail($id);
@@ -118,8 +119,6 @@ class NetworkOperatorsController extends Controller
public function show($id)
{
$operator = NetworkOperator::findOrFail($id);
// Fetch all services for the dropdown
$allServices = \App\Models\Service::orderBy('name')->get();
$staffMembers = \App\Models\StaffMember::orderBy('name')->get();
@@ -134,13 +133,9 @@ class NetworkOperatorsController extends Controller
'required',
'string',
function ($attribute, $value, $fail) {
// 1. Check if valid IP Address (IPv4 or IPv6)
$isIp = filter_var($value, FILTER_VALIDATE_IP);
// 2. Check if valid Domain Name (e.g., smpp.operator.com)
$isDomain = preg_match('/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i', $value);
// 3. Check if valid CIDR Subnet (e.g., 196.200.15.0/24)
$isSubnet = preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/', $value);
if (!$isIp && !$isDomain && !$isSubnet) {
@@ -192,7 +187,7 @@ class NetworkOperatorsController extends Controller
\App\Models\MnoNote::create([
'mno_id' => $operatorId,
'services' => $request->services ?? '',
'user_id' => auth()->id(), // Automatically assign the logged-in user
'user_id' => auth()->id(),
'notes_body' => $request->notes_body,
]);

View File

@@ -0,0 +1,22 @@
<?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('staff_members', function (Blueprint $table) {
$table->integer('report_points')->default(0)->after('status');
});
}
public function down()
{
Schema::table('staff_members', function (Blueprint $table) {
$table->dropColumn('report_points');
});
}
};

6
info.md Normal file
View File

@@ -0,0 +1,6 @@
Each team Member should have a profile
Names
Job Description
Each day they should have a template with fixed date to submit report, after 48 hours the write pad locks and can only be opened with my approval.
Each submission carries points which will be reflected on a employee graph - just like the tracker I share.
When the report is submitted, an e-mail sent to me where I can reply from.

View File

@@ -20,9 +20,7 @@
<i class="bi bi-border-center me-2"></i> Management Hub
</a>
@endif
<a href="{{ url('management') }}" class="sidebar-nav-item {{ request()->is('management') ? 'active' : '' }}">
<i class="bi bi-border-center me-2"></i> Management Hub
</a>
<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

View File

@@ -137,13 +137,11 @@
@push('scripts')
<script>
$(document).ready(function() {
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Handle Approve / Reject actions
$('.btn-action-leave').on('click', function() {
const requestId = $(this).data('id');
const action = $(this).data('action');

View File

@@ -40,7 +40,7 @@
<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>Pending Requests:</strong> Can be canceled before HR approval. Once approved, contact HR to amend.</li>
<li><strong>Pending Requests:</strong> Can be canceled before approval.</li>
<li><strong>Policy 2</strong></li>
<li><strong>Policy 3</strong></li>
<li><strong>Policy 4</strong></li>

View File

@@ -99,6 +99,7 @@
<th class="ps-3">Staff Member</th>
<th>Designation</th>
<th>Reports Submitted</th>
<th>Report Points</th>
<th class="text-end pe-3">Performance (Approx)</th>
</tr>
</thead>
@@ -124,6 +125,11 @@
<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>
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning-subtle">
<i class="bi bi-star-fill text-warning me-1"></i>{{ number_format($staff->report_points ?? 0) }}
</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>

View File

@@ -117,7 +117,7 @@
</div>
</div>
<h6 class="fw-bold mb-3 text-secondary text-uppercase" style="font-size: 0.75rem;">Multi-Entry & Services Configuration</h6>
<h6 class="fw-bold mb-3 text-secondary text-uppercase" style="font-size: 0.75rem;">Services | Support & Finance Emails</h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Services</label>

View File

@@ -49,9 +49,14 @@
<h5 class="fw-bold mb-1 text-dark">{{ $staff->name }}</h5>
<p class="text-secondary small mb-3">{{ $staff->designation ?? 'Staff Member' }}</p>
<span class="badge {{ $staff->status == 'ACTIVE' ? 'bg-success' : 'bg-warning' }} bg-opacity-10 text-{{ $staff->status == 'ACTIVE' ? 'success' : 'warning' }} px-3 py-2 rounded-pill mb-4">
<div class="d-flex justify-content-center gap-2 mb-4">
<span class="badge {{ $staff->status == 'ACTIVE' ? 'bg-success' : 'bg-warning' }} bg-opacity-10 text-{{ $staff->status == 'ACTIVE' ? 'success' : 'warning' }} px-3 py-2 rounded-pill">
{{ $staff->status }}
</span>
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning-subtle px-3 py-2 rounded-pill">
<i class="bi bi-star-fill text-warning me-1"></i> {{ number_format($staff->report_points ?? 0) }} Points
</span>
</div>
<div class="border-top pt-4 text-start">
<h6 class="fw-bold text-secondary mb-3 text-uppercase" style="font-size: 0.75rem; letter-spacing: 1px;">Work Information</h6>
@@ -79,7 +84,13 @@
<div class="fw-medium text-dark small">{{ $staff->hire_date ? date('M d, Y', strtotime($staff->hire_date)) : 'N/A' }}</div>
</div>
</div>
<div class="d-flex align-items-center mb-4">
<div class="bg-warning bg-opacity-10 p-2 rounded me-3 text-warning"><i class="bi bi-star-fill"></i></div>
<div>
<div class="small text-secondary fw-semibold" style="font-size: 0.7rem;">REPORT POINTS</div>
<div class="fw-medium text-dark small"><strong class="text-dark">{{ number_format($staff->report_points ?? 0) }}</strong> Points</div>
</div>
</div>
<a href="{{ route('leave.my_leave') }}" class="btn w-100 fw-bold border text-primary d-flex justify-content-between align-items-center" style="background-color: #f8f9fc;">
<span><i class="bi bi-airplane me-2"></i> Leave Management</span>
<span class="badge bg-primary rounded-pill">{{ $staff->annual_leave_balance }} Days</span>

View File

@@ -129,8 +129,11 @@
<span class="text-secondary text-truncate" style="max-width: 180px;"><i class="bi bi-envelope me-2"></i>{{ $staff->email }}</span>
<a href="mailto:{{ $staff->email }}" class="text-secondary"><i class="bi bi-box-arrow-up-right" style="font-size: 0.7rem;"></i></a>
</div>
<div class="contact-row d-flex align-items-center">
<span class="text-secondary"><i class="bi bi-geo-alt me-2"></i>{{ $staff->location_country ?? 'Not Specified' }}</span>
<div class="contact-row d-flex justify-content-between align-items-center">
<span class="text-secondary text-truncate" style="max-width: 140px;"><i class="bi bi-geo-alt me-2"></i>{{ $staff->location_country ?? 'Not Specified' }}</span>
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning-subtle" title="Accumulated Report Points" style="font-size: 0.65rem;">
<i class="bi bi-star-fill text-warning me-1"></i>{{ number_format($staff->report_points ?? 0) }}
</span>
</div>
</div>
@@ -330,8 +333,16 @@
<div class="fw-medium text-dark small"><span id="viewStaffLeaveBalance">0</span> Days</div>
</div>
<div class="col-md-4">
<div class="text-secondary small fw-semibold">ANNUAL LEAVE BALANCE</div>
<div class="fw-medium text-dark small"><span id="viewStaffLeaveBalance">0</span> Days</div>
<div class="text-secondary small fw-semibold">REWARD POINTS</div>
<div class="fw-medium text-dark small">
<i class="bi bi-star-fill text-warning me-1"></i><span id="viewStaffPoints">0</span>
</div>
</div>
<div class="col-md-4">
<div class="text-secondary small fw-semibold">Date of Birth</div>
<div class="fw-medium text-dark small">
<i class="bi bi-person-bounding-box text-danger me-1"></i><span id="viewStaffDoB">N/A</span>
</div>
</div>
<!-- NEW ROLES SECTION -->
@@ -589,6 +600,8 @@
$('#viewStaffPersonalEmail').text('');
$('#viewStaffLocation').text('');
$('#viewStaffLeaveBalance').text('');
$('#viewStaffPoints').text('...');
$('#viewStaffDoB').text('...');
$('#viewStaffDependentsList').html('<tr><td colspan="4" class="text-center text-secondary py-3">Loading...</td></tr>');
$('#viewStaffRoles').html('<span class="text-muted small">Loading...</span>');
viewModal.show();
@@ -609,6 +622,8 @@
$('#viewStaffPersonalEmail').text(staff.personal_email || 'N/A');
$('#viewStaffLocation').text(staff.location_country || 'N/A');
$('#viewStaffLeaveBalance').text(staff.annual_leave_balance ?? 0);
$('#viewStaffPoints').text(staff.report_points ?? 0);
$('#viewStaffDoB').text(staff.dob ?? 'N/A');
// Status Badge
let badgeClass = staff.status === 'ACTIVE' ? 'bg-success bg-opacity-10 text-success' : 'bg-warning bg-opacity-10 text-warning';