diff --git a/app/Http/Controllers/AttendanceRecordController.php b/app/Http/Controllers/AttendanceRecordController.php index e0be8e5ea..877714e08 100644 --- a/app/Http/Controllers/AttendanceRecordController.php +++ b/app/Http/Controllers/AttendanceRecordController.php @@ -53,8 +53,15 @@ class AttendanceRecordController extends Controller // Handle branch filter if ($request->has('branch_id') && ! empty($request->branch_id) && $request->branch_id !== 'all') { - $query->whereHas('employee', function ($subQ) use ($request) { - $subQ->where('branch_id', $request->branch_id); + $branchId = $request->branch_id; + $query->where(function ($q) use ($branchId) { + $q->where('attendance_records.branch_id', $branchId) + ->orWhere(function ($subQ) use ($branchId) { + $subQ->whereNull('attendance_records.branch_id') + ->whereHas('employee.employee', function ($empQ) use ($branchId) { + $empQ->where('branch_id', $branchId); + }); + }); }); } @@ -868,8 +875,15 @@ class AttendanceRecordController extends Controller } if ($branch_id) { - $query->whereHas('employee', function($q) use ($branch_id) { - $q->where('branch_id', $branch_id); + $query->where(function ($q) use ($branch_id, $startDate, $endDate) { + $q->whereHas('employee', function ($subQ) use ($branch_id) { + $subQ->where('branch_id', $branch_id); + })->orWhereIn('id', function ($subQ) use ($branch_id, $startDate, $endDate) { + $subQ->select('employee_id') + ->from('attendance_records') + ->where('branch_id', $branch_id) + ->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]); + }); }); } diff --git a/app/Models/AttendanceRecord.php b/app/Models/AttendanceRecord.php index 5894e9b21..11c903309 100644 --- a/app/Models/AttendanceRecord.php +++ b/app/Models/AttendanceRecord.php @@ -11,6 +11,7 @@ class AttendanceRecord extends BaseModel protected $fillable = [ 'employee_id', + 'branch_id', 'shift_id', 'attendance_policy_id', 'date', diff --git a/branch-agent/sync-agent.exe b/branch-agent/sync-agent.exe index 4879e49c8..f75da7b4c 100644 Binary files a/branch-agent/sync-agent.exe and b/branch-agent/sync-agent.exe differ diff --git a/database/migrations/2026_05_25_022152_add_branch_id_to_attendance_records_table.php b/database/migrations/2026_05_25_022152_add_branch_id_to_attendance_records_table.php new file mode 100644 index 000000000..ffeb288c8 --- /dev/null +++ b/database/migrations/2026_05_25_022152_add_branch_id_to_attendance_records_table.php @@ -0,0 +1,30 @@ +unsignedBigInteger('branch_id')->nullable()->after('employee_id'); + $table->foreign('branch_id')->references('id')->on('branches')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('attendance_records', function (Blueprint $table) { + $table->dropForeign(['branch_id']); + $table->dropColumn('branch_id'); + }); + } +}; diff --git a/docs/PLAN-biometric-agent-7days.md b/docs/PLAN-biometric-agent-7days.md new file mode 100644 index 000000000..f39df6d9e --- /dev/null +++ b/docs/PLAN-biometric-agent-7days.md @@ -0,0 +1,23 @@ +# PLAN: Biometric Agent 7-Day Sync +**Target Files:** `branch-agent/sync.js`, `app/Http/Controllers/Api/AttendanceSyncController.php` + +## Phase 0: Discovery & Context +- **Current State:** The Node.js agent (`sync.js`) currently pulls the last **15 days** of data and pushes it to the API. +- **Current State (API):** The API controller (`AttendanceSyncController@sync`) already correctly intercepts this payload and saves it into the new `biometric_attendances` database table. +- **Objective:** The user wants to adjust the agent to pull exactly the past **7 days** of data, ensuring it integrates with the newly rebuilt biometric database architecture. + +## Phase 1: Implementation Breakdown +1. **Update Agent Filtering Logic (`branch-agent/sync.js`)** + - Modify the date calculation from `setDate(getDate() - 15)` to `- 7`. + - Update console logs to clearly state it is fetching the "last 7 days". + +2. **Verify API Save Logic (`AttendanceSyncController.php`)** + - Confirm that the `sync()` method correctly handles the payload. (Already verified: it uses `BiometricAttendance::create` and ignores duplicates). + +3. **Agent Build / Packaging** + - Since the agent runs locally on Windows, any updates to `sync.js` will require repackaging (e.g., using `pkg`) so the user can download the new `.exe` to their branches. + +## Phase 2: Verification Checklist +- [x] Check if `sync.js` correctly filters logs strictly >= 7 days ago. +- [x] Verify that pushing to the API successfully inserts rows into `biometric_attendances`. +- [x] Ensure that duplicates are rejected based on `biometric_emp_id` and `punch_time`. diff --git a/docs/PLAN-multi-branch-attendance.md b/docs/PLAN-multi-branch-attendance.md new file mode 100644 index 000000000..554556f51 --- /dev/null +++ b/docs/PLAN-multi-branch-attendance.md @@ -0,0 +1,30 @@ +# PLAN: Multi-Branch Attendance + +**Target Files:** +- `branch-agent/sync.js` +- `app/Http/Controllers/Api/AttendanceSyncController.php` +- `app/Http/Controllers/BiometricAttendanceController.php` +- `app/Http/Controllers/AttendanceRecordController.php` +- `database/migrations/xxxx_add_branch_id_to_attendance_records.php` (if needed) + +## Phase 0: Context & Socratic Alignment +- **Problem:** Employees travel and clock in at multiple branches. Currently, the dashboard filters attendance based on the Employee's "Home Branch", which hides their records from the branch they actually visited. +- **Goal:** The dashboard must show attendance records based on **where they physically punched in**. +- **Agent Change:** The local agent shouldn't restrict punches. We will keep `BRANCH_ID` in the `.env` solely to tell the cloud *where* the machine is located, but we will remove any restrictions that block employees from other branches. + +## Phase 1: Database & Sync Engine Updates +1. **AttendanceRecord Migration:** + - Check if `attendance_records` table has a `branch_id` column. If not, create a migration to add it. + - This column will store the physical location where the punch happened. +2. **Sync Logic (`BiometricAttendanceController@syncAll`):** + - When converting a `biometric_attendance` record into a final `attendance_record`, map the `branch_id` from the physical ZKTeco punch directly into the `attendance_record`, bypassing the employee's default home branch. + +## Phase 2: Web Dashboard & Filtering Updates +1. **Dashboard Query (`AttendanceRecordController@index`):** + - Update the branch filter. Instead of searching `employee.branch_id`, it must search the `attendance_records.branch_id` (the actual location of the punch). + - This ensures Branch Managers can see *anyone* who clocked in at their building, even if the employee officially belongs to another branch. + +## Phase 3: Verification Checklist +- [ ] Ensure the sync engine correctly assigns the punch location's `branch_id` to the final attendance record. +- [ ] Verify that a Branch Manager can see a visiting employee's punch on their dashboard. +- [ ] Verify that the local agent still sends its location identity to the API without blocking unassigned employees.