File "StaffAttendanceController.php"
Full Path: /home/trinadezambia/public_html/admin_panel/app/Http/Controllers/StaffAttendanceController.php
File size: 39.74 KB
MIME-type: text/x-php
Charset: utf-8
<?php
namespace App\Http\Controllers;
use App\Models\SessionYear;
use App\Models\Expense;
use App\Models\Leave;
use App\Repositories\StaffAttendance\StaffAttendanceInterface;
use App\Repositories\Staff\StaffInterface;
use App\Repositories\LeaveMaster\LeaveMasterInterface;
use App\Repositories\Leave\LeaveInterface;
use App\Repositories\LeaveDetail\LeaveDetailInterface;
use App\Services\BootstrapTableService;
use App\Services\CachingService;
use App\Services\ResponseService;
use App\Models\Holiday;
use App\Models\Staff;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Throwable;
use function PHPUnit\Framework\isEmpty;
class StaffAttendanceController extends Controller
{
private StaffAttendanceInterface $staffAttendance;
private StaffInterface $staff;
private CachingService $cache;
private LeaveMasterInterface $leaveMaster;
private LeaveInterface $leave;
private LeaveDetailInterface $leaveDetail;
public function __construct(StaffAttendanceInterface $staffAttendance, StaffInterface $staff, CachingService $cachingService, LeaveMasterInterface $leaveMaster, LeaveInterface $leave, LeaveDetailInterface $leaveDetail)
{
$this->staffAttendance = $staffAttendance;
$this->staff = $staff;
$this->cache = $cachingService;
$this->leaveMaster = $leaveMaster;
$this->leave = $leave;
$this->leaveDetail = $leaveDetail;
}
public function index()
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-list']);
$sessionYear = $this->cache->getSessionYear();
$sessionYearStart = $sessionYear->original_start_date;
$sessionYearEnd = $sessionYear->original_end_date;
return view('staff-attendance.index', compact('sessionYear', 'sessionYearStart', 'sessionYearEnd'));
}
public function view()
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-list']);
$sessionYear = $this->cache->getSessionYear();
$sessionYearStart = $sessionYear->original_start_date;
$sessionYearEnd = $sessionYear->original_end_date;
return view('staff-attendance.view', compact('sessionYear', 'sessionYearStart', 'sessionYearEnd'));
}
public function getAttendanceData(Request $request)
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-list']);
$sessionYear = $this->cache->getSessionYear();
if ($request->mode == 'monthly') {
$startDate = Carbon::createFromDate($request->year, $request->month, 1)->startOfMonth();
$endDate = Carbon::createFromDate($request->year, $request->month, 1)->endOfMonth();
$attendance = $this->staffAttendance->builder()
->with(['user:id,first_name,last_name,email,image'])
->where('session_year_id', $sessionYear->id)
->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->when($request->search, function ($query) use ($request) {
$query->whereHas('user', function ($q) use ($request) {
$q->where('first_name', 'like', '%' . $request->search . '%')
->orWhere('last_name', 'like', '%' . $request->search . '%')
->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE '%" . $request->search . "%'");
});
})
->get(['id', 'staff_id', 'date', 'type']);
$holidayAttendance = Holiday::where('date', '>=', $startDate->format('Y-m-d'))
->where('date', '<=', $endDate->format('Y-m-d'))
->get()
->map(function ($holiday) {
$rawDate = Carbon::parse($holiday->getRawOriginal('date'));
return (object) [
'date' => $rawDate->format('d-m-Y'),
'dmyFormat' => $rawDate->format('d-m-Y'),
'title' => $holiday->title
];
});
$leaves = $this->leave->builder()
->where('status', 1)
->with([
'leave_detail' => function ($q) use ($startDate, $endDate) {
$q->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]);
}
])
->get();
$weeklyOffDays = $this->leaveMaster->builder()
->where('session_year_id', $sessionYear->id)
->pluck('holiday')
->filter()
->flatMap(function ($day) {
return collect(explode(',', $day))
->map(fn($d) => ucfirst(strtolower(trim($d))));
})
->unique()
->values();
// 4️⃣ Generate all weekly-off dates in the selected month
$weeklyOffDates = collect();
$current = $startDate->copy();
$end = $endDate->copy();
while ($current->lte($end)) {
if ($weeklyOffDays->contains($current->format('l'))) {
$weeklyOffDates->push($current->format('d-m-Y'));
}
$current->addDay();
}
// 5️⃣ Add weekly-off dates to holiday list (no duplicates)
foreach ($weeklyOffDates as $woDate) {
if (!$holidayAttendance->contains(fn($h) => data_get($h, 'date') === $woDate)) {
$holidayAttendance->push((object) [
'dmyFormat' => $woDate,
'title' => Carbon::parse($woDate)->format('l') . ' (Weekly Off)'
]);
}
}
$staff = Staff::with([
'user:id,first_name,last_name,email,image'
])
->whereHas('user', function ($q) {
$q->where('status', 1)->whereNull('deleted_at');
})
->when($request->search, function ($query) use ($request) {
$query->whereHas('user', function ($q) use ($request) {
$q->where('first_name', 'like', '%' . $request->search . '%')
->orWhere('last_name', 'like', '%' . $request->search . '%')
->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", ["%{$request->search}%"]);
});
})
->get(['id', 'user_id']);
$responseData = [
'success' => true,
'staff' => $staff,
'attendance' => $attendance,
'holiday' => $holidayAttendance,
'leaves' => $leaves,
];
return response()->json($responseData);
} else {
$response = $this->staffAttendance->builder()->select('type')->where(['date' => date('Y-m-d', strtotime($request->date)), 'session_year_id' => $sessionYear->id])->pluck('type')->first();
return response()->json($response);
}
}
public function store(Request $request)
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-edit']);
$request->validate(['date' => 'required|date|before_or_equal:today']);
try {
DB::beginTransaction();
$sessionYear = $this->cache->getSessionYear();
$dateYmd = date('Y-m-d', strtotime($request->date));
$attendanceMonth = Carbon::parse($dateYmd)->month;
$attendanceYear = Carbon::parse($dateYmd)->year;
$holiday = Holiday::where('date', $dateYmd)->first();
if ($holiday) {
DB::rollBack();
return ResponseService::errorResponse(
"The selected date ($dateYmd) is marked as holiday ({$holiday->title}). Attendance cannot be modified."
);
}
$leaveMasterRecord = $this->leaveMaster->builder()->where('session_year_id', $sessionYear->id)->first();
$hasAbsentOrHalfDay = collect($request->attendance_data)->contains(function ($row) {
return in_array((int) ($row['type'] ?? 1), [0, 4, 5]);
});
if ($hasAbsentOrHalfDay && !$leaveMasterRecord) {
DB::rollBack();
return ResponseService::errorResponse("Leave settings are not configured for this session year.");
}
if ($leaveMasterRecord && $leaveMasterRecord->holiday) {
$holidayDays = explode(',', $leaveMasterRecord->holiday);
$dayName = Carbon::parse($dateYmd)->format('l');
if (in_array($dayName, $holidayDays)) {
DB::rollBack();
return ResponseService::errorResponse(
"Attendance cannot be modified on $dayName."
);
}
}
$attendanceRows = [];
$absentUsers = [];
/**
* SAFE HELPERS (NO refresh(), NO broken relations)
*/
// Create a new leave_detail
$mkDetail = function (int $leaveId, string $type) use ($dateYmd) {
return $this->leaveDetail->create([
'leave_id' => $leaveId,
'date' => $dateYmd,
'type' => $type,
'school_id' => Auth::user()->school_id,
]);
};
$reason = $request->attendance_data[0]['reason'] ?? 'System: Attendance';
$leaveMasterId = $leaveMasterRecord->id ?? null;
// Create complete leave + detail
$mkLeaveWithDetail = function (int $userId, string $type) use ($dateYmd, $mkDetail, $reason, $leaveMasterId) {
$leave = $this->leave->create([
'user_id' => $userId,
'reason' => $reason,
'from_date' => $dateYmd,
'to_date' => $dateYmd,
'leave_master_id' => $leaveMasterId,
'status' => 1,
'school_id' => Auth::user()->school_id,
]);
$detail = $mkDetail($leave->id, $type);
return [$leave, $detail];
};
// SAFE delete detail + parent if needed (NO relation calls)
$deleteDetailAndCascade = function ($detail) {
if (!$detail)
return;
$leaveId = $detail->leave_id; // capture ID safely
$detail->delete();
$leave = Leave::find($leaveId);
if ($leave && $leave->leave_detail()->count() == 0) {
$leave->delete();
}
};
// Helper: find first detail of given type
$firstByType = function ($details, string $type) {
return $details->firstWhere('detail.type', $type);
};
$isBulk = count($request->attendance_data) > 1;
$skippedStaffCount = 0;
$singleSkipped = false;
foreach ($request->attendance_data as $row) {
$staffId = (int) $row['staff_id'];
$attendanceType = (int) ($row['type'] ?? 1);
$reason = $row['reason'] ?? null;
$staffIds = $this->staff->builder()->where('user_id', $staffId)->first();
$payrollExists = Expense::where('staff_id', $staffIds->id)
->where('month', $attendanceMonth)
->where('year', $attendanceYear)
->exists();
if ($payrollExists) {
if ($isBulk) {
$skippedStaffCount++;
} else {
$singleSkipped = true;
}
continue; // skip processing this staff
}
// ✅ HOLIDAY (type=3, do NOT touch leaves)
if ($attendanceType === 3) {
$attendanceRows[] = [
'id' => $row['id'] ?? null,
'staff_id' => $staffId,
'session_year_id' => $sessionYear->id,
'type' => 3,
'date' => $dateYmd,
'reason' => null,
'leave_id' => null,
'leave_detail_id' => null,
];
continue;
}
// ✅ Load all leaves for the date
$leaves = $this->leave->builder()
->where('user_id', $staffId)
->where('from_date', '<=', $dateYmd)
->where('to_date', '>=', $dateYmd)
->where('status', 1)
->with(['leave_detail' => fn($q) => $q->where('date', $dateYmd)])
->get();
// ✅ Existing attendance record (if any)
$attendance = $this->staffAttendance->builder()
->where('staff_id', $staffId)
->where('date', $dateYmd)
->first();
// ✅ Split leave details into admin vs attendance-created
$adminDetails = collect();
$attnDetails = collect();
foreach ($leaves as $leave) {
foreach ($leave->leave_detail as $d) {
if ($attendance && $attendance->leave_detail_id == $d->id) {
$attnDetails->push(['leave' => $leave, 'detail' => $d]);
} else {
$adminDetails->push(['leave' => $leave, 'detail' => $d]);
}
}
}
$has = function ($set, $type) {
return $set->firstWhere('detail.type', $type) !== null;
};
// Flags
$adminHasFull = $has($adminDetails, 'Full');
$adminHasFirst = $has($adminDetails, 'First Half');
$adminHasSecond = $has($adminDetails, 'Second Half');
$attnHasFull = $has($attnDetails, 'Full');
$attnHasFirst = $has($attnDetails, 'First Half');
$attnHasSecond = $has($attnDetails, 'Second Half');
$leaveIdForAttendance = null;
$leaveDetailIdForAttn = null;
/**
* ✅ APPLY ATTENDANCE LOGIC
*/
switch ($attendanceType) {
// ✅ FULL PRESENT → delete ALL attendance-created details
case 1:
foreach (['Full', 'First Half', 'Second Half'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
break;
// ✅ FULL ABSENT
case 0:
$absentUsers[] = $staffId;
// CASE A: Admin already full → attendance creates nothing
if ($adminHasFull || ($adminHasFirst && $adminHasSecond)) {
foreach (['Full', 'First Half', 'Second Half'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
}
// CASE B: Admin First only → attendance creates Second
elseif ($adminHasFirst && !$adminHasSecond) {
foreach (['Full', 'First Half'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
$node = $firstByType($attnDetails, 'Second Half');
if ($node) {
$leaveIdForAttendance = $node['leave']->id;
$leaveDetailIdForAttn = $node['detail']->id;
} else {
[$leave, $detail] = $mkLeaveWithDetail($staffId, 'Second Half');
$leaveIdForAttendance = $leave->id;
$leaveDetailIdForAttn = $detail->id;
}
}
// CASE C: Admin Second only → attendance creates First
elseif ($adminHasSecond && !$adminHasFirst) {
foreach (['Full', 'Second Half'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
$node = $firstByType($attnDetails, 'First Half');
if ($node) {
$leaveIdForAttendance = $node['leave']->id;
$leaveDetailIdForAttn = $node['detail']->id;
} else {
[$leave, $detail] = $mkLeaveWithDetail($staffId, 'First Half');
$leaveIdForAttendance = $leave->id;
$leaveDetailIdForAttn = $detail->id;
}
}
// CASE D: No admin leaves → attendance creates full
else {
// convert half to full
if ($attnHasFirst || $attnHasSecond) {
$node = $attnHasFirst ? $firstByType($attnDetails, 'First Half')
: $firstByType($attnDetails, 'Second Half');
$oldLeaveId = $node['leave']->id;
$deleteDetailAndCascade($node['detail']);
$leave = Leave::find($oldLeaveId);
if (!$leave) { // deleted by cascade
[$leave, $detail] = $mkLeaveWithDetail($staffId, 'Full');
} else {
$detail = $mkDetail($leave->id, 'Full');
}
$leaveIdForAttendance = $leave->id;
$leaveDetailIdForAttn = $detail->id;
}
// no attendance leave → create new full
elseif (!$attnHasFull) {
[$leave, $detail] = $mkLeaveWithDetail($staffId, 'Full');
$leaveIdForAttendance = $leave->id;
$leaveDetailIdForAttn = $detail->id;
} elseif ($attnHasFull) {
$node = $firstByType($attnDetails, 'Full');
$leaveIdForAttendance = $node['leave']->id;
$leaveDetailIdForAttn = $node['detail']->id;
}
}
break;
// ✅ FIRST HALF PRESENT → attendance creates Second Half
case 4:
// admin already second → enforce admin
if ($adminHasSecond) {
foreach (['First Half', 'Full'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
} else {
// remove conflicts
foreach (['First Half', 'Full'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
$node = $firstByType($attnDetails, 'Second Half');
if ($node) {
$leaveIdForAttendance = $node['leave']->id;
$leaveDetailIdForAttn = $node['detail']->id;
} else {
[$leave, $detail] = $mkLeaveWithDetail($staffId, 'Second Half');
$leaveIdForAttendance = $leave->id;
$leaveDetailIdForAttn = $detail->id;
}
}
break;
// ✅ SECOND HALF PRESENT → attendance creates First Half
case 5:
if ($adminHasFirst) {
foreach (['Second Half', 'Full'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
} else {
foreach (['Second Half', 'Full'] as $t) {
$node = $firstByType($attnDetails, $t);
if ($node)
$deleteDetailAndCascade($node['detail']);
}
$node = $firstByType($attnDetails, 'First Half');
if ($node) {
$leaveIdForAttendance = $node['leave']->id;
$leaveDetailIdForAttn = $node['detail']->id;
} else {
[$leave, $detail] = $mkLeaveWithDetail($staffId, 'First Half');
$leaveIdForAttendance = $leave->id;
$leaveDetailIdForAttn = $detail->id;
}
}
break;
}
// If attendance reason exists AND leave is linked → update the leave reason
if (!empty($reason) && $leaveIdForAttendance) {
// Always update the parent leave reason
Leave::where('id', $leaveIdForAttendance)
->update(['reason' => $reason]);
}
// ✅ BUILD ATTENDANCE ROW
$attendanceRows[] = [
'id' => $row['id'] ?? null,
'staff_id' => $staffId,
'session_year_id' => $sessionYear->id,
'type' => $attendanceType,
'date' => $dateYmd,
'reason' => $reason,
'leave_id' => $leaveIdForAttendance,
'leave_detail_id' => $leaveDetailIdForAttn,
];
}
// ✅ Upsert
$this->staffAttendance->upsert(
$attendanceRows,
['id'],
['staff_id', 'session_year_id', 'type', 'date', 'reason', 'leave_id', 'leave_detail_id']
);
DB::commit();
if ($request->absent_notification && !empty($absentUsers)) {
$d = Carbon::parse($dateYmd)->format('F jS, Y');
send_notification($absentUsers, 'Absent', "You are marked absent on $d", 'attendance');
}
if ($isBulk) {
if ($skippedStaffCount > 0) {
ResponseService::successResponse(
"Data Stored Successfully — {$skippedStaffCount} staff skipped due to payroll lock"
);
} else {
ResponseService::successResponse("Data Stored Successfully");
}
} else { // single staff mode
if ($singleSkipped) {
ResponseService::errorResponse(
"Attendance skipped — payroll for this month is already generated"
);
} else {
ResponseService::successResponse("Attendance Stored Successfully");
}
}
} catch (Throwable $e) {
DB::rollBack();
ResponseService::logErrorResponse($e, "Staff Attendance Controller -> Store");
ResponseService::errorResponse();
}
}
public function show(Request $request)
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-list']);
$date = date('Y-m-d', strtotime($request->date));
$sort = $request->input('sort', 'id');
$order = $request->input('order', 'ASC');
$search = $request->input('search');
/* -------------------------
SESSION + MASTER LEAVE
--------------------------*/
$sessionYear = $this->cache->getSessionYear();
$leaveMaster = $this->leaveMaster->builder()
->where('session_year_id', $sessionYear->id)
->first();
$holiday_days = $leaveMaster->holiday ?? null;
/* -------------------------
HOLIDAYS
--------------------------*/
$holidays = Holiday::where('date', $date)->first();
if ($holidays != null) {
$holidays = true;
}
/* -------------------------
FETCH ATTENDANCE
--------------------------*/
$attendanceRecords = $this->staffAttendance->builder()
->with('user.staff')
->where('date', $date)
->whereHas('user', fn($q) => $q->where('status', 1)->whereNull('deleted_at'))
->orderBy($sort, $order)
->get()
->keyBy('staff_id');
/* -------------------------
FETCH STAFF
--------------------------*/
$staffQuery = $this->staff->builder()->with([
'user',
'leave' => fn($q) => $q->with([
'leave_detail' => fn($d) => $d->where('date', $date)
])
->where('from_date', '<=', $date)
->where('to_date', '>=', $date)
->where('status', 1)
])->whereHas('user', function ($q) {
$q->where('status', 1)->whereNull('deleted_at');
});
if ($search) {
$staffQuery->where(function ($q) use ($search) {
$q->where('user_id', 'like', "%{$search}%")
->orWhereHas('user', function ($u) use ($search) {
$u->where('first_name', 'like', "%{$search}%")
->orWhere('last_name', 'like', "%{$search}%")
->orWhereRaw("CONCAT(first_name,' ',last_name) LIKE ?", ["%{$search}%"]);
});
});
}
$staffMembers = $staffQuery->get();
$rows = [];
$no = 1;
foreach ($staffMembers as $staff) {
$attendance = $attendanceRecords->get($staff->user_id);
$user = $staff->user;
$userId = $user->id ?? null;
$staffId = $staff->id ?? null;
$attendanceMonth = Carbon::parse($date)->format('m');
$attendanceYear = Carbon::parse($date)->format('Y');
$payrollExists = Expense::where('staff_id', $staffId)
->where('month', $attendanceMonth)
->where('year', $attendanceYear)
->exists();
/* ------------------------------------------------
CLASSIFY LEAVE DETAILS (Reducing Duplicate Loop)
-------------------------------------------------*/
$adminHalves = [];
$attnHalves = [];
foreach ($staff->leave as $leave) {
foreach ($leave->leave_detail as $detail) {
$isAttendanceCreated = $attendance && $attendance->leave_detail_id == $detail->id;
if ($isAttendanceCreated) {
$attnHalves[] = $detail->type;
} else {
$adminHalves[] = $detail->type;
}
}
}
$adminHasFull = in_array('Full', $adminHalves);
$adminFirst = in_array('First Half', $adminHalves);
$adminSecond = in_array('Second Half', $adminHalves);
$attnHasFull = in_array('Full', $attnHalves);
$attnFirst = in_array('First Half', $attnHalves);
$attnSecond = in_array('Second Half', $attnHalves);
/* -------------------------
CALCULATE LEAVE PRIORITY
--------------------------*/
$leaveType =
$adminHasFull ? 'Full' : ($attnHasFull ? 'Full' : ($adminFirst ? 'First Half' : ($adminSecond ? 'Second Half' : ($attnFirst ? 'First Half' : ($attnSecond ? 'Second Half' : null)))));
$isAdminLeave = !empty($adminHalves);
$isAttendanceLeave = !empty($attnHalves);
/* -------------------------
ALREADY MARKED ATTENDANCE
--------------------------*/
if ($attendance) {
$rows[] = [
'id' => $attendance->id,
'no' => $no++,
'staff_id' => $attendance->staff_id,
'status' => $attendance->type,
'user' => [
'full_name' => $user,
'staff' => [
'id' => $attendance->user->staff->id ?? '',
'user_id' => $attendance->user->staff->user_id ?? '',
],
],
'type' => [
'id' => $userId,
'name' => $user,
'date' => Carbon::parse($date)->format('l, F j, Y'),
'status' => 'Update',
'type' => $attendance->type,
'reason' => $attendance->reason,
],
'leave_type' => $leaveType,
'admin_leave' => $isAdminLeave,
'attendance_leave' => $isAttendanceLeave,
'holiday_days' => $holiday_days,
'holiday' => $holidays ?? false,
'day_name' => Carbon::parse($date)->format('l'),
'date' => $date,
'payroll_exists' => $payrollExists ?? false,
];
continue;
}
/* -------------------------
NOT MARKED ATTENDANCE
--------------------------*/
$rows[] = [
'id' => '',
'no' => $no++,
'staff_id' => $staff->user_id,
'status' => $leaveType === 'Full' ? 'Full Day Leave' : 'not marked',
'user' => [
'full_name' => $user,
'staff' => [
'id' => $staff->id,
'user_id' => $staff->user_id ?? '',
],
],
'type' => [
'id' => $userId,
'name' => $user,
'date' => Carbon::parse($date)->format('l, F j, Y'),
'status' => 'Mark',
],
'leave_type' => $leaveType,
'admin_leave' => $isAdminLeave,
'attendance_leave' => $isAttendanceLeave,
'holiday' => $holidays ?? false,
'holiday_days' => $holiday_days,
'day_name' => Carbon::parse($date)->format('l'),
'date' => $date,
'payroll_exists' => $payrollExists ?? false,
];
}
return response()->json([
'total' => count($rows),
'rows' => $rows,
]);
}
public function attendance_show(Request $request)
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-list']);
$offset = request('offset', 0);
$limit = request('limit');
$sort = request('sort', 'staff_id');
$order = request('order', 'ASC');
$search = request('search');
$attendanceType = request('attendance_type');
$date = date('Y-m-d', strtotime(request('date')));
$validator = Validator::make($request->all(), ['date' => 'required']);
if ($validator->fails()) {
ResponseService::errorResponse($validator->errors()->first());
}
$sessionYear = $this->cache->getSessionYear();
$sql = $this->staffAttendance->builder()->where(['date' => $date, 'session_year_id' => $sessionYear->id])->with('user.staff');
if ($attendanceType != null) {
$sql = $sql->where('type', $attendanceType);
}
if ($search) {
$sql = $sql->whereHas('user', function ($q) use ($search) {
$q->where('first_name', 'like', '%' . $search . '%')
->orWhere('last_name', 'like', '%' . $search . '%')
->orwhereRaw("concat(users.first_name,' ',users.last_name) LIKE '%" . $search . "%'")
->orwhere('id', 'like', '%' . $search . '%');
});
}
$total = $sql->count();
$sql = $sql->orderBy($sort, $order);
if ($limit) {
if ($offset >= $total && $total > 0) {
$lastPage = floor(($total - 1) / $limit) * $limit; // calculate last page offset
$offset = $lastPage;
}
$sql = $sql->skip($offset)->take($limit);
}
$attendanceData = $sql->get();
$no = 1;
foreach ($attendanceData as $attendance) {
$attendance->no = $no++;
}
$data = [
'total' => $total,
'rows' => $attendanceData
];
return response()->json($data);
}
public function monthWiseIndex()
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
ResponseService::noAnyPermissionThenRedirect(['staff-attendance-list']);
$sessionYear = $this->cache->getSessionYear();
return view('staff-attendance.month-wise', compact('sessionYear'));
}
public function monthWiseShow(Request $request, $user_id = null)
{
$limit = request('limit');
$offset = request('offset', 0);
$schoolSettings = $this->cache->getSchoolSettings();
$month = null;
$year = null;
if (str_contains($request->month, '_')) {
[$month, $year] = explode('_', $request->month);
} else {
$month = $request->month;
$year = $request->year ?? date('Y');
}
$sessionYear = $this->cache->getSessionYear();
$sessionYearId = $request->session_year_id ?? $sessionYear->id;
$sql = $this->staff->builder()
->with('user')
->whereHas('staffAttendance', function ($q) use ($month, $year, $sessionYearId) {
$q->whereMonth('date', $month)
->whereYear('date', $year)
->where('session_year_id', $sessionYearId);
})
->orderBy('user_id', 'ASC');
if ($user_id) {
$sql->where('user_id', $user_id);
}
if ($request->search) {
$search = $request->search;
$sql->whereHas('user', function ($q) use ($search) {
$q->where('first_name', 'like', "%$search%")
->orWhere('last_name', 'like', "%$search%")
->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE '%$search%'");
});
}
$total = $sql->count();
if ($limit) {
if ($offset >= $total && $total > 0) {
$offset = floor(($total - 1) / $limit) * $limit;
}
$sql->skip($offset)->take($limit);
}
$res = $sql->get();
/* ================= PRELOAD DATA ================= */
$date = Carbon::create($year, $month, 1);
// Month holidays (fixed dates)
$holidayDates = Holiday::whereMonth('date', $month)
->whereYear('date', $year)
->get()
->map(function ($holiday) {
// Get raw date value from database (Y-m-d format)
return Carbon::parse($holiday->getRawOriginal('date'))->format('Y-m-d');
})
->toArray();
// Weekly holidays
$leaveMasterHoliday = $this->leaveMaster->builder()
->where('session_year_id', $sessionYearId)
->value('holiday');
$holidayDays = $leaveMasterHoliday ? explode(',', $leaveMasterHoliday) : [];
// Leaves
$leavesQuery = $this->leave->builder()
->where('status', 1)
->with([
'leave_detail' => function ($q) use ($month, $year) {
$q->whereMonth('date', $month)->whereYear('date', $year);
}
]);
if ($user_id) {
$leavesQuery->where('user_id', $user_id);
}
$leaves = $leavesQuery->get();
/* ===== BUILD LEAVE MAP (user_id + date) ===== */
$leaveMap = [];
foreach ($leaves as $leave) {
foreach ($leave->leave_detail as $detail) {
$leaveMap[$leave->user_id][$detail->date] = 'leave'; // 2 = Leave
}
}
/* ================= BUILD ROWS ================= */
$rows = [];
foreach ($res as $row) {
// Attendance indexed by date
$attendanceByDate = $row->staffAttendance()
->whereMonth('date', $month)
->whereYear('date', $year)
->where('session_year_id', $sessionYearId)
->pluck('type', 'date');
$staffAttendance = [
'full_name' => $row->user->full_name,
'user_id' => $row->user_id,
];
for ($day = 1; $day <= $date->daysInMonth; $day++) {
$currentDate = $date->copy()->day($day)->format('Y-m-d');
$dayName = Carbon::parse($currentDate)->format('l');
// Attendance value (0 or 1 are VALID)
$attendanceValue = $attendanceByDate[$currentDate] ?? null;
if (in_array($currentDate, $holidayDates)) {
$staffAttendance["day_$day"] = 3;
} elseif (in_array($dayName, $holidayDays)) {
$staffAttendance["day_$day"] = 3;
} else if ($attendanceValue !== null) {
$staffAttendance["day_$day"] = $attendanceValue;
} elseif (isset($leaveMap[$row->user_id][$currentDate])) {
$staffAttendance["day_$day"] = 'leave';
} else {
$staffAttendance["day_$day"] = null;
}
}
$rows[] = $staffAttendance;
}
return response()->json([
'total' => $total,
'rows' => $rows,
'leaves' => $leaves,
]);
}
public function yourIndex()
{
ResponseService::noFeatureThenRedirect('Staff Attendance Management');
$sessionYear = $this->cache->getSessionYear();
return view('staff-attendance.your-index', compact('sessionYear'));
}
}