# Attendance & Payroll — সিরিয়াল বিল্ড সিকোয়েন্স

**কার্ডের বিস্তারিত (source of truth):** [ATTENDANCE_PAYROLL_TASK_CARDS.md](./ATTENDANCE_PAYROLL_TASK_CARDS.md)  
**ডোমেইন রুলস (source of truth):** [ATTENDANCE_PAYROLL_MODULE_SPEC.md](./ATTENDANCE_PAYROLL_MODULE_SPEC.md)
**টিম বণ্টন:** [DEV_TASK_DISTRIBUTION.md](./DEV_TASK_DISTRIBUTION.md) · **Docs index:** [../README.md](../README.md)

এই ফাইল task cards **প্রতিস্থাপন করে না**। শুধু cards-এর **`Start after` / `Also needs`** অনুযায়ী **merge dependency** দিয়ে সাজায় — story number অনুযায়ী নয়।

**Last verified against branch work:** 13 Sep 2026 — ✅ **CORS local+server (card নয়).** `CorsOriginResolver`: local `localhost`/`127.0.0.1` FE ports + server public host `:3010`/`:3011` from `VITE_API_URL`; prod healthcheck + deploy preflight. · 13 Sep 2026 — ✅ **Production CORS fix (card নয়).** `CorsOriginResolver` merges `VITE_API_URL` host → `:3010` Origin; prod php-fpm healthcheck; deploy CORS preflight gate; `.env` still never regenerated by CI/CD. · 10 Sep 2026 — ✅ **CI/CD deploy branch → `development` (card নয়).** `main` আর deploy trigger নয়; push to `development` → green CI → SHA deploy; fail after checkout → automatic previous-SHA rollback (no migrate rollback). `.github/workflows/ci.yml` · `scripts/deploy-production.sh` · [ops/CICD.md](../ops/CICD.md). · 6 Sep 2026 — ✅ **`AttendanceCorrectionExecutor` missing_in/out supersede existing punches (card নয়).** When an in/out already exists, approve supersedes it (same as incorrect_time) so consecutive-type check passes. · 6 Sep 2026 — ✅ **CI/CD (GitHub Actions + SHA deploy, card নয়).** `.github/workflows/ci.yml` · `scripts/deploy-production.sh` · [ops/CICD.md](../ops/CICD.md). · 6 Sep 2026 — ✅ **PHPUnit 31-failure cleanup (CI baseline, card নয়)।** Queue jobs `$this->onQueue('long')` · weekday test 1=Sunday · `specific_user` resolver + shift-span check restored · employment own-profile route · bKash/Nagad provider guard · payroll run tests without `Company::factory()` · PHPUnit 12 `#[DataProvider]` · monthly bulk-approve queued contract। Targeted 133 tests green; full `make test` re-run this pass. · 25 Aug 2026 — ✅ **`8.3b-FE` Disbursement UI done।** `/payroll/disbursement-batches` · channel summary + readiness · bank/mobile vs cash/cheque detail · A4 register print · nav Payroll › Disbursement। **Wave 6 বন্ধ: 228 pts / 54 cards, বাকি 0।** · 25 Aug 2026 — ✅ **`8.3b-BE` Multi-Channel Disbursement done।** `disbursement_batches` / `disbursement_batch_items` · allocation-driven disburse · channel-aware readiness · send/confirm/retry · cash/cheque acknowledge · export · `PayslipPaid`/`PayrollRunPaid` · **18** test green। **Wave progress: 223 pts / 53 cards, বাকি 5 pts / 1 card।** পরের spine **`8.3b-FE`**। · 25 Aug 2026 — ✅ **`8.3a-FE` Run Approval UI done।** `/payroll/payroll-runs/{id}/approval` · `approval-summary` · platform approve/reject · `ApprovalStatusBadge` + steps trail · Net Pay≠Net Payable · flagged payslips · settlement confirm · disbursement link stub। **Wave progress: 215 pts / 52 cards, বাকি 13 pts / 2 cards।** পরের spine **`8.3b-BE`**। · 25 Aug 2026 — ✅ **`8.3a-BE` Run Approval & Advance Settlement done।** `PayrollRunExecutor` · generation→`pending_approval` · paid→settled · carry-forward · `approval-summary` · **9** test green। **Wave progress: 210 pts / 51 cards, বাকি 18 pts / 3 cards।** পরের spine **`8.3b-BE`**। · 24 Aug 2026 (পঞ্চম যাচাই) — ✅ **`8.2-FE` Payslips UI done।** Run payslips list + generate/poll/retry · detail (settlement ৩ লাইন, frozen allocations, carry-forward) · My Payslips · PDF download · regenerate draft। Nav **My Payslips** · run detail **View Payslips**। **Wave progress: 205 pts / 50 cards, বাকি 23 pts / 4 cards।** পরের spine **`8.3a-BE`**। · 24 Aug 2026 (চতুর্থ যাচাই) — ✅ **`8.2b-BE` Advance Settlement & Payment Allocation done।** `PayslipFinaliser` paid advance নেট করে; `PaymentAllocator` `payslip_payment_allocations` freeze; `GET …/allocations`; PDF settlement + channel breakdown। Draft generation `salary_advances` ছোঁয় না। **Wave progress: 197 pts / 49 cards, বাকি 31 pts / 5 cards।** পরের spine **`8.3a-BE`**। **`8.3b-এর আগে এই card merge করতেই হবে`।** · 24 Aug 2026 (তৃতীয় যাচাই) — ✅ **`8.2a-BE` Payslip Generation done।** `payslips` টেবিল (advance কলাম সহ) · queued `generate-payslips` · generation-status · list/show/me · regenerate · PDF download · `PayslipFinaliser` pass-through (`net_payable = net_pay`, `advance_paid = 0`)। Attendance শুধু snapshot। **Wave progress: 192 pts / 48 cards, বাকি 36 pts / 6 cards।** পরের spine **`8.2b-BE`**। ⚠ GitHub PR **#228** আগের মতোই 8.0-FE polish — এই card আলাদা। · 24 Aug 2026 (দ্বিতীয় যাচাই) — ⚠ **`8.2a-BE` complete নয়** ছিল — `HEAD` তখন merge PR **#228** (`pay-8-2a-be-payslip-generation`), কিন্তু diff = snapshot/freeze FE। · 24 Aug 2026 — `8.0-FE` polish: list “Attendance divergence detected” + diagnostic copy; snapshot vs live visual contrast; freeze list `employee_id` filter (existing `/payroll/monthly-freeze` route)। · 23 Aug 2026 (তৃতীয় পাস) — ✅ **`8.0-FE` Snapshot UI done।** Run-scoped list/detail, draft-only build, freeze 409/404 pre-flight, diagnostic divergence; আলাদা nav item নেই। **Wave progress: 184 pts / 47 cards, বাকি 44 pts / 7 cards।** পরের spine **`8.2a-BE` Payslip Generation**। · 23 Aug 2026 (দ্বিতীয় পাস) — ✅ **`8.0-BE` Attendance Snapshot done।** `attendance_snapshots` — timestamp-বিহীন, immutable (model-level update/delete guard), `unique(payroll_run_id, employee_id)`। চারটে endpoint: build (draft-only, `payroll.run-create`) · list · show · divergence (diagnostic, read-only)। Frozen monthly approval থেকে হুবহু কপি, কিছু পুনর্গণনা হয় না; rebuild idempotent; off-cycle run নতুন সেট পায়, পুরনোটা অক্ষত। **Wave progress তখন: 182 pts / 46 cards, বাকি 46 pts / 8 cards।** `8.0-FE` তখনো খোলা ছিল। **`8.2a-BE` Payslip Generation** spine-এর পরের ধাপ। ⚠ **Payslip integration করা যায়নি — payslip generation কোডবেসে এখনো নেই** (`8.2a-BE` pending); তার বদলে payroll attendance-এর একমাত্র উৎস হিসেবে `getPayrollAttendanceForRun()` ঘোষিত, সাথে guard test যা প্রমাণ করে ওই path-এ `attendance_records`/`monthly_attendance_approvals`-এর কোনো query যায় না। ১৭টা নতুন test সবুজ; suite **36 failed / 626 passed** — ফেল-তালিকা baseline-এর সাথে হুবহু এক, কোনো regression নেই। বিস্তারিত ফাইলের শেষে। · 23 Aug 2026 — ✅ **তিনটে card done, Wave 5 বন্ধ, Wave 6 শুরু।** `6.2-FE` Freeze UI (`1a93334f` + fix `36d66df6`) · **`8.1-BE`** Payroll Run Creation (`6fcd9218`, fix PR #215) · **`8.1-FE`** Payroll Run UI (PR #209)। **Wave progress: 177 pts / 45 cards, বাকি 51 pts / 9 cards — বাকিটা পুরো Wave 6।** এতে **`8.0-BE` Attendance Snapshot unlock হলো** (`8.1-BE` + `6.2-BE` দুটোই লাগত), এখন critical path। ⚠ **দুটো gate-ই আগের চেয়ে অনেক বেশি লাল** — `php artisan test` **36 failed / 609 passed** (ছিল 1 failed) ১০টা class জুড়ে, আর `tsc -b --force` **৩৫ error** (ছিল ১৫)। এর মধ্যে **`8.1-BE`-র নিজের দুটো test ফাইলের একটাও চলে না** (`Company::factory()` নেই · PHPUnit 12-তে `@dataProvider` docblock উপেক্ষিত) আর **`8.1-FE` ১০টা নতুন tsc error** এনেছে — পঞ্চম FE card যেটা লাল build নিয়ে merge হলো। এর বাইরে PR #208 · #210–#214 · #216–#219 সবই Employee/Platform-এর কাজ ও bug fix, **card নয়, points অপরিবর্তিত**। বিস্তারিত ফাইলের শেষে। · 20 Aug 2026 — 🎨 **UI design pass** (card নয়, points অপরিবর্তিত; branch `design-change-based-on-gemini`, 62 ফাইল · +226/−278): breadcrumb বাদ · `PageHeader`-এর subtitle ৫২টা call site থেকে বাদ ও উচ্চতা ~82px→~60px · header-এর অ্যাকশন বাটন `md`→`sm` (৫৪ জায়গা) · `DataTable`-এর row **40px→28px** (`py-1.5` + 12px font, ৬৫টা পেজে) · Employee list-এ select bar আর টেবিল ঠেলে না · company switch-এ blocking overlay + `Switched to X` toast। **Attendance/Payroll-এর business logic ছোঁয়া হয়নি**, কিন্তু `PageHeader`/`DataTable` প্রায় প্রতিটা স্ক্রিনে বসে — screenshot মেলাতে গেলে জানা দরকার। Gate: `tsc -b` **১৫ error, baseline-এর সাথে হুবহু এক** (একটাও নতুন নয়), `oxlint` অপরিবর্তিত, `vite build` সবুজ; `check-style-boundary.sh` 🔴 তবে **আগে থেকেই** — `JobStatusPanel.tsx:87`-এর hard-coded `text-[10px]`, clean HEAD-এ যাচাই করা, আর **`npm run build` ওখানেই আটকায়**। বিস্তারিত ফাইলের শেষে। · 18 Aug 2026 (দ্বিতীয় পাস) — **`6.1-FE` Monthly Approval UI merged (PR #198, Bablu)** ⚠ **দুটো gate-ই ভেঙে**: `tsc -b` **১৫ error** (তাই `npm run build` লাল) আর `php artisan test` **1 failed / 452 passed** — bulk-approve-এর response contract queued batch-এ বদলেছে কিন্তু `6.1-BE`-র নিজের `MonthlyAttendanceApprovalServiceTest` আপডেট হয়নি। **Wave progress: 167 pts / 42 cards, বাকি 61 pts / 12 cards।** কার্ডের কয়েকটা মূল rule-ও আসেনি — **detail page-এর Approve সবসময় `override: true` পাঠায়**, unlock reason hardcoded, reject action নেই, day-by-day breakdown placeholder, আর unresolved popover কেবল একটা সাধারণ বাক্য দেখায় (blocker list বা deep link নেই, API-ও ওটা দেয় না)। বিস্তারিত ফাইলের শেষে। · 18 Aug 2026 — **`9.4-FE` Attendance Jobs UI merged (PR #197, Munna)**, তাই Wave 5-এর jobs track শেষ। **Wave progress: 162 pts / 41 cards, বাকি 66 pts / 13 cards** (উপরের হিসাবটা `6.1-BE` ও `6.2-BE` দুটো card পিছিয়ে ছিল, Seq সারি থেকে গুনে মেলানো হয়েছে)। **আগের তিনটে blocker-ই এখন শেষ** — `php artisan test` **453 passed / 1438 assertions** (fatal নেই, ছয় executor-এই `reject()` আছে, `phpunit.xml`-এ `Modules` testsuite declare করা), `Modules/Attendance/tests` **79 passed**, `tsc -b --force` ০ error, `vite build` সবুজ। তবে `9.4-FE` চারটে ঋণ নিয়ে ঢুকেছে — **"Fix Assignment" link আসলে কিছুই filter করে না**, timezone hardcoded `Asia/Dhaka`, status panel unfiltered activity log পড়ে, আর `JobStatusPanel.tsx` dead code; বিস্তারিত ফাইলের শেষে। · 17 Aug 2026 (তৃতীয় পাস) — **`5.5-FE` Correction Approval UI merged (PR #189)** ⚠ `tsc -b` ভেঙে, আর **`9.2-BE` Leave Accrual & Carry-Forward merged (PR #190)**। এতে **Wave 4 বন্ধ (25/25)** আর **`9.4-FE` unlock হলো**। **Wave progress: 148 pts / 38 cards, বাকি 80 pts / 16 cards।** এই পাসে তিনটে blocker যাচাই করা হয়েছে, যার একটা আগে কোথাও লেখাই ছিল না — `phpunit.xml`-এ module testsuite declare করা নেই বলে **`Modules/Attendance`-এর ৬০টা test `php artisan test`-এ কোনোদিন চলেই না**; explicit path দিলে **20 failed / 40 passed**। বাকি দুটো: executor `reject()` fatal (অপরিবর্তিত) আর `npm run build` লাল। সাথে **PR #191 — platform company context** (card নয়, points অপরিবর্তিত)। · 17 Aug 2026 — 🐛 **Platform module-এ দুটো infinite-render loop সারানো** (`UserRolesDrawer`, `RolePermissionsDrawer`) — "Maximum update depth exceeded"; বিস্তারিত এই ফাইলের শেষে। **Attendance/Payroll-এর কোনো card এগোয়নি, points অপরিবর্তিত।** · 17 Aug 2026 — ✅ **`5.3-FE`-র build-ঋণ শোধ:** `npm run build` আবার সবুজ (`tsc -b` ০ error, `vite build` পাস), আর stale-balance banner-এর **"Refresh Balance" button এখন render হয়** (`Alert`-এ `tone`/`actions`)। সাথে একটা **নতুন bug ধরা পড়েছে যা আগের নোটে উল্টো লেখা ছিল** — per-day breakdown runtime-এ কাজ করছিল **না**, কারণ `leaveRequestApi.normalizeRequest` `days` ফেলে দিচ্ছিল; এখন carry করে। নিচের নোট দেখুন। · 17 Aug 2026 — **`5.3-FE` Leave Approvals UI merged (PR #188)**, তাই **Wave 4-এ এখন শুধু `5.5-FE` বাকি**। ⚠ কিন্তু ঐ PR-এ `npm run build` ভেঙে গেছে (`tsc -b` ১৬ error) আর একটা card rule চুপচাপ কাজ করছে না — নিচের নোট দেখুন। **Wave progress টেবিলটা সাতটা merged card পিছিয়ে ছিল, এখন গুনে ঠিক করা হয়েছে: 140 pts / 36 cards, বাকি 88 pts / 18 cards** (আগে ভুল করে 114/29 লেখা ছিল)। · 17 Aug 2026 — Employee module-এ ছয়টা কাজ শেষ (contact-এ user email/mobile default · emergency phone rule · address geo cascade · contact update-এর "already in use" bug · salary tab-এ `*` · dropdown deselect bug); প্রতিটার বিস্তারিত এই ফাইলের শেষে। **Attendance/Payroll-এর কোনো card এগোয়নি, points অপরিবর্তিত।** তবে suite চালাতে গিয়ে **একটা hard blocker বেরিয়েছে যা এতদিন কোথাও লেখা ছিল না** — নিচের "Next" দেখুন। · 13 Aug 2026 (দ্বিতীয় পাস) — **`5.3a-BE`** (PR #180) · **`5.4-FE`** (PR #181) merged, আর **`5.3b-BE`-র কাজটাও 5.3a-র PR-এই ঢুকে গেছে** (আসল `LeaveDayResolverService` bind + R2 branch live) — তবে তিনটে DoD-ঋণ সহ, দেখুন "5.3b-এর ঋণ" অংশ। · 13 Aug 2026 — **`2.2-FE`** Applied Policy Panel (PR #182) merged; snapshot-এর পুরো ফিল্ড সেট + Recalculate split control এসেছে, তাই **Story 2.2 ও Wave 3 দুটোই সম্পূর্ণ** (43/43 pts)। · 11 Aug 2026 — **Story 5.2 সম্পূর্ণ** (`5.2-BE` PR-পরবর্তী fix + `5.2-FE`, PR #164), **`5.4-BE`** Correction Request (PR #163), **`3.2-FE`** Daily Summary / Records (PR #165) merged। `2.2-FE`-র read-only panel 3.2-FE-র detail view-তে ঢুকেছে, কিন্তু recalculate control এখনো নেই — **partial**। · 10 Aug 2026 (recheck) — **3.1-BE** Punches · **3.1-FE** Punch UI · **3.2-BE** Daily Summary · **7.4-BE** Deductions & Loans · **7.0-FE** Settings UI · **7.3-FE** Tax Slabs UI সব merged (PR #150–#155)। Earlier same day: **7.2-FE** · **7.2-BE** · **4.1-FE** · **5.1-BE** · **5.1-FE**. এর পর merge হয়েছে **7.4-FE** Deductions & Loans UI (PR #157) ও **10.2-BE** Bank Account Validation। **Story 7.5 সম্পূর্ণ merged — `7.5-BE` (PR #160) ও `7.5-FE` (PR #161)।** · 24 Aug 2026 — `8.0-FE` polish: list “Attendance divergence detected” + diagnostic copy; snapshot vs live visual contrast; freeze list `employee_id` filter (existing `/payroll/monthly-freeze` route)। · 23 Aug 2026 (তৃতীয় পাস) — ✅ **`8.0-FE` Snapshot UI done।** Run-scoped list/detail, draft-only build, freeze 409/404 pre-flight, diagnostic divergence; আলাদা nav item নেই। **Wave progress: 184 pts / 47 cards, বাকি 44 pts / 7 cards।** পরের spine **`8.2a-BE` Payslip Generation**। · 23 Aug 2026 (দ্বিতীয় পাস) — ✅ **`8.0-BE` Attendance Snapshot done।** `attendance_snapshots` — timestamp-বিহীন, immutable (model-level update/delete guard), `unique(payroll_run_id, employee_id)`। চারটে endpoint: build (draft-only, `payroll.run-create`) · list · show · divergence (diagnostic, read-only)। Frozen monthly approval থেকে হুবহু কপি, কিছু পুনর্গণনা হয় না; rebuild idempotent; off-cycle run নতুন সেট পায়, পুরনোটা অক্ষত। **Wave progress তখন: 182 pts / 46 cards, বাকি 46 pts / 8 cards।** `8.0-FE` তখনো খোলা ছিল। **`8.2a-BE` Payslip Generation** spine-এর পরের ধাপ। ⚠ **Payslip integration করা যায়নি — payslip generation কোডবেসে এখনো নেই** (`8.2a-BE` pending); তার বদলে payroll attendance-এর একমাত্র উৎস হিসেবে `getPayrollAttendanceForRun()` ঘোষিত, সাথে guard test যা প্রমাণ করে ওই path-এ `attendance_records`/`monthly_attendance_approvals`-এর কোনো query যায় না। ১৭টা নতুন test সবুজ; suite **36 failed / 626 passed** — ফেল-তালিকা baseline-এর সাথে হুবহু এক, কোনো regression নেই। বিস্তারিত ফাইলের শেষে। · 23 Aug 2026 — ✅ **তিনটে card done, Wave 5 বন্ধ, Wave 6 শুরু।** `6.2-FE` Freeze UI (`1a93334f` + fix `36d66df6`) · **`8.1-BE`** Payroll Run Creation (`6fcd9218`, fix PR #215) · **`8.1-FE`** Payroll Run UI (PR #209)। **Wave progress: 177 pts / 45 cards, বাকি 51 pts / 9 cards — বাকিটা পুরো Wave 6।** এতে **`8.0-BE` Attendance Snapshot unlock হলো** (`8.1-BE` + `6.2-BE` দুটোই লাগত), এখন critical path। ⚠ **দুটো gate-ই আগের চেয়ে অনেক বেশি লাল** — `php artisan test` **36 failed / 609 passed** (ছিল 1 failed) ১০টা class জুড়ে, আর `tsc -b --force` **৩৫ error** (ছিল ১৫)। এর মধ্যে **`8.1-BE`-র নিজের দুটো test ফাইলের একটাও চলে না** (`Company::factory()` নেই · PHPUnit 12-তে `@dataProvider` docblock উপেক্ষিত) আর **`8.1-FE` ১০টা নতুন tsc error** এনেছে — পঞ্চম FE card যেটা লাল build নিয়ে merge হলো। এর বাইরে PR #208 · #210–#214 · #216–#219 সবই Employee/Platform-এর কাজ ও bug fix, **card নয়, points অপরিবর্তিত**। বিস্তারিত ফাইলের শেষে। · 20 Aug 2026 — 🎨 **UI design pass** (card নয়, points অপরিবর্তিত; branch `design-change-based-on-gemini`, 62 ফাইল · +226/−278): breadcrumb বাদ · `PageHeader`-এর subtitle ৫২টা call site থেকে বাদ ও উচ্চতা ~82px→~60px · header-এর অ্যাকশন বাটন `md`→`sm` (৫৪ জায়গা) · `DataTable`-এর row **40px→28px** (`py-1.5` + 12px font, ৬৫টা পেজে) · Employee list-এ select bar আর টেবিল ঠেলে না · company switch-এ blocking overlay + `Switched to X` toast। **Attendance/Payroll-এর business logic ছোঁয়া হয়নি**, কিন্তু `PageHeader`/`DataTable` প্রায় প্রতিটা স্ক্রিনে বসে — screenshot মেলাতে গেলে জানা দরকার। Gate: `tsc -b` **১৫ error, baseline-এর সাথে হুবহু এক** (একটাও নতুন নয়), `oxlint` অপরিবর্তিত, `vite build` সবুজ; `check-style-boundary.sh` 🔴 তবে **আগে থেকেই** — `JobStatusPanel.tsx:87`-এর hard-coded `text-[10px]`, clean HEAD-এ যাচাই করা, আর **`npm run build` ওখানেই আটকায়**। বিস্তারিত ফাইলের শেষে। · 18 Aug 2026 (দ্বিতীয় পাস) — **`6.1-FE` Monthly Approval UI merged (PR #198, Bablu)** ⚠ **দুটো gate-ই ভেঙে**: `tsc -b` **১৫ error** (তাই `npm run build` লাল) আর `php artisan test` **1 failed / 452 passed** — bulk-approve-এর response contract queued batch-এ বদলেছে কিন্তু `6.1-BE`-র নিজের `MonthlyAttendanceApprovalServiceTest` আপডেট হয়নি। **Wave progress: 167 pts / 42 cards, বাকি 61 pts / 12 cards।** কার্ডের কয়েকটা মূল rule-ও আসেনি — **detail page-এর Approve সবসময় `override: true` পাঠায়**, unlock reason hardcoded, reject action নেই, day-by-day breakdown placeholder, আর unresolved popover কেবল একটা সাধারণ বাক্য দেখায় (blocker list বা deep link নেই, API-ও ওটা দেয় না)। বিস্তারিত ফাইলের শেষে। · 18 Aug 2026 — **`9.4-FE` Attendance Jobs UI merged (PR #197, Munna)**, তাই Wave 5-এর jobs track শেষ। **Wave progress: 162 pts / 41 cards, বাকি 66 pts / 13 cards** (উপরের হিসাবটা `6.1-BE` ও `6.2-BE` দুটো card পিছিয়ে ছিল, Seq সারি থেকে গুনে মেলানো হয়েছে)। **আগের তিনটে blocker-ই এখন শেষ** — `php artisan test` **453 passed / 1438 assertions** (fatal নেই, ছয় executor-এই `reject()` আছে, `phpunit.xml`-এ `Modules` testsuite declare করা), `Modules/Attendance/tests` **79 passed**, `tsc -b --force` ০ error, `vite build` সবুজ। তবে `9.4-FE` চারটে ঋণ নিয়ে ঢুকেছে — **"Fix Assignment" link আসলে কিছুই filter করে না**, timezone hardcoded `Asia/Dhaka`, status panel unfiltered activity log পড়ে, আর `JobStatusPanel.tsx` dead code; বিস্তারিত ফাইলের শেষে। · 17 Aug 2026 (তৃতীয় পাস) — **`5.5-FE` Correction Approval UI merged (PR #189)** ⚠ `tsc -b` ভেঙে, আর **`9.2-BE` Leave Accrual & Carry-Forward merged (PR #190)**। এতে **Wave 4 বন্ধ (25/25)** আর **`9.4-FE` unlock হলো**। **Wave progress: 148 pts / 38 cards, বাকি 80 pts / 16 cards।** এই পাসে তিনটে blocker যাচাই করা হয়েছে, যার একটা আগে কোথাও লেখাই ছিল না — `phpunit.xml`-এ module testsuite declare করা নেই বলে **`Modules/Attendance`-এর ৬০টা test `php artisan test`-এ কোনোদিন চলেই না**; explicit path দিলে **20 failed / 40 passed**। বাকি দুটো: executor `reject()` fatal (অপরিবর্তিত) আর `npm run build` লাল। সাথে **PR #191 — platform company context** (card নয়, points অপরিবর্তিত)। · 17 Aug 2026 — 🐛 **Platform module-এ দুটো infinite-render loop সারানো** (`UserRolesDrawer`, `RolePermissionsDrawer`) — "Maximum update depth exceeded"; বিস্তারিত এই ফাইলের শেষে। **Attendance/Payroll-এর কোনো card এগোয়নি, points অপরিবর্তিত।** · 17 Aug 2026 — ✅ **`5.3-FE`-র build-ঋণ শোধ:** `npm run build` আবার সবুজ (`tsc -b` ০ error, `vite build` পাস), আর stale-balance banner-এর **"Refresh Balance" button এখন render হয়** (`Alert`-এ `tone`/`actions`)। সাথে একটা **নতুন bug ধরা পড়েছে যা আগের নোটে উল্টো লেখা ছিল** — per-day breakdown runtime-এ কাজ করছিল **না**, কারণ `leaveRequestApi.normalizeRequest` `days` ফেলে দিচ্ছিল; এখন carry করে। নিচের নোট দেখুন। · 17 Aug 2026 — **`5.3-FE` Leave Approvals UI merged (PR #188)**, তাই **Wave 4-এ এখন শুধু `5.5-FE` বাকি**। ⚠ কিন্তু ঐ PR-এ `npm run build` ভেঙে গেছে (`tsc -b` ১৬ error) আর একটা card rule চুপচাপ কাজ করছে না — নিচের নোট দেখুন। **Wave progress টেবিলটা সাতটা merged card পিছিয়ে ছিল, এখন গুনে ঠিক করা হয়েছে: 140 pts / 36 cards, বাকি 88 pts / 18 cards** (আগে ভুল করে 114/29 লেখা ছিল)। · 17 Aug 2026 — Employee module-এ ছয়টা কাজ শেষ (contact-এ user email/mobile default · emergency phone rule · address geo cascade · contact update-এর "already in use" bug · salary tab-এ `*` · dropdown deselect bug); প্রতিটার বিস্তারিত এই ফাইলের শেষে। **Attendance/Payroll-এর কোনো card এগোয়নি, points অপরিবর্তিত।** তবে suite চালাতে গিয়ে **একটা hard blocker বেরিয়েছে যা এতদিন কোথাও লেখা ছিল না** — নিচের "Next" দেখুন। · 13 Aug 2026 (দ্বিতীয় পাস) — **`5.3a-BE`** (PR #180) · **`5.4-FE`** (PR #181) merged, আর **`5.3b-BE`-র কাজটাও 5.3a-র PR-এই ঢুকে গেছে** (আসল `LeaveDayResolverService` bind + R2 branch live) — তবে তিনটে DoD-ঋণ সহ, দেখুন "5.3b-এর ঋণ" অংশ। · 13 Aug 2026 — **`2.2-FE`** Applied Policy Panel (PR #182) merged; snapshot-এর পুরো ফিল্ড সেট + Recalculate split control এসেছে, তাই **Story 2.2 ও Wave 3 দুটোই সম্পূর্ণ** (43/43 pts)। · 11 Aug 2026 — **Story 5.2 সম্পূর্ণ** (`5.2-BE` PR-পরবর্তী fix + `5.2-FE`, PR #164), **`5.4-BE`** Correction Request (PR #163), **`3.2-FE`** Daily Summary / Records (PR #165) merged। `2.2-FE`-র read-only panel 3.2-FE-র detail view-তে ঢুকেছে, কিন্তু recalculate control এখনো নেই — **partial**। · 10 Aug 2026 (recheck) — **3.1-BE** Punches · **3.1-FE** Punch UI · **3.2-BE** Daily Summary · **7.4-BE** Deductions & Loans · **7.0-FE** Settings UI · **7.3-FE** Tax Slabs UI সব merged (PR #150–#155)। Earlier same day: **7.2-FE** · **7.2-BE** · **4.1-FE** · **5.1-BE** · **5.1-FE**. এর পর merge হয়েছে **7.4-FE** Deductions & Loans UI (PR #157) ও **10.2-BE** Bank Account Validation। **Story 7.5 সম্পূর্ণ merged — `7.5-BE` (PR #160) ও `7.5-FE` (PR #161)।**

**বেসলাইন (ইতোমধ্যে শেষ):** Part 0 · Part A (1.1–1.4) · 2.1-BE · 2.1-FE · 2.2-BE  
**Story 2.2 সম্পূর্ণ** — `2.2-FE` merged (PR #182)। Snapshot-এর পুরো ফিল্ড সেট আর Recalculate split control দুটোই এসেছে (দেখুন Seq 27)। **Wave 3 বন্ধ।**

### এখনকার বোর্ড (25 Aug 2026, কোডবেস যাচাই)

Waves **1–6 বন্ধ**। Wave 6 **59/59 pts** (`8.1-BE` · `8.1-FE` · `8.0-BE` · `8.0-FE` · `8.2a-BE` · `8.2b-BE` · `8.2-FE` · `8.3a-BE` · `8.3a-FE` · `8.3b-BE` · **`8.3b-FE`**).

| খোলা এখন | Pts | কোডবেস |
|---|---|---|
| — | — | Attendance/Payroll spine complete |

সিরিয়াল: `8.2a-BE` → `8.2b-BE` → `8.3a-BE` → `8.3b-BE` → `8.3b-FE`। **`8.2b` আগে, তারপর `8.3b`।**

Payslip attendance source (ইতিমধ্যে বেঁধে দেওয়া): `AttendanceSnapshotServiceInterface::getPayrollAttendanceForRun()` — `attendance_records` নয়।

### এখন পর্যন্ত Wave progress

| Done                                  | Pts    | Evidence (short)                                                                                             |
| ------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------ |
| **3.1-BE** Multi-Punch Check-In/Out   | 5      | `attendance_punches` (append-only) + `PunchService`; `/attendance/punch` · `/punches`; `PunchCreated`        |
| **3.1-FE** Punch UI                   | 3      | `PunchWidget` + Employee Punch page; `punchApi.ts`                                                            |
| **3.2-BE** Daily Attendance Summary   | 8      | `attendance_records` + calculation/recalculation service; today/list/show/punches/export; null LeaveDayResolver |
| **4.1-BE** Approval Integration       | 5      | 5 executor stubs + registry; demo `approval_settings` seeder; bypass/workflow tests                          |
| **4.1-FE** Approval Settings Exposure | 3      | Core › Approval Settings filters + New highlight; shared badge/pending action; requests list reuse           |
| **5.1-BE** Leave Balances             | 5      | `leave_balances` + ledger; `/attendance/leave-balances` list/me/history/adjust                               |
| **5.1-FE** Leave Balance UI           | 3      | Attendance › Leave › Balances; employee history; adjust modal; Dashboard My Balances                         |
| **7.0-BE** Payroll Settings           | 3      | `/payroll/settings` API                                                                                      |
| **7.0-FE** Payroll Settings UI        | 3      | Payroll › Config › Settings (`ready: true`) + worked-example panel                                           |
| **7.1-BE** Salary Structures          | 3      | `/payroll/salary-structures` API                                                                             |
| **7.1-FE** Salary Structures UI       | 2      | Payroll › Configuration › Salary Structures                                                                  |
| **7.2-BE** Structure Components       | 5      | `salary_structure_components` + nested CRUD/reorder/preview; shared calculator                               |
| **7.2-FE** Structure Components UI    | 5      | Payroll › Config › Salary Structures — stacked Offcanvas builder + preview; `salaryStructureComponentApi.ts` |
| **7.3-BE** Tax Slabs                  | 3      | `/payroll/tax-slabs` API (+ calculate)                                                                       |
| **7.3-FE** Tax Slabs UI               | 3      | Payroll › Config › Tax Slabs list + form (`ready: true`)                                                     |
| **7.4-BE** Deductions & Loans         | 5      | `employee_deductions` + `_entries` (unique per run); ৬টা endpoint; idempotent `applyInstallment` + review flag |
| **7.4-FE** Deductions & Loans UI      | 3      | Payroll › Config › Deductions & Loans (`ready: true`); list + detail page; `employeeDeductionApi.ts`         |
| **7.5-BE** Salary Advances            | 8      | `salary_advances` + ৮ endpoint; frozen `basis_gross`; মাস-total ceiling; executor + দুই approval path (PR #160) |
| **7.5-FE** Salary Advances UI         | 5      | Payroll › Salary Advances — single screen, সব operation stacked Offcanvas-এ; `salaryAdvanceApi.ts` (PR #161) |
| **7.6-BE** Payment Mode Split         | 5      | `/payroll/employee-salaries/{id}/payment-modes`                                                              |
| **7.6-FE** Payment Mode Split UI      | 3      | Employees › Salary › Payment split panel                                                                     |
| **10.1-BE** Salary Field Validation   | 1      | currency/frequency/active-structure 422 + tests                                                              |
| **10.2-BE** Bank Account Validation   | 1      | OR-method rule + single-primary (model-level) + partial-update fix; 36 tests green                           |
| **9.3-BE** Default Data Seeders       | 3      | ১১ system type + default shift, per company; `Company::created` provisioning hook; re-run/customisation test |
| **10.3-BE** Missing Payment Method Report | 3  | `/employee/employees/without-primary-payment-method`; একটাই `NOT EXISTS` query, 8.3b readiness এটাই resolve করবে; 17 tests green |
| **5.2-BE** Leave Application          | 5      | `leave_requests` + ৫ endpoint; এক `evaluate()` path preview ও store-এ; ISO working-day count; IDOR বন্ধ; 25 tests green |
| **5.2-FE** Leave Application UI       | 5      | Attendance › Leave › Requests — apply form (debounced preview, live day count + before/after balance), list, detail, cancel |
| **3.2-FE** Daily Summary / Records    | 5      | Attendance › Attendance Records — record grid + detail view; applied-policy snapshot panel detail-এ embedded (PR #165) |
| **5.4-BE** Correction Request         | 3      | `correction_requests` + store/index/show/cancel; duplicate guard; policy + approval executor; 7 tests green (PR #163) |
| **2.2-FE** Applied Policy Panel       | 2      | Snapshot-এর পুরো ফিল্ড সেট + Recalculate split control (PR #182)                                            |
| **5.3a-BE** Leave Approval            | 5      | `LeaveRequestExecutor` — lockForUpdate, execution-time balance পুনঃযাচাই, ledger + `leave_request_days` (PR #180) |
| **5.4-FE** Correction Request UI      | 3      | Attendance › Corrections — submit + list + detail + cancel (PR #181)                                        |
| **5.3b-BE** Punch Voids Leave         | 3      | তিনটে DoD-ঋণ শোধ; `PunchLeaveVoidTest` 7 test green (PR #186)                                              |
| **5.5-BE** Correction Approval        | 5      | Executor + supersede + locked-month override (PR #185) ⚠ cross-lane gate ভেঙে merged                        |
| **9.1-BE** Nightly Attendance Close   | 5      | `ScheduleCloseDayCommand` — এক hourly cron, per-company timezone; query-তেই idempotent (PR #187) ⚠ ৩ DoD-ঋণ  |
| **5.3-FE** Leave Approvals UI         | 3      | Queue + detail + reject modal; fresh balance, stale-approve guard (PR #188) ✅ build-ঋণ শোধ — `tsc -b` সবুজ, Refresh button render হয়, day breakdown সত্যিই আসে |
| **5.5-FE** Correction Approval UI     | 3      | HR queue + detail + day comparison + reject modal + locked-month banner (PR #189) ⚠ `tsc -b` ভেঙে merged |
| **9.2-BE** Leave Accrual & Carry-Forward | 5   | দুটো scheduled command + chunk job + manual trigger endpoint; `LeaveAccrualJobTest` green (PR #190)        |
| **6.1-BE** Monthly Attendance Approval | 8     | HR মাস বন্ধ করে — ৯টা endpoint; suite 452/452 (PR #192)                                                     |
| **6.2-BE** Monthly Freeze             | 3      | Finance lock — `MonthlyAttendanceFreezeService` + freeze/unfreeze endpoint + `MonthFrozen` event (PR #195)   |
| **9.4-FE** Attendance Jobs UI         | 3      | Attendance › Jobs — unassigned report + তিন-tab manual trigger + batch progress (`sessionStorage` + `?batch=`); সাথে accrual-এ annual cap guard (PR #197) ⚠ ৪ DoD-ঋণ |
| **6.1-FE** Monthly Approval UI        | 5      | Attendance › Monthly Approval — grid + filter/search + build drawer + bulk approve (queued batch, live progress) + detail (PR #198) ⚠ build ও suite দুটোই ভেঙে merged |
| **6.2-FE** Freeze UI                  | 2      | Payroll › Monthly Freeze — `MonthlyFreezePage.tsx` (`1a93334f`), পরে list-না-দেখানোর fix (`36d66df6`) — **Wave 5 বন্ধ** |
| **8.1-BE** Payroll Run Creation       | 5      | `payroll_runs` টেবিল + `PayrollRun`/`PayrollRunService` + run index/store/readiness/show (`6fcd9218`, fix PR #215) ⚠ নিজের দুটো test ফাইলই চলে না |
| **8.1-FE** Payroll Run UI             | 3      | Payroll › Payroll Runs — list/create/detail + `payrollRunApi.ts` (PR #209) ⚠ tsc-তে ১০ নতুন error |
| **8.0-BE** Attendance Snapshot        | 5      | `attendance_snapshots` (immutable, timestamp-বিহীন) + build/list/show/divergence; payroll attendance-এর একমাত্র উৎস, ১৭ test সবুজ |
| **8.0-FE** Snapshot UI                | 2      | run detail → snapshots; draft-only build; freeze pre-flight; diagnostic divergence; list copy + snapshot/live contrast; freeze `employee_id` query |
| **8.2a-BE** Payslip Generation         | 8      | payslips pipeline + generate/status/list/me/regenerate/PDF (earlier card) |
| **8.2b-BE** Advance Settlement         | 5      | Finaliser net + PaymentAllocator freeze |
| **8.2-FE** Payslips UI                 | 8      | Run payslips + My Payslips + detail |
| **8.3a-BE** Run Approval & Settlement  | 5      | Executor · paid→settled · carry-forward · approval-summary |
| **8.3a-FE** Run Approval UI            | 5      | `/payroll/payroll-runs/{id}/approval` decision screen · platform approve/reject |
| **8.3b-BE** Multi-Channel Disbursement | 8      | batches/items · send/confirm/retry/ack · export · 18 test |
| **8.3b-FE** Disbursement UI            | 5      | `/payroll/disbursement-batches` · channel summary + readiness · register vs transmitted detail · A4 print |
| **মোট Wave done**                     | **228** | 54 cards                                                                                                    |

**Wave breakdown:** Wave 1 done **39/39 pts** · Wave 2 done **31/31 pts** · Wave 3 done **43/43 pts** · **Wave 4 done 25/25 pts — বন্ধ** · **Wave 5 done 31/31 pts — বন্ধ** (`9.1-BE` · `9.2-BE` · `6.1-BE` · `6.2-BE` · `9.4-FE` · `6.1-FE` · `6.2-FE`) · **Wave 6 done 59/59** (`8.1-BE` · `8.1-FE` · `8.0-BE` · `8.0-FE` · `8.2a-BE` · `8.2b-BE` · `8.2-FE` · `8.3a-BE` · `8.3a-FE` · `8.3b-BE` · `8.3b-FE`) — **বন্ধ**।

**বাকি:** 0 card · **0 pts** — Attendance/Payroll spine complete।

> এই টেবিলটা 16 Aug পর্যন্ত সাতটা merged card পিছিয়ে ছিল (114/29 লেখা ছিল)। 17 Aug 2026-এ Seq সারিগুলোর status থেকে গুনে মেলানো হয়েছে — **যেকোনো অমিলে Seq সারিই সঠিক ধরুন**, ওগুলো card-ভিত্তিক।

**টেস্ট gap (merge হয়েছে, DoD পুরোপুরি হয়নি):** **7.4-BE** — CRUD/422/409 feature test আছে (`backend/tests/Feature/EmployeeDeductionTest.php`), কিন্তু DoD-এর idempotency test (generate + ২ বার regenerate = একটাই decrement) ও negative-net-pay skip test নেই। **3.2-BE** — status-ladder branch এখন ঢাকা (নিচের ফিক্স দেখুন), কিন্তু session-pairing ও visibility-scoping test এখনো নেই। **8.2a-BE** ধরার আগে বাকিটা ঢাকা উচিত।

> ### ✅ 3.2-BE — `calculateDaily`-র shift resolution ঠিক করা হয়েছে (10 Aug 2026, Siam)
>
> **যা ভাঙা ছিল:** `AssignmentResolutionService` `ResolvedContext::$shift`-এ **`Assignment` row** বসায় (`assignable_type='shift'`, `assignable_id=<shift id>`) — আসল `Shift` কখনো load হতো না। `AttendanceRecordService` ওটাকে shift ধরে `start_time` · `grace_minutes` · `working_hours` · `min_hours_present` · `min_hours_half_day` · `working_days` পড়ত, যার একটাও `assignments`-এ নেই। ফলে **weekend ও holiday কখনো resolve হতো না**, সব shift parameter fallback default-এ চলত, আর `attendance_records.shift_id`-তে assignment id বসত।
>
> **ফিক্স:** `AttendanceRecordService` এখন assignment থেকে আসল `Shift` ও holiday `AttendancePolicy` load করে — ঠিক যেভাবে `PunchService` আগে থেকেই করত। `ResolvedContext`-এর আকার বদলানো হয়নি, তাই `PunchService` / resolution controller / recalculation service অক্ষত। `isWorkingDay()` এখন ISO day number (1=Mon … 7=Sun) মেলায়, `isHoliday()` policy config পড়ে ও recurring holiday একই month/day-তে মেলায় (1.3-BE-র রুল)।
>
> **সাথে আরেকটা bug:** `AttendancePunchRepository` `where('attendance_date', $date)` করত, কিন্তু model তারিখটা time-সহ persist করে। MySQL DATE কলামে truncate হয় বলে prod-এ চলত, **SQLite-এ কখনোই মিলত না** — এ কারণেই 3.2-BE-র calculation test কোনোদিন লেখা যায়নি। এখন `whereDate()`, দুই driver-এই সঠিক।
>
> **টেস্ট:** `AttendanceDailyCalculationTest` — weekend · working day · holiday · recurring holiday · non-recurring অন্য বছরে না মেলা · grace-এর ভিতরে present · grace-এর বাইরে late (shift-এর নিজের 15 মিনিট) · shift-এর নিজের 3-ঘণ্টা half-day threshold · `shift_id` ও snapshot-এ আসল shift। **9 test green।**
>
> এতে 3.2-BE-র টেস্ট-ঋণের status-ladder অংশটা ঢাকা পড়েছে; **session-pairing ও visibility-scoping test এখনো বাকি**।

### ⚠ Employee module-এর দুটো পরিবর্তন যা **সব lane-কে ছোঁয়** (17 Aug 2026)

এই দফার ছয়টা কাজের বিস্তারিত ফাইলের শেষে। তার মধ্যে দুটো নিজের module-এ আটকে নেই — attendance/payroll-এ কাজ করলেও গায়ে লাগবে:

| কী | কাকে ছোঁয় |
|---|---|
| **`BangladeshGeoSeeder` এখন `DatabaseSeeder`-এ** — নতুন `geo_divisions` / `geo_districts` / `geo_post_offices` টেবিল, ১৩৩০ row | **যে কোনো test যা `$this->seed()` ডাকে** এখন এই row গুলোও seed করে। পাঁচটা bulk `upsert` statement বলে খরচ নগণ্য, কিন্তু `migrate:fresh` ছাড়া পুরনো DB-তে নতুন migration চালানো লাগবে |
| **শেয়ার্ড `Select`-এ deselect fix** — placeholder এখন optional field-এ selectable | **৭৬টা ফাইল `<Select>` ব্যবহার করে** (attendance ২০, configuration ১৯, employee ১৪, platform ১১, payroll ৯)। Optional dropdown ও filter এখন খালি করা যায়; required গুলো অপরিবর্তিত |

দ্বিতীয়টা আচরণ বদলায় বলে আলাদা করে বলা — কোনো attendance/payroll screen যদি "একবার বাছলে আর বদলানো যাবে না" ধরে নিয়ে বানানো থাকে, সেটা এখন আর সত্যি নয়। (এমন কিছু চোখে পড়েনি, কিন্তু খোঁজা হয়নি।)

### Next (recommended)

**Wave 4, Wave 5 ও Wave 6 সব বন্ধ** (25/25 · 31/31 · 59/59)। Attendance/Payroll spine complete — `8.3b-FE` was the last card.

#### 🔴 দুটো gate লাল, আর অনেক বেশি লাল (23 Aug 2026, PR #219 পর্যন্ত মাপা)

| Gate | 20 Aug | এখন |
|---|---|---|
| `tsc -b --force` | ১৫ error | 🔴 **৩৫ error** |
| `php artisan test` | 1 failed / 452 passed | 🔴 **36 failed / 609 passed** |

**Test — ১০টা class ভাঙা:**

| Class | # | কারণ |
|---|---:|---|
| `CorrectionRequestApiTest` | 13 | PR #212 correction fix — `ValidationException` |
| `PayrollRunControllerTest` | 7 | **`8.1-BE`-র নিজের test** — `Company::factory()` ডাকে, অথচ `Company`-তে `HasFactory` নেই আর `database/factories/`-এ কেবল `UserFactory.php`। এই ফাইল **কোনোদিন চলেনি** |
| `EmployeePersonalInfoIdAutoGeneratedTest` | 4 | PR #213 (employee ID uniqueness + DOB) |
| `EmployeePersonalInfoUpdateTest` | 3 | PR #213 |
| `AttendancePayrollApprovalIntegrationTest` | 2 | — |
| `PayrollRunStatusTest` | 2 | **`8.1-BE`-র নিজের test** — `@dataProvider` docblock, কিন্তু প্রকল্পে **PHPUnit ^12.5.12** যেখানে doc-comment metadata সরানো হয়েছে। `#[DataProvider]` attribute লাগবে |
| `MonthlyAttendanceApprovalServiceTest` | 2 | পুরনো `6.1-BE` contract ঋণ (আগে ১ ছিল) |
| `SalaryAdvanceApiTest` · `EmployeeSalaryPaymentAccountsTest` · `AttendanceCorrectionExecutorTest` | ১ করে | — |

**tsc-র ৩৫টার ভাগ:** `monthly-approval` ১২ (পুরনো `6.1-FE` ঋণ) · **`8.1-FE` ১০** (`PayrollRunCreatePage` 4 · `List` 2 · `Detail` 2 · `payrollRunApi.ts` 2) · `UserFormDrawer.tsx` 4 (PR #210 — `Property 'id' does not exist on type 'Company'`) · `DocumentsTab` 3 · `ImportUserModal` 3 · বাকি ছড়ানো।

> **এই নিয়ে পঞ্চমবার একটা FE card লাল build নিয়ে merge হলো** (`5.3-FE` ১৬ · `5.5-FE` ৬ · `6.1-FE` ১৫ · এখন `8.1-FE` ১০)। আর `8.1-BE` merge হয়েছে এমন অবস্থায় যেখানে **তার নিজের দুটো test ফাইলের একটাও চলতে পারে না** — অর্থাৎ card-টার BE আচরণ কার্যত অপরীক্ষিত।

#### কোন card গুলো এখন খোলা

| Card | Pts | অবস্থা |
|---|---|---|
| **8.3a-BE** Run Approval & Advance Settlement | 5 | ✅ done — `paid → settled` stamp এখানে |

বাকি pending: none। `8.3b-FE` · `8.3b-BE` · `8.3a-FE` done — Wave 6 বন্ধ।

> `8.2a-BE` attendance শুধু `getPayrollAttendanceForRun()` থেকে পড়ে; `attendance_records` / `monthly_attendance_approvals` calculator SQL-এ নেই (guard test)।

1. 🔴 **উপরের দুটো gate** — ৩৬টা লাল test আর ৩৫টা tsc error; নতুন card ধরার আগে এগুলো। সবচেয়ে সস্তা দুটো: `CompanyFactory` বানানো (৭টা test একসাথে সবুজ) আর `PayrollRunStatusTest`-এ `#[DataProvider]` (২টা)।
2. **`8.1-BE`-র নিজের test চালু করা** — card-টা done ধরা হয়েছে কোড আছে বলে, কিন্তু যাচাই নেই।
3. **`6.1-FE`-র ছয়টা DoD-ঋণ** — একটা নিছক ঋণ নয়, **নিরাপত্তার প্রশ্ন**: detail page-এর Approve **সবসময় `override: true`** পাঠায়, অর্থাৎ `6.1-BE`-র unresolved guard UI থেকে কার্যত বন্ধ।
4. **`9.4-FE`-র চারটে ঋণ** (নিচের নোট) — এখনো খোলা; "Fix Assignment" button কোনো filter লাগায় না।

#### 🔧 Platform — company context ঠিক করা হয়েছে (17 Aug 2026, PR #191)

Attendance/Payroll card নয়, **points অপরিবর্তিত** — কিন্তু সব lane-কে ছোঁয় বলে এখানে লেখা:

- Super admin এখন navbar-এর **company tab strip** দিয়ে যেকোনো active company-তে ঢুকতে পারে; আগে header নীরবে ফেলে দেওয়া হতো বলে context একটাই company-তে আটকে থাকত।
- **ভুল/stale `X-Company-Id` এখন 403** — আগে চুপচাপ caller-এর default company-তে fall back করত, অর্থাৎ ভুল tenant-এ write হয়ে যেতে পারত। **যেকোনো test যা company header পাঠায় সেটা এখন uuid-ই পাঠাতে হবে** (`EmployeeDeductionTest` company id পাঠাচ্ছিল, accidentally pass করত — সারানো হয়েছে)।
- Company switch করলে redux + react-query দুটোই reset হয় আর routed page remount হয়, তাই আর reload লাগে না।

বিস্তারিত এই ফাইলের শেষে তিনটে নোটে।

#### ~~5.3b-এর ঋণ~~ — শোধ হয়েছে (PR #186, 16 Aug 2026)

তিনটে DoD-ঋণই মিটেছে। যাচাই করা হয়েছে, নিচে প্রতিটার আগে-পরে:

| ঋণ | ছিল | এখন |
|---|---|---|
| **1. `calculateDaily` transaction-এ মোড়া নেই** | `AttendanceRecordService`-এ `DB::transaction` শূন্য; মাঝপথে ব্যর্থ হলে balance refund হয়ে যেত অথচ record লেখা হতো না | পুরো `calculateDaily` এখন `DB::transaction`-এ (`AttendanceRecordService.php:57`) |
| **2. `LeaveDayVoided` দুবার fire** | resolver-এ একবার, `AttendanceRecordService`-এর call site-এ আরেকবার | কেবল `LeaveDayResolverService.php:86`-এ একবার; service আর ছাড়ে না |
| **3. DoD-র চারটে test অনুপস্থিত** | `punched`/`voidLeaveDay` নিয়ে একটাও test ছিল না | `PunchLeaveVoidTest` — **7 test green (32 assertion)**, চারটে DoD case-ই ঢাকা |

**Re-entrancy-টা যেভাবে সামলানো হয়েছে:** `calculateDaily` transactional হওয়ায় ওর ভিতর থেকে `voidLeaveDay()` ডাকলে resolver আবার `calculateDaily` ডাকত — অসীম recursion। তাই `voidLeaveDay()`-তে তৃতীয় parameter `bool $recalculateAttendance = true` যোগ হয়েছে; `AttendanceRecordService.php:164` ওটা `false` দিয়ে ডাকে। Default `true` বলে **বাইরের caller-দের signature ভাঙেনি** — backward compatible।

> **ছোট leftover:** `AttendanceRecordService.php:10`-এ `LeaveDayVoided`-এর `use` statement এখনো আছে, অথচ ক্লাসটা আর ওখানে ব্যবহৃত হয় না। ক্ষতিকর নয়, পরের বার ঐ ফাইল ছুঁলে সরিয়ে দেবেন।
>
> এবং `5.3a`-র executor-এর নিজস্ব test-ও এখন আছে (`LeaveApprovalExecutionTest`, 4 test) — আগের নোটে "নেই" লেখা ছিল, সেটা আর সত্যি নয়।

> **7.4-FE merge হওয়ায় payroll Configuration section সম্পূর্ণ** — settings · salary structures · components · tax slabs · deductions সব `ready: true`। **7.5-FE** merged: Salary Advances একটা আলাদা nav item, Configuration tab নয়, আর `advance_enabled = false` হলে item ও route দুটোই থাকে না। পরের payroll FE = **`8.2-FE`** (`8.2a-BE`-র পর)। **`8.0-FE` done.**
> Attendance punch/leave/monthly track এই sequence অনুযায়ী **শেষ** (Waves 3–5 বন্ধ)।

**প্রতিটি সারি কীভাবে পড়বেন**

| ফিল্ড                  | মানে                                                              |
| ---------------------- | ----------------------------------------------------------------- |
| **Seq**                | বিল্ড অর্ডার। ছোট নম্বর আগে। একই wave = একসাথে parallel করা যায়। |
| **Status**             | `done` = merge/shipped · `pending` = এখনো বাকি।                   |
| **কেন এই phase**       | কার্ড এখানে কেন (কী unlock করে / কী লাগে)।                        |
| **সম্পর্কিত**          | Upstream blocker ও downstream consumer — existing cards থেকে।     |
| **Card `Start after`** | Task card থেকে হুবহু hard blocker।                                |

---

## শেষ — আবার বানাবেন না

| Cards                                  | Points | Status | নোট                                            |
| -------------------------------------- | ------ | ------ | ---------------------------------------------- |
| 0.1-BE · 0.1-FE · 0.2-BE · 0.3-BE      | 12     | done   | Modules, company timezone, permission registry |
| 1.1 / 1.2 / 1.3 / 1.4a / 1.4b / 1.4-FE | 37     | done   | Types, shifts, policies, assignments           |
| 2.1-BE · 2.1-FE · **2.2-BE**           | 15     | done   | Resolver + policy snapshot **service**         |
|                                        | **64** |        | Story 2.2-এর FE অংশ Seq 27-এ, সেটিও **done**   |

---

## Wave 1 — এখনই unblock (parallel)

নিচের সবগুলোর hard blocker ইতোমধ্যে merge হয়ে গেছে। Wave-এর ভিতরে যেকোনো অর্ডারে শুরু করা যায়।

| Seq    | Card                                | Pts | Status  | কেন এই phase                                                                                                                            | সম্পর্কিত                                                                     | Card `Start after`             |
| ------ | ----------------------------------- | --- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------ |
| **01** | **4.1-BE** Approval Integration     | 5   | done    | 5 executor stub + registry; demo seeder; gateway bypass/workflow wired। Executor **bodies** পরের cards (5.3a / 5.5 / 6.1 / 7.5 / 8.3a)। | Unlock করে **5.2, 5.3a, 5.4, 5.5, 6.1, 7.5, 8.3a**। UI = **4.1-FE** _(done)_। | 0.3-BE _(done)_                |
| **02** | **3.1-BE** Multi-Punch Check-In/Out | 5   | done    | Append-only `attendance_punches`; consecutive-type reject, overnight `attendance_date`, supersession filter, `punch-create-others` gate। | Unlock করে **3.1-FE** _(done)_ · **3.2-BE** _(done)_। `PunchCreated` → **3.2**। | 2.1-BE _(done)_                |
| **03** | **5.1-BE** Leave Balances           | 5   | done    | Balance + ledger tables; list/me/history/adjust API; derived available; unit tests।                                                     | Unlock করে **5.1-FE** _(done)_ · **5.2-BE** · **9.2-BE**।                     | 1.3-BE _(done)_ · stub 1.4a ok |
| **04** | **7.0-BE** Payroll Settings         | 3   | done    | Company payroll switch (advance enabled ইত্যাদি)।                                                                                       | Unlock করে **7.0-FE** _(done)_ · **7.5, 8.1/8.2**।                            | 0.1-BE _(done)_ · stub 0.3     |
| **05** | **7.1-BE** Salary Structures        | 3   | done    | Component-এর আগে structure header।                                                                                                      | Unlock করে **7.1-FE** _(done)_ · **7.2-BE** · **8.1**।                        | 0.1-BE _(done)_ · stub 0.3     |
| **06** | **7.3-BE** Tax Slabs                | 3   | done    | Slab config + calculate API।                                                                                                            | Unlock করে **7.3-FE** _(done)_; ব্যবহার করে **8.2a**।                         | 0.1-BE _(done)_ · stub 0.3     |
| **07** | **7.4-BE** Deductions & Loans       | 5   | done    | `employee_deductions` + `_entries` (`unique(deduction, run)`); ৬ endpoint; idempotent `applyInstallment`, negative-net skip, cancel/complete। **Idempotency ও skip test বাকি।** | Unlock করে **7.4-FE**; ব্যবহার করে **8.2a**।                                  | 0.1-BE _(done)_ · stub 0.3     |
| **08** | **7.6-BE** Payment Mode Split       | 5   | done    | Salary-তে cash/cheque/bank ভাগ।                                                                                                         | Unlock করে **7.6-FE** _(done)_; ব্যবহার করে **8.2b**।                         | 0.1-BE _(done)_ · stub 7.1     |
| **09** | **10.1-BE** Salary Field Validation | 1   | done    | Currency/frequency allow-list + active structure + gross > 0; unit/feature tests।                                                       | Payroll readiness / salary tab।                                               | nothing                        |
| **10** | **10.2-BE** Bank Account Validation | 1   | done    | OR-method রুল একটাই path-এ (validation service); single-primary model-level + row lock; partial update / `set-primary` 500 fix; bank column nullable। | Unlock করেছে **10.3-BE**; **8.3b**-এ যায়।                                     | nothing                        |
| **11** | **9.3-BE** Default Data Seeders     | 3   | done    | `AttendanceDefaultsService` (১১ system type + default `GENERAL` shift), **সব company-র জন্য**; নতুন tenant-এ `Company::created` hook; `firstOrCreate` তাই re-run duplicate করে না ও HR-এর name/color/icon ফেরায় না; ৬ test green। | ব্যবহার করে **1.1, 1.2**।                                                     | 1.1-BE · 1.2-BE _(done)_       |

**Wave 1 মোট: 39 pts** · **সবগুলো done (39)**

---

## Wave 2 — Wave 1-এর FE + পরের BE

| Seq    | Card                                      | Pts | Status  | কেন এই phase                                                                                                                                                              | সম্পর্কিত                                                               | Card `Start after` |
| ------ | ----------------------------------------- | --- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------ |
| **12** | **4.1-FE** Approval Settings Exposure     | 3   | done    | Core › Approval Settings filters + New highlight; `ApprovalStatusBadge` / `PendingApprovalAction`; requests list/detail reuse। Leave/run list badge → 5.x / 7.5 / 8.x FE। | **4.1-BE** _(done)_।                                                    | 4.1-BE _(done)_    |
| **13** | **3.1-FE** Punch UI                       | 3   | done    | `PunchWidget` + Employee Punch page; `punchApi.ts` (punch + punches list)।                                                                                                | **3.1-BE** _(done)_।                                                    | 3.1-BE _(done)_    |
| **14** | **5.1-FE** Leave Balance UI               | 3   | done    | Attendance › Leave › Balances grid + employee ledger; adjust modal; Dashboard My Balances card।                                                                           | **5.1-BE** _(done)_।                                                    | 5.1-BE _(done)_    |
| **15** | **7.0-FE** Payroll Settings UI            | 3   | done    | Payroll › Config › Settings (`ready: true`); `payrollSettingsApi.ts` + `usePayrollSettingsStore`; worked-example panel।                                                    | **7.0-BE** _(done)_।                                                    | 7.0-BE _(done)_    |
| **16** | **7.1-FE** Salary Structures UI           | 2   | done    | List + Offcanvas; Payroll › Configuration shell; System Config salary tab সরানো।                                                                                          | **7.1-BE** _(done)_। **7.2-FE** builder এখানে stacked Offcanvas-এ host। | 7.1-BE _(done)_    |
| **17** | **7.2-BE** Structure Components           | 5   | done    | `salary_structure_components` table; CRUD/reorder/preview; `SalaryComponentCalculator` shared path; activate unlock।                                                      | Unlock করে **7.2-FE** _(done)_; ব্যবহার করে **8.2a**।                   | 7.1-BE _(done)_    |
| **18** | **7.3-FE** Tax Slabs UI                   | 3   | done    | Payroll › Config › Tax Slabs list + form (`ready: true`); `taxSlabApi.ts`।                                                                                                | **7.3-BE** _(done)_।                                                    | 7.3-BE _(done)_    |
| **19** | **7.4-FE** Deductions & Loans UI          | 3   | done    | Payroll › Config › Deductions & Loans (`ready: true`); list + detail page (installment/entry timeline), `employeeDeductionApi.ts`।                                         | **7.4-BE** _(done)_।                                                    | 7.4-BE _(done)_    |
| **20** | **7.6-FE** Payment Mode Split UI          | 3   | done    | Employees › Salary tab › Payment split panel (`PaymentSplitPanel`).                                                                                                       | **7.6-BE** · **10.1** _(done)_।                                         | 7.6-BE _(done)_    |
| **21** | **10.3-BE** Missing Payment Method Report | 3   | done    | `GET /employees/without-primary-payment-method`; active employee + কোনো `is_primary` row নেই; department/branch/employment-status filter; কোনো migration/permission লাগেনি। | **8.3b** readiness `EmployeeMissingPaymentMethodReportServiceInterface` resolve করবে। | 10.2-BE _(done)_   |

**Wave 2 মোট: 31 pts** · done **31** · বাকি **0** — **Wave 2 সম্পূর্ণ**

---

## Wave 3 — Daily engine, leave apply, advances, তারপর records + applied policy

| Seq    | Card                                          | Pts | Status  | কেন এই phase                                                                                                                       | সম্পর্কিত                                                                                     | Card `Start after`                         |
| ------ | --------------------------------------------- | --- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------ |
| **22** | **3.2-BE** Daily Attendance Summary           | 8   | done    | `attendance_records` + calculation/recalculation service; today/list/show/punches/queued export; `NullLeaveDayResolverService` (5.3b বদলাবে)। **Status-ladder / pairing / visibility test বাকি।** | **3.1-BE** · **2.2-BE** _(done)_। Unlock করে **3.2-FE, 5.3b, 5.4, 5.5, 6.1, 9.1**। | 3.1-BE _(done)_ · 2.2-BE _(done)_          |
| **23** | **5.2-BE** Leave Application                  | 5   | done    | `leave_requests` + ৫ endpoint; approval gateway দিয়ে submit। **Munna-র draft-এ Siam যা ঠিক করেছেন:** working-day count ISO day-number-এ (আগে day-name মেলাত → প্রতিটা দিন non-working, `total_days` সবসময় 0); recurring holiday; preview ও store এক `evaluate()` path-এ (`LeaveEvaluation` DTO); preview আর business rule-এ throw করে না — `blockers`/`warnings`/`excluded_dates` ফেরত দেয় আর কিছু **লেখে না**; `document_required_after_days: 0` = কখনো লাগবে না; `show`/`index`-এর IDOR বন্ধ (অন্যের leave দেখতে `leave-approve` লাগে); `LeaveRequestResource` + paginated envelope; **নতুন `GET /leave-requests/applicable-policies`** — assign করা policy + balance দেয় (balance row না থাকলে entitlement), কারণ balance row তৈরি হয় ছুটি নেওয়ার পর — তাই balance দিয়ে dropdown বানালে নতুন employee কোনোদিন apply করতে পারত না। **32 test green** (আগে 11টার ১১টাই fail)। | **5.1, 2.1, 4.1** লাগে — সব done। Unlock করে **5.2-FE** _(done)_ · **5.3a**।                  | 5.1-BE _(done)_ · 2.1-BE · 4.1-BE _(done)_ |
| **24** | **7.2-FE** Structure Components UI            | 5   | done    | List/detail থেকে wide stacked Offcanvas — component list, add/edit, drag reorder, server-side payslip preview; `meta.code_locked`। | **7.2-BE** _(done)_।                                                                          | 7.2-BE _(done)_                            |
| **25** | **7.5-BE** Salary Advances                    | 8   | done    | PR #160। `salary_advances` + ৮ endpoint; server-resolved amount, frozen `basis_gross`, মাস-total ceiling (দুই cap); `SalaryAdvanceExecutor` body + দুই approval path; `SalaryAdvanceApproved`/`Paid`; ৪৪ test green। | **7.0 + 4.1** লাগে। Unlock করেছে **7.5-FE** _(done)_; ব্যবহার করে **8.2b / 8.3a**।             | 7.0-BE _(done)_ · 4.1-BE _(done)_          |
| **26** | **3.2-FE** Daily Summary / Attendance Records | 5   | done    | PR #165। Attendance › Attendance Records — record grid (`AttendanceRecordListPage`) + detail view; nav `/attendance/attendance-records` live। **2.2-FE**-র applied-policy panel এই detail view-তেই বসেছে; **5.4-FE**-র entry point এখন প্রস্তুত। | **3.2-BE** _(done)_। **2.2-FE** panel host করে; **5.4-FE** entry।                             | 3.2-BE _(done)_                            |
| **27** | **2.2-FE** Applied Policy Panel               | 2   | done    | PR #182। Panel-টা `AttendanceRecordDetailView`-এ, snapshot-এর পুরো ফিল্ড সেট সহ — grace · working hours · min-hours present/half-day · working days · timezone · `resolved_at`, প্রতিটাই snapshot না থাকলে live shift-এ fallback করে। **Recalculate split control** এসেছে `attendance.record-recalculate` permission gate + `RecalculateConfirmationModal`-এ "CONFIRM" টাইপ করার শর্ত সহ, আর locked record-এ control-টা render-ই হয় না। BE-তেও `AttendanceRecalculationService` + `RecalculationUnauthorizedException` বদলেছে। **Story 2.2 সম্পূর্ণ।** | **2.2-BE** + **3.2-FE** _(done)_। Story **2.2** এখানে সম্পূর্ণ হলো।                          | 2.2-BE · **3.2-FE** _(done)_               |
| **28** | **5.2-FE** Leave Application UI               | 5   | done    | `leaveRequestApi.ts` (debounced preview) · routes `/attendance/leave/requests[/new\|/:id]` · nav **Attendance › Leave › Requests** (`attendance.leave-apply`)। Server-owned working-day count, before/after balance panel, greyed holidays/non-working days (`preview.excluded_dates` থেকে — মাসের calendar probe-ও একই endpoint), LWP warn vs non-LWP block, auto document requirement, pending-only cancel। | **5.2-BE** _(done)_-এর সাথে।                                                                  | 5.2-BE _(done)_                            |
| **29** | **7.5-FE** Salary Advances UI                 | 5   | done    | PR #161। `salaryAdvanceApi.ts`; **single-screen — সব operation stacked Offcanvas-এ, page change নেই** (request · detail · record-payment · cancel-confirm); list-এ filter + period-total; nav ও route দুই গেটে — `payroll.advance-manage` **এবং** `advance_enabled`। | **7.5-BE** _(done)_-এর সাথে।                                                                  | 7.5-BE _(done)_                            |

**Wave 3 মোট: 43 pts** · done **43** · বাকি **0** — **Wave 3 সম্পূর্ণ**

---

## Wave 4 — Leave/correction approval + punch-voids

| Seq    | Card                              | Pts | Status  | কেন এই phase                                       | সম্পর্কিত                                   | Card `Start after`                           |
| ------ | --------------------------------- | --- | ------- | -------------------------------------------------- | ------------------------------------------- | -------------------------------------------- |
| **30** | **5.3a-BE** Leave Approval        | 5   | done    | PR #180। `LeaveRequestExecutor` সম্পূর্ণ: request + balance দুটোই `lockForUpdate`, execution-time-এ balance পুনঃযাচাই (LWP allowed হলে ছাড়), `consumption` ledger row, প্রতি counted working day-তে `leave_request_days` row, আর leave type stamp — holiday ও shift working-day filter 5.2-র মতোই। `sum(day_value) != total_days` হলে 422। Guard: already finalized → 409, approved leave-এর সাথে overlap → 409, locked month → 409। `LeaveDecided` emit হয়। Cancel path (`LeaveRequestService::cancel`) day row void করে, `reversal` ledger লেখে, ঐ দিনগুলো recalculate করায়। **`leave_request_days` migration** `voided_reason`/`voided_at` সহ আছে। | **5.2 + 4.1** _(done)_। Unlock করে **5.3-FE, 5.3b**। | 5.2-BE _(done)_ · 4.1-BE _(done)_            |
| **31** | **5.4-BE** Correction Request     | 3   | done    | PR #163। `correction_requests` + store/index/show/cancel; duplicate guard; policy + approval executor; 7 test green। Unlock করেছে **5.4-FE, 5.5-BE**। | **3.2 + 4.1** _(done)_। Unlock করে **5.4-FE, 5.5**। | 3.2-BE _(done)_ · 4.1-BE _(done)_    |
| **32** | **5.3-FE** Leave Approvals UI     | 3   | done ⚠  | PR #188। Queue (status/employee/policy/from/to — চারটে filter), detail, reject modal। Card-এর কঠিন rule গুলো সত্যিই আছে: balance detail খুললে **নতুন করে fetch** হয়, `disabled={isBalanceLoading \|\| isInsufficient \|\| ...}` বলে stale figure-এ approve করা যায় না, reject reason `trim()` করে খালি হলে আটকায়, day breakdown-এ `voided-by-punch` + void তারিখ দেখায়। **✅ build-ঋণ শোধ (17 Aug 2026): `tsc -b` ০ error, Refresh button render হয়, আর day breakdown-টা runtime-এও সত্যিই আসে** — নিচের নোট দেখুন। | **5.3a** _(done)_-এর সাথে।         | 5.3a-BE _(done)_ · 5.3b _(done)_ · 4.1-FE _(done)_ |
| **33** | **5.4-FE** Correction Request UI  | 3   | done    | PR #181। `pages/corrections/` — list · detail · form; `components/corrections/CorrectionRequestForm` + `CorrectionDatePicker`; `correctionRequestApi.ts` + types; nav **Attendance › Corrections**। Record detail view থেকেও entry। BE-তে `CorrectionRequestService` সামান্য বদল + নতুন `CorrectionRequestApiTest`। | **5.4-BE + 3.2-FE** _(done)_। Unlock করে **5.5-BE**। | 5.4-BE _(done)_ · 3.2-FE _(done)_            |
| **34** | **5.3b-BE** Punch Voids Leave     | 3   | done    | আচরণটা 5.3a-র PR-এ (#180) ঢুকেছিল, **তিনটে DoD-ঋণ শোধ হয়েছে PR #186-এ** (Munna)। `AttendanceServiceProvider` আসল `LeaveDayResolverService` bind করে, আর `calculateDaily`-তে R2 branch card-এর pseudocode মেনে বসেছে — full-day leave বা `working_hours >= min_hours_present` হলে `voidLeaveDay(day, 'punched')`, নইলে half-day carve-out। **এখন `calculateDaily` পুরোটা `DB::transaction`-এ**, `LeaveDayVoided` একবারই fire হয় (resolver-এ), আর `voidLeaveDay()`-তে নতুন `$recalculateAttendance` flag এসেছে যাতে transaction-এর ভিতর থেকে ডাকলে recursion না হয়। **`PunchLeaveVoidTest` — 7 test green (32 assertion)**, DoD-র চারটে case-ই ঢাকা। | **5.3a + 3.2** _(done)_।           | 5.3a-BE _(done)_ · 3.2-BE _(done)_           |
| **35** | **5.5-BE** Correction Approval    | 5   | done    | PR #185 (Bablu)। `AttendanceCorrectionExecutor` পাঁচটা request type-ই সামলায় — `missing_in` · `missing_out` · `incorrect_time` · `wrong_status` · `other`। **কোনো punch row কখনো update হয় না, মোছেও না** — কেবল `superseded_by_id` বসে, নতুন manual punch যোগ হয়, superseded punch recalculation থেকে বাদ পড়ে। Locked month: `attendance.correction-override-lock` ছাড়া 403, থাকলে পাশ। Reject-এ reason বাধ্যতামূলক (নইলে 422) আর কোনো punch/record ছোঁয়া হয় না। `CorrectionDecided` emit হয়; finalised request দ্বিতীয়বার approve/reject করলে আটকায়। `index`/`show` এখন `correction-create\|correction-approve` দুটোর যেকোনো একটিতে খোলে (আগে শুধু requester দেখত, HR queue দেখতে পেত না)। **15 test green (64 assertion)** — কার্ডের তিনটে DoD item-ই ঢাকা। **⚠ cross-lane gate ভাঙা হয়েছে — নিচে দেখুন।** | **5.4 + 3.2 + 4.1** _(সব done)_।            | 5.4-BE _(done)_ · 3.2-BE _(done)_ · 4.1-BE _(done)_ · 3.1 _(done)_ |
| **36** | **5.5-FE** Correction Approval UI | 3   | done ⚠  | PR #189 (Bablu)। `CorrectionApprovalListPage` (HR queue) + `CorrectionApprovalDetailPage`, সাথে `DayComparisonPanel` (আগে/পরে projection), `LockedMonthBanner`, `RejectCorrectionModal`; `correctionProjection.ts`-এ projection logic; nav-এ **Attendance › Correction Approvals**। BE-তে `index`/`show` approver-scope + `CorrectionRequestResource` সমৃদ্ধ হয়েছে। **⚠ `tsc -b` ভেঙে merged — ঐ এক ফাইলে ৬ error, নিচের blocker দেখুন।** | **5.5-BE** _(done)_।                        | 5.5-BE _(done)_ · 4.1-FE _(done)_            |

**Wave 4 মোট: 25 pts** · done **25** — **Wave 4 বন্ধ** (5.3a · 5.4-BE · 5.3-FE · 5.4-FE · 5.3b · 5.5-BE · 5.5-FE)

---

## Wave 5 — Monthly close + jobs

| Seq    | Card                                     | Pts | Status  | কেন এই phase                                       | সম্পর্কিত                                       | Card `Start after`                         |
| ------ | ---------------------------------------- | --- | ------- | -------------------------------------------------- | ----------------------------------------------- | ------------------------------------------ |
| **37** | **6.1-BE** Monthly Attendance Approval   | 8   | done    | PR #192। HR মাস বন্ধ করে — ৯টা endpoint (list · show · breakdown · build · approve · bulk-approve · unlock), `MonthlyAttendanceApproval` model + `MonthlyAttendanceExecutor`। Override করে approve করলে activity log-এ যায়। **`MonthlyAttendanceApprovalServiceTest` 3 + `MonthlyAttendanceExecutorTest` green।** | **3.2 + 4.1** _(done)_। Unlock করে **6.1-FE, 6.2**। | 3.2-BE _(done)_ · 4.1-BE _(done)_ · stub 5.3a / 5.5 |
| **38** | **6.1-FE** Monthly Approval UI           | 5   | done ⚠ | PR #198 (Bablu)। `/attendance/monthly-approval` list + `/{id}` detail, nav ও home card দুটোই এখন `attendance.monthly-view`-এ পয়েন্ট করে; `monthlyAttendanceApi.ts`, month/year/status/search filter, row selection + **bulk approve queued batch** (১ সেকেন্ডে poll, progress bar, succeeded/failed আলাদা result table)। BE-তেও বড় বদল: `build` ও `bulk-approve` এখন **202 queued** (নতুন দুটো job + cache-ভিত্তিক batch status + `GET /monthly-attendance/batch/{batchId}`), list-এ `search` filter, department filter `designation` ছেড়ে `currentOrganizationAssignment`-এ। **⚠ দুটো gate ভাঙা + ছয়টা DoD-ঋণ — ফাইলের শেষের নোট দেখুন।** | **6.1-BE**-এর সাথে। | 6.1-BE _(done)_ · 4.1-FE _(done)_ |
| **39** | **6.2-BE** Monthly Freeze                | 3   | done    | PR #195। Finance lock, payroll run-এর hard gate; `MonthlyAttendanceFreezeService` + freeze/unfreeze endpoint, `MonthFrozen` event। `MonthlyAttendanceFreezeTest` green। | **6.1**-এর পর। Unlock করে **6.2-FE, 8.1, 8.0**। | 6.1-BE                                     |
| **40** | **6.2-FE** Freeze UI                     | 2   | done    | Payroll › Monthly Freeze।                          | **6.2-BE**-এর সাথে।                             | 6.2-BE · stub 6.1-FE                       |
| **41** | **9.1-BE** Nightly Attendance Close      | 5   | done ⚠ | PR #187 (Ashraful)। `ScheduleCloseDayCommand` hourly চলে আর প্রতিটা active company-র **নিজের timezone-এ `hour === 2`** হলে batch ছাড়ে; `CloseDayService` employee-দের ২০০-র chunk-এ `ProcessCloseDayChunkJob`-এ ভাগ করে, প্রতিটায় `calculateDaily`। **Idempotency নকশাতেই আছে** — `chunkActiveWithoutAttendance()` কেবল সেইসব employee তোলে যাদের ঐ তারিখে record নেই। Manual trigger + status: `POST /jobs/close-day`, `GET /jobs/close-day/{batchId}`। Locked period হলে skip। **⚠ তিনটে DoD-ঋণ — দেখুন নিচে।** | **3.2** _(done)_-এর পর।            | 3.2-BE _(done)_                            |
| **42** | **9.2-BE** Leave Accrual & Carry-Forward | 5   | done    | PR #190 (Munna)। `ScheduleLeaveAccrualCommand` + `ScheduleLeaveCarryForwardCommand`, chunk job দুটো (`ProcessLeaveAccrualChunkJob` · `ProcessCarryForwardChunkJob`), manual trigger `POST /leave-balances/accrue` · `/carry-forward` + batch status, `LeaveBalanceService`-এ accrual/carry-forward logic। **`LeaveAccrualJobTest` ও `LeaveBalanceServiceTest` green** — তবে দুটোই module suite-এ, যেটা default run-এ ঢোকে না (নিচের blocker দেখুন)। **এটাই `9.4-FE` unlock করেছে।** | **5.1**-এর পর _(done)_।                         | 5.1-BE _(done)_ · stub 1.3                 |
| **43** | **9.4-FE** Attendance Jobs UI            | 3   | done ⚠ | PR #197 (Munna)। `/attendance/jobs`, nav **Attendance › Jobs**; `attendanceJobApi.ts` (close-day · accrue-leave · carry-forward · batch status) + `useAttendanceJobs` polling hook — ২ সেকেন্ডে poll, finished/failed/cancelled-এ থামে, batch id `sessionStorage` ও `?batch=` দুটোতেই রাখে বলে page ছেড়ে ফিরলে progress ফেরে। Unassigned report-ই screen-এর primary content; permission না থাকলে manual run panel render-ই হয় না। সাথে BE-তে monthly accrual-এ **annual entitlement cap guard** + `applied` return, আর accrual/carry-forward chunk job এখন activity log লেখে (এটাই status panel-এর উৎস)। **⚠ ৪ DoD-ঋণ — ফাইলের শেষের নোট দেখুন।** | **9.1 + 9.2** _(done)_। | 9.1-BE _(done)_ · 9.2-BE _(done)_ |

**Wave 5 মোট: 31 pts** · done **31** _(9.1-BE · 9.2-BE · 6.1-BE · 6.2-BE · 9.4-FE · 6.1-FE · 6.2-FE)_ · বাকি **0** — **Wave 5 বন্ধ**

---

## Wave 6 — Payroll execution

| Seq    | Card                                                | Pts | Status  | কেন এই phase                                                                | সম্পর্কিত                                  | Card `Start after`                   |
| ------ | --------------------------------------------------- | --- | ------- | --------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------ |
| **44** | **8.1-BE** Payroll Run Creation                     | 5   | done ⚠  | মাস freezeযোগ্য হওয়ার পর run header।                                       | **6.2** লাগে; **7.0/7.1** stub ok।         | 6.2-BE · stub 7.0 / 7.1              |
| **45** | **8.1-FE** Payroll Run UI                           | 3   | done ⚠  | Run তৈরি/তালিকা।                                                            | **8.1-BE**-এর সাথে।                        | 8.1-BE · stub 6.2-FE                 |
| **46** | **8.0-BE** Attendance Snapshot for Payroll          | 5   | done    | Run-এ attendance input freeze (story নম্বর ইচ্ছাকৃত — 8.1-এর পরে)।          | **8.1 + 6.2**।                             | 8.1-BE · 6.2-BE                      |
| **47** | **8.0-FE** Snapshot UI                              | 2   | ✅ done | Run-scoped list/detail; draft-only build; freeze pre-flight; diagnostic divergence। | **8.0-BE**-এর সাথে।                        | 8.0-BE · stub 8.1-FE                 |
| **48** | **8.2a-BE** Payslip Generation                      | 8   | ✅ done | মূল payslip pipeline; অস্থায়ী `net_payable = net_pay`।                     | **8.0** লাগে। Unlock করে **8.2b, 8.2-FE**। | 8.0-BE · stub 7.0 / 7.2 / 7.3 / 7.4  |
| **49** | **8.2b-BE** Advance Settlement & Payment Allocation | 5   | ✅ done | Advance নেট + channel ভাগ। শুধু `paid` advance নেট; draft generation কোনো `salary_advances` row ছোঁয় না। | **8.2a**-এর পর; **7.5** ও **7.6** দুটোই _(done)_ | 8.2a-BE · 7.5-BE _(done)_ · 7.6-BE _(done)_ |
| **50** | **8.2-FE** Payslips UI                              | 8   | ✅ done | Run payslips + My Payslips + detail; frozen allocations; settlement ৩ লাইন; generate/poll/retry। | **8.2a**; **8.2b** landed।                | 8.2a-BE · 8.2b-BE                  |
| **51** | **8.3a-BE** Run Approval & Advance Settlement       | 5   | ✅ done | `PayrollRunExecutor` · paid→settled · carry-forward · `approval-summary` · bypass/workflow · **9** test। | **8.2b + 4.1**।                            | 8.2b-BE · 4.1-BE _(done)_ · 7.5-BE _(done)_ |
| **52** | **8.3a-FE** Run Approval UI                         | 5   | ✅ done | Decision screen · platform approve/reject · approval-summary · Net Pay≠Net Payable · flagged list · settlement confirm। | **8.3a-BE**-এর সাথে।                       | 8.3a-BE · 4.1-FE _(done)_            |
| **53** | **8.3b-BE** Multi-Channel Disbursement              | 8   | ✅ done | `disbursement_batches`/`items` · allocation-driven · channel-aware readiness · send/confirm/retry · acknowledge · export · **18** test। | **8.3a + 8.2b**।                           | 8.3a-BE · **8.2b-BE**                |
| **54** | **8.3b-FE** Disbursement UI                         | 5   | ✅ done | Batch ও register স্ক্রিন · channel summary + readiness · A4 print stylesheet। | **8.3b-BE**-এর সাথে।                       | 8.3b-BE · stub 8.3a-FE               |

**Wave 6 মোট: 59 pts** · done **59** _(… · 8.3a-BE · 8.3a-FE · 8.3b-BE · 8.3b-FE)_ · বাকি **0** — Wave 6 বন্ধ।

**কঠোর নিয়ম (cards + spec থেকে):** Seq **49 (`8.2b-BE`)** আগে, তারপর Seq **53 (`8.3b-BE`)**।

---

## এক পাতার চেকলিস্ট (কপি/পেস্ট)

```
01 4.1-BE     done
02 3.1-BE     done
03 5.1-BE     done
04 7.0-BE     done
05 7.1-BE     done
06 7.3-BE     done
07 7.4-BE     done      ← idempotency + negative-net test বাকি
08 7.6-BE     done
09 10.1-BE    done
10 10.2-BE    done
11 9.3-BE     done      ← per-company + Company::created hook

12 4.1-FE     done
13 3.1-FE     done
14 5.1-FE     done
15 7.0-FE     done
16 7.1-FE     done
17 7.2-BE     done
18 7.3-FE     done
19 7.4-FE     done
20 7.6-FE     done
21 10.3-BE    done      ← 8.3b-BE-র readiness গেট খুলে গেছে

22 3.2-BE     done      ← status-ladder / pairing / visibility test বাকি
23 5.2-BE     done      ← Munna draft + Siam fix (working-day · IDOR · preview contract)
24 7.2-FE     done
25 7.5-BE     done      ← PR #160
26 3.2-FE     done      ← PR #165; 2.2-FE panel + 5.4-FE entry এর host
27 2.2-FE     done      ← PR #182; snapshot ফিল্ড সেট + recalculate control — Wave 3 বন্ধ
28 5.2-FE     done
29 7.5-FE     done      ← PR #161

30 5.3a-BE    done      ← PR #180; executor + cancel path
31 5.4-BE     done      ← PR #163
32 5.3-FE     done      ← PR #188; ✅ build-ঋণ শোধ: tsc -b সবুজ + Refresh button render হয়
33 5.4-FE     done      ← PR #181
34 5.3b-BE    done      ← PR #186; তিনটে DoD-ঋণ শোধ, 7 test green
35 5.5-BE     done      ← PR #185; executor + supersede + locked-month override
36 5.5-FE     done ⚠    ← PR #189; queue + detail + day comparison — কিন্তু tsc -b ভেঙে merged

37 6.1-BE     done      ← PR #192; 9 endpoint, 3 test green
38 6.1-FE     done ⚠    ← PR #198; build লাল + ১টা test লাল, ৬ DoD-ঋণ
39 6.2-BE     done      ← PR #195; freeze/unfreeze + MonthFrozen
40 6.2-FE     done      ← `1a93334f` "attendance freez" + `36d66df6` list fix — Wave 5 বন্ধ
41 9.1-BE     done      ← PR #187; তিনটে DoD-ঋণ শোধ (18 Aug), 4 test green
42 9.2-BE     done      ← PR #190; accrual + carry-forward command/job/endpoint
43 9.4-FE     done ⚠    ← PR #197; jobs screen live, ৪ DoD-ঋণ

44 8.1-BE     done ⚠    ← `6fcd9218` + PR #215 fix; নিজের দুটো test ফাইলই চলে না
45 8.1-FE     done ⚠    ← PR #209; tsc-তে ১০ নতুন error
46 8.0-BE     done      ← attendance_snapshots + build/list/show/divergence; 17 test সবুজ
47 8.0-FE     done      ← attendanceSnapshotApi + list/detail; draft-only build; freeze `employee_id`; diagnostic copy + contrast
48 8.2a-BE    done      ← payslips + queued generate + Finaliser pass-through; 19 test সবুজ
49 8.2b-BE    done      ← paid advance net + PaymentAllocator freeze; allocations GET; 41 related test সবুজ
50 8.2-FE     done      ← run list/generate/poll · detail settlement+allocations · My Payslips
51 8.3a-BE    done        ← PayrollRunExecutor; paid→settled; carry-forward; approval-summary; 9 test
52 8.3a-FE    done
53 8.3b-BE    done
54 8.3b-FE    done        ← disbursementApi · list/detail · A4 register print · nav Payroll › Disbursement
```

---

## মোট হিসাব

|                                                                                                                                 | Cards | Points | Status  |
| ------------------------------------------------------------------------------------------------------------------------------- | ----- | ------ | ------- |
| শেষ (বেসলাইন)                                                                                                                   | ~16   | ~64    | done    |
| Wave done (Seq 01–47-এর সব `done` সারি)                                                                                         | 47    | 184    | done    |
| বাকি (Seq 48–54-এর `pending` সারি)                                                                                              | 7     | 44     | pending |
| পুরো সিস্টেম                                                                                                                    | 70    | 292    |         |

> এক উৎস থেকে গোনা: **done 50 card / 205 pts · বাকি 4 card / 23 pts**। যোগ মিলিয়ে দেখা — Wave breakdown 39+31+43+25+31+36 = **205**, আর 205 + 23 = 228 pts (বেসলাইনের ~64 বাদে)।

---

## Dependency ডায়াগ্রাম (শুধু critical path)

```mermaid
flowchart LR
  subgraph done [শেষ]
    B21[2.1-BE]
    B22[2.2-BE]
    C31[3.1-BE]
    C31F[3.1-FE]
    C32[3.2-BE]
    A41[4.1-BE]
    A41F[4.1-FE]
    E51[5.1-BE]
    E51F[5.1-FE]
    P70[7.0-BE]
    P70F[7.0-FE]
    P71[7.1-BE]
    P71F[7.1-FE]
    P72[7.2-BE]
    P72F[7.2-FE]
    P73[7.3-BE]
    P73F[7.3-FE]
    P74[7.4-BE]
    P74F[7.4-FE]
    P76[7.6-BE]
    P76F[7.6-FE]
    E101[10.1-BE]
    E102[10.2-BE]
    H81[8.1-BE]
    H81F[8.1-FE]
    H80[8.0-BE]
    H80F[8.0-FE]
  end

  P71 --> P71F
  P71 --> P72
  P72 --> P72F
  P70 --> P70F
  P73 --> P73F
  P74 --> P74F
  P76 --> P76F
  A41 --> A41F
  E51 --> E51F
  C31 --> C31F
  E102 --> E103[10.3-BE]

  C32F[3.2-FE]
  B22F[2.2-FE]
  E52[5.2-BE]
  E53a[5.3a-BE]
  E53b[5.3b-BE]
  E54[5.4-BE]
  F61[6.1-BE]
  F62[6.2-BE]
  J91[9.1-BE]
  H82a[8.2a-BE]
  H82b[8.2b-BE]
  H83a[8.3a-BE]
  H83b[8.3b-BE]

  B21 --> C31
  C31 --> C32
  B22 --> C32
  C32 --> C32F
  C32 --> E54
  C32 --> J91
  C32F --> B22F
  A41 --> E52
  E51 --> E52
  E52 --> E53a
  E53a --> E53b
  C32 --> E53b
  C32 --> F61
  A41 --> F61
  F61 --> F62
  F62 --> H81
  H81 --> H81F
  H81 --> H80
  H80 --> H80F
  H80 --> H82a
  H82a --> H82b
  H82b --> H83a
  H83a --> H83b
  H82b --> H83b
```

---

## নোট

1. **Card-এর বিস্তারিত থাকে** `ATTENDANCE_PAYROLL_TASK_CARDS.md`-এ — এই ফাইল শুধু execution order + কারণ ব্যাখ্যা।
2. **Parallel wave** মানে hard-blocker সেট ইতোমধ্যে পূরণ; wave-এর ভিতরেও প্রতিটি card-এর নিজস্ব `Start after` মানতে হবে।
3. **`Also needs (can be stubbed)`** মানে fake দিয়ে আগে শুরু করা যায়, কিন্তু merge-এর আগে আসল dependency জোড়াতে হবে — উপরের sequence শুধু hard blocker দিয়ে সাজানো।
4. Board-এ “Story 2.2” — **2.2-BE এবং 2.2-FE দুটোই merged** (Seq 27, PR #182), তাই **complete মার্ক করা যাবে**।
5. **Recheck (24 Aug 2026, কোডবেস):** Wave 6 হিসাব মিলিয়ে দেখা — done **15/59** (`8.1-BE` 5 + `8.1-FE` 3 + `8.0-BE` 5 + `8.0-FE` 2)। Seq টেবিলের ফুটার আগে **13/46** লেখা ছিল (`8.0-FE` বাদ) — সেটা ভুল, Seq 47 done। **`8.0-BE`** — `attendance_snapshots` + `AttendanceSnapshotService` (build/list/show/divergence + `getPayrollAttendanceForRun`) + `AttendanceSnapshotTest` (**১৭** test)। **`8.0-FE`** — `attendanceSnapshotApi.ts`; routes `/payroll/payroll-runs/:id/snapshots` ও `…/:employeeId` (`payroll.run-create` group); run detail Quick Link; **nav-এ আলাদা item নেই**। List: BE `PAYROLL_FIELDS` কলাম + `snapshot_taken_at` + diagnostic badge। Detail: captured fields + “Attendance divergence detected” + snapshot/live contrast। Build শুধু `run.status === 'draft'`। Pre-flight লিংক `/payroll/monthly-freeze?month=&year=&employee_id=` — freeze **list API**-তে `employee_id` ফিল্টার আছে (`MonthlyAttendanceFreezeService` + `MonthlyAttendanceFreezeController` + `test_index_filters_by_employee_id`)। **`8.2a-BE` pending** — `Payslip` class নেই; `payroll_payslips` টেবিল নেই (শুধু deduction-entries migration-এ `hasTable` গার্ড)। **FE টেস্ট রানার নেই** (`package.json`-এ vitest/jest নেই)। Card-এর `payroll.payslip-view-all` snapshot routes-এ **BE লাগায় না** — FE BE অনুসরণ (`payroll.run-create`)। Targeted test (24 Aug): `AttendanceSnapshotTest` + `MonthlyAttendanceFreezeTest` **33 passed**।
6. **Recheck (23 Aug 2026, কোডবেস):** **`8.0-BE` done** — snapshot pipeline + ১৭ test। **`8.0-FE` done** (প্রথম কাট) — list/detail + draft build + 409/404 pre-flight। Wave 6 done = 15 pts।
7. **Recheck (10 Aug 2026, দ্বিতীয় পাস):** কোড যাচাই করে **3.1-BE · 3.1-FE · 3.2-BE · 7.0-FE · 7.3-FE · 7.4-BE** → done করা হলো।
   - **3.1-BE** — `attendance_punches` migration (append-only, `created_at` only), `PunchService` (consecutive-type reject · overnight `attendance_date` · `findLastNonSuperseded` · manual `punch_time` কেবল `punch-create-others` হলে), `PunchCreated` event, `PunchApiTest` (১০ কেস) + `PunchServiceTest`, `api collection/Attendance/Punch/*.yml`. কোনো update/delete endpoint নেই — DoD অনুযায়ী সঠিক।
   - **7.4-BE** — দুইটা migration (`unique(employee_deduction_id, payroll_run_id)` = `deduction_run_unique`), ছয়টা রুট `payroll.deduction-manage`-এর নিচে, `applyInstallment` reverse-then-reapply + completed transition + `flagForReview`, `api collection/Payroll/Employee Deductions/*.bru`.
   - **বাকি DoD gap:** 7.4-BE-তে idempotency ও negative-net-pay test নেই; 3.2-BE-তে status-ladder / session-pairing / visibility test নেই। **8.2a-BE** শুরুর আগে ধরা দরকার।
   - **10.2-BE** — এর পরে merge হয়েছে: OR-method রুল একটাই validation path-এ, single-primary model-level + row lock, partial update / `set-primary` 500 fix, bank column nullable; 36 test green। ফলে **10.3-BE** unblocked।
   - **7.4-FE** — PR #157-এ merge (Bablu)। Config nav-এ `deductions: ready: true`, list + detail page, `employeeDeductionApi.ts`। এতে payroll **Configuration section সম্পূর্ণ** — Wave 2-এ এখন শুধু **10.3-BE** বাকি।
   - **10.3-BE Missing Payment Method Report (10 Aug 2026):** `GET /api/v1/employee/employees/without-primary-payment-method`, permission `payroll.disburse|employee.menu-view` (দুটোই আগে থেকেই seed করা — নতুন permission key লাগেনি)। "কোনো primary payment method নেই" রুলটা একটাই জায়গায়: `EmployeePersonalInfoRepository::withoutPrimaryPaymentMethodQuery()` — একটা `NOT EXISTS`, `employee_bank_accounts(employee_id, is_primary)` index-এ পড়ে, তাই **কোনো migration লাগেনি**। Filter: `department_id`, `branch_id`, `employment_status`, `search`; base সেট = `employee_personal_infos.status = 'active'` (soft-deleted বাদ)। Primary row-টা bank না mobile banking তাতে কিছু যায় আসে না — row থাকলেই employee list-এ নেই। 17 test green, full suite-এ নতুন regression নেই।
     - **8.3b-BE-র জন্য seam:** `EmployeeMissingPaymentMethodReportServiceInterface`-এ `paginate()` (HTTP) আর `employeeIdsMissingPaymentMethod(companyId, employeeIds)` (readiness) — দুটোই ওই একই private query share করে। Payroll module এতদিন Employee-র শুধু Model ছুঁত; এটাই প্রথম published service contract। **8.3b readiness এই contract resolve করবে, query copy করবে না।**
     - **Tenant guard দ্বিগুণ:** employee query company-scoped, আর `NOT EXISTS` closure-এও `company_id` — অন্য company-র primary row কারও gap লুকাতে পারবে না (test দিয়ে ধরা)।
8. **7.5-BE Salary Advances (10 Aug 2026, PR #160 merged):** `salary_advances` migration; ৮টা endpoint `payroll.advance-manage`-এর নিচে; `AdvanceCeilingCalculator` (pure, DB-মুক্ত — create ও `summary` একই অঙ্ক ব্যবহার করে); `SalaryAdvanceExecutor` body + registry (bypass ও workflow দুটো path-ই executor-মুখী `SalaryAdvanceService::approve()`-এ মেলে, idempotent); `SalaryAdvanceApproved` / `SalaryAdvancePaid` (listener নেই); `api collection/Payroll/Salary Advances/*.bru`। ৪৪ test green, full suite-এ নতুন regression নেই।
   - **`payroll_runs` এখন আছে (`8.1-BE` done)** — আগে `PayrollPeriodGuard` `Schema::hasTable` দিয়ে stub করত; run টেবিল merge-এর পর ওই guard আসল query-তে বসার কথা।
   - **ইচ্ছাকৃতভাবে period-guard মুক্ত:** `submit` ও `record-payment`। card-এর রুল "created, edited, cancelled" পর্যন্তই — approved-but-unpaid advance পরে pay করা গেলেই তবে সেটা পরের run-এ carry করতে পারে।
   - **খোলা gap:** workflow reject হলে `rejected` status কেউ সেট করে না — platform reject path-এ executor/event কিছুই fire করে না (card-এও reject flow নেই)। HR cancel করতে পারে। Platform-এ hook লাগলে আলাদা card।
9. **7.5-FE Salary Advances UI (10 Aug 2026, PR #161 merged):** `modules/payroll/api/salaryAdvanceApi.ts`; **পুরোটা একটাই স্ক্রিন** — list page (employee / month-year / status filter + period-total সারি), আর প্রতিটি operation shared `Offcanvas`-এ stack হয়ে খোলে: request drawer · detail drawer · record-payment drawer · cancel confirm। Status অনুযায়ী action render (draft → edit + submit, approved → record-payment, paid → settlement waiting, settled → payslip link)। Money format সর্বত্র shared `formatMoney` (`payroll/utils/allocatePayment.ts`) — নতুন local helper লেখা হয়নি। `ApprovalStatusBadge` reuse (draft…settled সব status ওতে আগেই ছিল)।
   - **`/salary-advances/:id` কোনো আলাদা page নয়** — একই list page render করে, id শুধু detail offcanvas খোলে। ফলে deep link কাজ করে অথচ কোথাও navigate হয় না; panel বন্ধ করলে URL list-এ ফিরে যায়।
   - **Cancel confirm-এ shared `ConfirmDialog` ব্যবহার করা যায়নি:** ওটা Bootstrap modal (z-index 1055), আর দ্বিতীয় stacked offcanvas বসে 1065-এ — dialog যে panel-কে থামাতে চায় তারই পিছনে খুলত। তাই ছোট একটা `ConfirmDrawer` (offcanvas) লেখা হয়েছে।
   - **Headroom-ই screen-এর মূল মান:** drawer employee + period বাছলেই `summary` কল করে, value field `max_requestable`-এ cap করা, আর resolved টাকা সবসময় দৃশ্যমান — ৬০% টাইপ করে save চাপলে 422 আসে না, form-এই আটকায়।
   - **দুটো গেট মেরামত করতে হয়েছে:** (ক) nav item-এর path `/payroll/advances` → `/payroll/salary-advances` ও permission `payroll.menu-view` → `payroll.advance-manage` (card-এর নিয়ম); পুরনো path redirect করে। (খ) `advance_enabled` কেবল Payroll Settings page ভিজিট করলে store-এ বসত, তাই nav item **কখনোই** দেখাত না — এখন `useAdvanceEnabled()` `GET /payroll/settings` একবার পড়ে (settings page-এর সাথে একই query key, তাই বাড়তি request নেই) এবং জানা না যাওয়া পর্যন্ত nav ও route অপেক্ষা করে, false ধরে নেয় না।
   - **একটা backend স্পর্শ লেগেছে:** card-এর API তালিকায় `GET /payroll/settings` আছে অথচ route-টা `payroll.settings-manage`-এ আটকানো ছিল, ফলে শুধু `advance-manage` থাকা HR nav item-ই দেখত না। `EnsurePermission` এখন `permission:a|b` (any-of) নেয় — backward compatible, single key আগের মতোই — আর GET settings দুটো key-র যেকোনো একটিতে খোলে; PUT আগের মতোই `settings-manage`-এ। Test: advance-only HR settings **পড়তে পারে, লিখতে পারে না**।
   - **Settled payslip link `/payroll/payslips/{id}` এখন 8.2-FE route-এ যায়।** `settled` advance এখনো 8.3a-BE সেট করে।

---

## ফ্রন্টএন্ড শেয়ার্ড ইনফ্রা — Qbits design migration (11 Aug 2026)

এটা কোনো story card নয়, তাই উপরের অর্ডার বা lane বদলায় না। কিন্তু **shared frontend** বদলেছে, তাই আসন্ন FE card-গুলোর জানা দরকার। Branch: `new-design`।

**দুই styling system একসাথে, স্থায়ীভাবে।** Bootstrap 5 + Sneat চালায় attendance · payroll · configuration · platform · onboarding — **এই মডিউলগুলোর কোনো ফাইল ছোঁয়া হয়নি**। Tailwind চালায় login · layout shell · dashboard · employee।

- **Tailwind class-এ `tw:` prefix বাধ্যতামূলক** (`tw:flex`, `tw:px-4`)। দুই framework ২৩টা নাম ভাগ করে, তার ছয়টায় (`px-3`, `px-4`, `py-3`, `py-4`, `p-5`, `gap-3`) মাপ আলাদা — Bootstrap-এর `px-4` ১.৫rem, Tailwind-এর ১rem। Prefix ছাড়া দুই দিকেই ভাঙে। বিস্তারিত `src/styles/tailwind.css`-এ।
- **`scripts/check-style-boundary.sh` build-এ চলে** — migrated ডিরেক্টরিতে Bootstrap class, bootstrap-icons বা prefix-হীন utility থাকলে build ফেল। Bootstrap-side মডিউল এই gate-এর বাইরে, আগের মতোই চলে।
- **নতুন design system:** `src/shared/components/ui/` — Button · Input · Select · Textarea · Checkbox · DataTable · Modal · **Drawer** · ConfirmDialog · Card · Alert · StatusBadge · PageHeader · EmptyState · Avatar · Spinner। পুরনো `src/shared/components/common/*` **অপরিবর্তিত** — ওগুলোই Bootstrap-side মডিউলগুলো ব্যবহার করছে (Modal ১৪ সাইট, DataTable ১৯, PageHeader ৪৩)।
- **z-index scale Bootstrap-এর উপরে বসানো** (drawer ১৩০০ · modal ১৪১০ · toast ১৫০০)। `7.5-FE`-র নোটে লেখা "ConfirmDialog (Bootstrap modal, ১০৫৫) stacked offcanvas-এর (১০৬৫) পিছনে খোলে" সমস্যাটা নতুন `ui/ConfirmDialog` + `ui/Drawer`-এ নেই — Tailwind-side কাজে ঐ workaround (`ConfirmDrawer`) আর লাগবে না। Bootstrap-side-এ পুরনো আচরণ যেমন ছিল তেমনই।
- **Toast অপরিবর্তিত** — `shared/context/ToastContext` (৬৬ call site) আগের মতোই, renderer এখনো Bootstrap-স্টাইলের। restyle পিছিয়ে রাখা হয়েছে।
- **অ্যাপজুড়ে font বদল:** Public Sans → **Inter**। ইচ্ছাকৃতভাবে global, তাই attendance/payroll স্ক্রিনের টাইপও বদলাবে (markup অপরিবর্তিত)।
- **`perfect-scrollbar` সরানো হয়েছে** — শুধু পুরনো Sneat sidebar-এ ছিল।

**`tsc -b`:** ৩৮ → ২৫ error। employee-র ১৩টাই সাফ (ঐ ফাইলগুলো এমনিতেই নতুন করে লেখা হয়েছিল)। বাকি ২৫ — attendance ১৫ · configuration ৭ · payroll ৩ — main-এ যেমন ছিল তেমনই আছে, **ছোঁয়া হয়নি**। মনে রাখা দরকার: `npm run build` এই error-গুলোর কারণে main-এও ফেল করে (`tsc -b && vite build`); `vite build` একা পাস করে।

---

## ফ্রন্টএন্ড শেয়ার্ড ইনফ্রা — overlay stacking (12 Aug 2026)

এটাও story card নয়, কিন্তু **`shared/components/ui`-র Modal ও Drawer দুটোরই আচরণ বদলেছে**, তাই FE lane-এর জানা দরকার। Branch: `new-design`।

**কারণ:** employee list-এর View এখন পুরো profile-টা একটা drawer-এ খোলে, আর ওই drawer-এর ভেতরের tab-গুলো নিজেরাই drawer/modal খোলে। এতদিন প্রতিটা overlay নিজের Escape handler বসাত আর নিজের backdrop আঁকত — দুটো layer খোলা থাকলে এক Escape-এ **দুটোই** বন্ধ হতো, dimming দ্বিগুণ হতো, আর body scroll lock LIFO ছাড়া ছাড়ত না।

- **নতুন `src/shared/components/ui/overlayStack.ts`** — `useOverlayLayer(active)`। Modal ও Drawer একই stack ভাগ করে, তাই **সবচেয়ে শেষে যেটা খুলেছে সেটাই top**, component যেটাই হোক। শুধু top layer backdrop আঁকে, Escape নেয়, ক্লিক নেয়; নিচেরগুলো ২৮px বাঁয়ে সরে সামান্য dim হয়ে দেখা যায়।
- **z-index এখন stack থেকে আসে** (backdrop ১৩২০ · panel ১৩২৫, প্রতি layer-এ +১০) — `--z-index-modal*` token দুটো আর `ui/`-তে ব্যবহার হয় না, ওগুলো এখন ceiling হিসেবে থাকে। `--z-index-drawer` (১৩১০) এখনো mobile sidebar-এর, তাই stack ওর উপরে বসানো।
- **body scroll lock ref-counted** — বাইরের drawer আগে বন্ধ হলেও ভেতরেরটা খোলা থাকা অবস্থায় page-এ scrollbar ফিরে আসে না।
- **`useFocusTrap` nesting-aware** — উপরে নতুন layer খুললে নিচেরটা আর focus কেড়ে নেয় না, আর layer বন্ধ হলে focus যে control থেকে খোলা হয়েছিল সেখানেই ফেরে।
- **`ui/Drawer`-এ দুটো নতুন prop:** `size="xl"` (max-w-5xl) আর `headerActions` (title-এর পাশে বসে)। বাকি contract অপরিবর্তিত — বিদ্যমান ২০টা call site ছোঁয়া লাগেনি।

**Bootstrap-side অপরিবর্তিত।** `common/Offcanvas` + `common/offcanvasStack` আগের মতোই চলে (z ১০৪০/১০৪৫)। দুই stack ইচ্ছাকৃতভাবে আলাদা — z-index scale আলাদা। ফলে একই স্ক্রিনে legacy Offcanvas আর `ui/Drawer` মেশালে Escape-এর পুরনো আচরণই থাকবে; salary-advance স্ক্রিনে ওটা এখনো তেমনই আছে, আলাদা করে ঠিক করা হয়নি।

### `main` merge into `new-design` (12 Aug 2026)

`main`-এর পাঁচটা bugfix PR (#170–#174) ঠিক ঐ employee tab ফাইলগুলোই ছুঁয়েছিল যেগুলো `new-design` Bootstrap থেকে Tailwind-এ লিখেছে। Logic-এর অংশ (state, validation, mutation) নিজে থেকেই merge হয়েছে; conflict হয়েছে শুধু JSX-এ, ৭টা ফাইলে।

**নিয়ম যেটা মানা হয়েছে:** markup সবসময় `new-design`-এর (Tailwind + `shared/components/ui`), behaviour সবসময় `main`-এর — কোনো fix বাদ যায়নি, শুধু নতুন component-এ বসানো হয়েছে।

- `AssetTab` — asset-code duplicate/already-assigned check pending table-এ Status column হয়ে বসেছে; drawer-এর Save এখন check ব্যর্থ হলে বন্ধ হয় না (নাহলে যে error দেখাতে চাইছি সেটাই ঢেকে যেত)।
- `BankInfoTab` — optimistic set-primary + rollback, "অন্তত একটা primary থাকতেই হবে" guard, আর row-এ "Pending approval" badge।
- `DocumentsTab` — document type-এর `requires_expiry` অনুযায়ী expiry field enable/required, document type লোড না হলে Retry।
- `EmploymentsTab` — Employee Category field (mock data) `main` তুলে দিয়েছে, তাই Tailwind Select-টাও গেছে; নিজের employment status নিজে বদলানো যায় না।
- `IdentitiesTab` — একই employee-তে duplicate document number আটকানো, issue date অতীত / expiry ভবিষ্যৎ, আর attachment বাছার পরে preview + Remove।
- `app.scss` — import extensionless (`main`), perfect-scrollbar line বাদ (package-টাই আর নেই)। `package-lock.json` — `new-design`-এরটা, কারণ merged `package.json` হুবহু `new-design`-এর।

---

## Attendance module → new design (12 Aug 2026) — Records slice

Branch: `attendence-new-design`। মোট ৪৭টা `.tsx` (৭,৭৯০ line), এর প্রথম slice — **Records (৬ ফাইল)** — শেষ। বাকি area-গুলোর অর্ডার: Punch → Config/Types/Shifts → Policies → Leave → Assignments।

**Toast আগে ঠিক করতে হয়েছে।** `.erpflow-toast-container` বসে ছিল `z-index: 1090`-এ, আর `ui/overlayStack` drawer/modal বসায় ১৩২০ থেকে। Employee module-এ প্রতিটা add/edit/delete form এখন drawer-এ, ফলে **প্রতিটা toast panel-এর পেছনে render হচ্ছিল — মনে হচ্ছিল কিছুই হয়নি**। এখন z-index ১৫০০, আর container-টা `document.body`-তে portal করা (Modal/Drawer যে কারণে করে — কোনো ancestor-এর transform/filter যেন fixed container-কে আটকাতে না পারে)। Renderer এখনো Bootstrap-স্টাইলের, restyle **এখনো বাকি**।

**নতুন shared infra:**
- **`ui/Dropdown`** — Bootstrap-এর `data-bs-toggle="dropdown"` bootstrap.bundle.js ছাড়া খোলে না। এটা `useOnClickOutside` দিয়ে dismiss করে, বন্ধ হলে focus trigger-এ ফেরে। পুরো attendance module-এ ঐ একটাই জায়গা Bootstrap JS-এর উপর নির্ভর করত।
- **`ui/Button` এখন `ref` নেয়** — React 19-এ function component-এ `ref` সাধারণ prop, কিন্তু DOM attribute type-এ নেই, তাই explicit করা হয়েছে। Dropdown-এর focus ফেরানোর জন্য দরকার।

**Icon landmine সমাধান।** Attendance type-এর icon **database-এ `bi-calendar-check` ধরনের bootstrap-icons class হিসেবে জমা**, আর style boundary Tailwind dir-এ `bi bi-*` ব্লক করে। `utils/attendanceTypeIcon.ts`-এ ৪৫টা stored নামের lucide mapping + fallback রাখা হয়েছে, render-time-এ resolve হয়। **কলামটা migrate না করা পর্যন্ত এই ফাইল মোছা যাবে না।** পরে নতুন IconPicker-ও `ATTENDANCE_ICON_NAMES` থেকেই তালিকা নেবে।

**যে pre-existing tsc error গুলো এই slice-এ সাফ হয়েছে (৭টা):** `types/attendanceRecord.ts`-এর তিনটা ভাঙা import (`Employee`/`AttendanceType`/`Punch` — কোনোটাই ঐ module-গুলো export করত না), `ExportResponse` missing, `PunchRecord.ip_address` missing, আর list page-এর unused `refetch`। `AttendanceType` এখন `AttendanceTypeRecord`-এর alias, আর `ExportResponse` backend-এর **flat** `{status, message, data}` shape ধরে — ওটা ইচ্ছাকৃতভাবে `ApiResponse<T>` নয়, controller ঐভাবেই ফেরত দেয়।

**`check-style-boundary.sh`-এ একটা false positive ঠিক করা হয়েছে** — `\brow\b` Tailwind-এর নিজের `flex-row`-এও লাগত। এখন `[" ]row\b`, অর্থাৎ class token-এর শুরুতে হলে তবেই। Bootstrap-এর `row` সবসময় আলাদা token, তাই ওটা আগের মতোই ধরা পড়ে। এতদিন migrated কোডে কেউ `flex-row` লেখেনি বলে ধরা পড়েনি।

`src/modules/attendance` **এখনো `TAILWIND_DIRS`-এ যোগ করা হয়নি** — পুরো module শেষ হলে তবেই, নাহলে বাকি ৪১টা ফাইলের জন্য build ফেল করবে।

### Toast → Tailwind + Punch slice (12 Aug 2026)

**Toast এখন পুরোপুরি Tailwind।** `shared/context/ToastContext.tsx`-এর renderer Bootstrap markup (`toast bs-toast`, `bi` icon, `btn-close`) ছেড়ে `ui/`-র ভাষায় লেখা হয়েছে — accent bar + lucide icon + framer enter/exit, tone-গুলো `ui/Alert`-এর সাথে মেলানো যাতে একই কথা inline alert আর toast-এ একরকম পড়ে। Container `tw:z-toast` ব্যবহার করে, তাই `--z-index-toast` token আবার bundle-এ emit হয় (Tailwind v4 unused theme variable ছেঁটে ফেলে — কেউ ব্যবহার না করায় ওটা বাদ পড়ে গিয়েছিল)। `erpflow.scss`-এর মৃত `.erpflow-toast-container` block (১৮ লাইন) মুছে দেওয়া হয়েছে।

**API অপরিবর্তিত** — `useToast()` আর তার চারটা method আগের মতোই, তাই ৬৬টা call site-এর একটাও ছোঁয়া লাগেনি। Bootstrap-side module-গুলোও এখন এই Tailwind toast-ই দেখবে; সেটাই উদ্দেশ্য ছিল।

`src/shared/context` এখন `TAILWIND_DIRS`-এ — ঐ ডিরেক্টরিতে ToastContext ছাড়া কিছু নেই, আর সেটা এখন সম্পূর্ণ migrated।

**Punch slice (২ ফাইল) শেষ।** `PunchWidget`-এর পাঁচটা hand-rolled inline SVG lucide-এ গেছে, আর inline `<style>`-এর `@keyframes pulse-dot`-এর বদলে Tailwind-এর নিজের `animate-pulse` — ঐ একটাই জায়গা ছিল যেখানে component নিজের keyframe নিয়ে ঘুরত। Gradient আর dial-এর মাপ inline style-এই আছে, কারণ ওগুলো design token নয়, এই widget-এর একান্ত নিজস্ব।

**বাকি ৩৯টা ফাইল** (Config/Types/Shifts ৮ · Policies ৬ · Leave ১২ · Assignments ১১ · Home ২) এখনো Bootstrap। এগুলো শুরুর আগে `ui/Tabs`, নতুন IconPicker আর TextDivider লাগবে — Part 1-এর infra তালিকা দেখুন।

### Config + Attendance Types + Shifts slice (12 Aug 2026)

আরো ৮টা ফাইল শেষ। মোট **১৬/৪৭**।

**দুটো নতুন shared component:**
- **`ui/Tabs`** — employee profile-এর strip-টার সাধারণ রূপ। একই strip-এ button-tab আর link-tab দুটোই নেয়, কারণ attendance configuration-এ চারটার দুটো `?tab=` দিয়ে panel বদলায় আর দুটো আলাদা route — ব্যবহারকারীর কাছে পার্থক্যটা ধরা পড়া উচিত নয়।
- **`AttendanceIconPicker`** — Bootstrap IconPicker-এর বদলি। **stored value এখনো `bi-*`** (column আর API contract ওটাই ধরে), শুধু আঁকা হয় lucide দিয়ে `resolveAttendanceIcon` হয়ে। Search স্টোর করা নামেই মেলে, যা যথেষ্ট পড়ার মতো: `bi-calendar-check` "calendar" বা "check" দুটোতেই আসে।

**যা রূপান্তরিত হলো:** `AttendanceConfigNav` (nav-tabs → `ui/Tabs`), `AttendanceConfigPage`, দুটো config tab, `AttendanceTypeForm` (Offcanvas → Drawer + নতুন picker + preview badge), `AttendanceTypeList` (hand-rolled table → `DataTable`), `ShiftForm` (Offcanvas → Drawer, working-days toggle now real buttons), `ShiftList` (table + hand-rolled pagination → `DataTable`-এর নিজের pagination)।

**দুটো pre-existing tsc error সাফ:** `ShiftList`-এর `onMutate`-এ destructure করা `{id, status}` কোনোটাই ব্যবহার হচ্ছিল না (TS6198) — optimistic snapshot-টা আসলে শুধু `previous` ধরে, তাই destructure-টাই বাদ। মোট error ২৬ → ২৫।

**বাকি ৩১টা ফাইল:** Policies ৬ · Leave ১২ · Assignments ১১ · Home ২। এর মধ্যে `TextDivider` (LeaveRulePanel, PolicyFormDrawer) এখনো বানানো হয়নি — Policies শুরুর আগে লাগবে।

### Policies slice (12 Aug 2026)

আরো ৬টা ফাইল। মোট **২২/৪৭**।

**নতুন `ui/TextDivider`** — caption সহ একটা রেখা, লম্বা ফর্মের ভাগগুলো heading-এর ভার ছাড়াই আলাদা করে। Bootstrap-এর `common/TextDivider`-এর আটটা color আর পাঁচটা align variant বাদ দেওয়া হয়েছে: ব্যবহার হতো দুই জায়গায়, দুটোই default রঙে।

**দুটো pre-existing tsc error এক লাইনে সাফ।** `PolicyStatusActions.onStatusChange`-এর ঘোষণা ছিল `Promise<void> | void`, কিন্তু দুই কলার-ই `mutateAsync` পাঠায় যা `Promise<AttendancePolicy>` ফেরত দেয়। Signature-টা `Promise<unknown> | void` করা হয়েছে — component-টা শুধু *কখন শেষ হলো* জানতে চায়, *কী ফিরল* নয়। এতে `PolicyFormDrawer:295` আর `PolicyListPage:117` দুটোই গেল।

**Policy type switch** (`nav-pills`) এখন `ui/Tabs`-এর button-mode ব্যবহার করে, তাই config-এর strip আর type-এর strip একই জিনিসের মতো দেখায়।

**বাকি ২৫টা ফাইল:** Leave ১২ · Assignments ১১ · Home ২। Leave-এর দুটো page এখনো `PageHeader`-এ `breadcrumbs` prop পাঠায় যা কোনো PageHeader-এই নেই — ওগুলো ঐ slice-এ ধরা হবে।

### Attendance module → new design: COMPLETE (12 Aug 2026)

শেষ ২৫টা ফাইল (Leave ১২ · Assignments ১১ · Home ২) হয়ে গেছে। **৪৭/৪৭** — পুরো module Tailwind-এ, আর `shared/components/common/*`-এর একটা import-ও attendance-এ আর নেই।

**`src/modules/attendance` এখন `TAILWIND_DIRS`-এ।** `check-style-boundary.sh` সাতটা ডিরেক্টরি পাহারা দেয় এবং clean — অর্থাৎ Bootstrap class, bootstrap-icon, prefix-হীন utility বা hard-coded z-index পুরো module-এ একটাও নেই। এটাই সবচেয়ে শক্ত প্রমাণ; আর কখনো ফিরে আসতে পারবে না, build আটকাবে।

**যে তিন জায়গায় component-এর নিজের CSS ছিল, তিনটাই মুছেছে** (`erpflow.scss` মোট ~১২৫ লাইন হালকা):
- `.leave-calendar-*` (৭৩ লাইন) — date-range picker। CSS source order-এর উপর নির্ভরশীল precedence (edge > nonworking > in-range) এখন একটা `dayClasses()` if-চেইন, তাই নিয়মটা পড়াই যায় আর ভাঙার সুযোগ নেই।
- `.unassigned-warning-*` (৩৪ লাইন) — preview page-এর warning card, hand-rolled inline SVG সহ।
- `.attendance-home-*` — home card-এর hover lift।

**API contract বদলেছে দুটো — দুটোই callers সহ:**
- `effectivenessBadgeClass()` → `effectivenessBadgeTone()`, Bootstrap class-এর বদলে `ui/StatusBadge` tone ফেরত দেয়।
- `LeaveBalanceGrid` এখন `pagination`/`onPageChange` নেয়, তাই `LeaveBalancesPage`-এর হাতে-লেখা pager বাদ।

**tsc:** attendance-এ শুরুতে যে ১৫টা pre-existing error ছিল, **সবগুলো শূন্য**। বাকি error কেবল configuration আর payroll-এ — ঐ দুটো module ছোঁয়া হয়নি।

**যা এখনো Bootstrap-side:** configuration (১০ ফাইল), payroll (১৮), platform (১৯), onboarding (২) — এগুলোই `shared/components/common/*` ধরে রেখেছে, তাই ঐ ডিরেক্টরিটা মোছা যাবে না।

---

## ডেভ এনভায়রনমেন্ট — `backend/.env` ↔ `docker-compose.yml` (12 Aug 2026)

Story card নয়, কিন্তু **যে কেউ stack চালাতে গিয়ে এতে হোঁচট খাবে**, তাই এখানে রাখা।

### `.env` আর `.env.example` এক করা হয়েছে

আগে নয়টা key কেবল এক ফাইলে ছিল — `.env`-এ `REDIS_*` কিছুই ছিল না, আর `.env.example`-এ `CACHE_STORE`/`JWT_ALGO` ছিল না। এখন দুটোর key-set হুবহু এক; `.env.example`-এ সব secret ফাঁকা।

**`REDIS_HOST` না থাকাটা আসল বাগ ছিল** — Laravel default `127.0.0.1` ধরত, যা backend container-এর ভিতরে **নিজেই**, redis নয়। এখন `redis:6379`-এ resolve করে। এখন কিছু redis ব্যবহার করে না (`CACHE_STORE`/`QUEUE`/`SESSION` সবই `database`), তাই কিছু ভাঙেনি — কিন্তু যেদিন লাগবে, কাজ করবে।

### `REDIS_PORT`-এর দুটো কাজ, নিরাপদ মান একটাই

Compose এটা দিয়ে **host port** publish করে (`${REDIS_PORT}:6379`), আর Laravel-ও একই var দিয়ে **connect** করে (`config/database.php`)। Database-এর বেলায় compose `backend`/`queue` service-এ `DB_HOST: mysql, DB_PORT: 3306` override করে রেখেছে — **Redis-এর কোনো override নেই**। তাই 6379 ছাড়া অন্য কিছু দিলে host-এ কাজ করছে মনে হবে, কিন্তু app container থেকে `redis:<সেই port>`-এ dial করে ভাঙবে। `.env.example`-এ comment দিয়ে লেখা আছে।

`DB_HOST`/`DB_PORT`-ও একই রকম দ্বৈত — ওগুলো **শুধু host থেকে চালানো artisan/GUI client-এর জন্য**; container-এর ভিতরে compose override জেতে। এটাও কোথাও লেখা ছিল না, এখন আছে।

### ⚠ Compose নিজে থেকে `backend/.env` পড়ে না

Compose শুধু **project root**-এর `.env` স্বয়ংক্রিয়ভাবে পড়ে, `backend/.env` নয়। ফলে `--env-file` ছাড়া চালালে প্রতিটা port `.env`-এর মান নয়, compose-এর **default**-এ যায়:

| | `backend/.env` | `--env-file` ছাড়া default |
|---|---|---|
| `FRONTEND_PORT` | 3000 | **3010** |
| `DB_PORT` | 3306 | **3310** |
| `REDIS_PORT` | 6379 | **6380** |

তাই সবসময়:

```
docker compose --env-file backend/.env up -d
```

**এটা একবার CORS ভেঙেছে**: frontend 3010-এ উঠেছিল, `CORS_ALLOWED_ORIGINS`-এ 3010 ছিল না, আর preflight fail করেছিল। মনে হচ্ছিল backend-এর বাগ — ছিল না।

`docker-compose.yml`-এ নিজেরই একটা অসংগতি আছে, ছোঁয়া হয়নি: `frontend` service `${FRONTEND_PORT:-3010}` ব্যবহার করে কিন্তু `frontend-prod` `${FRONTEND_PORT:-3000}` — একই var, দুই রকম default।

### ⚠ লোকাল port forward Docker-এর publish-কে ঢেকে দেয়

আরেকটা CORS-এর মতো দেখতে সমস্যা যেটা CORS ছিল না। IDE (Cursor/VS Code) port forward করলে সে `127.0.0.1:<port>`-এ bind করে, আর Docker করে `*:<port>`-এ। OS **বেশি specific socket-কে** আগে দেয়, তাই `localhost:8010` তখন Docker-এর nginx-এ যায় **না** — IDE-র forwarder-এ যায়, যেটা অন্য/পুরনো কোনো target-এ পাঠাতে পারে।

লক্ষণ: browser CORS error দেয়, অথচ `.env` আর `config/cors.php` ঠিক।

যাচাইয়ের উপায়:

```
lsof -nP -iTCP:8010 -sTCP:LISTEN     # একাধিক listener থাকলে ওটাই কারণ
curl -i -X OPTIONS http://<LAN-IP>:8010/api/v1/auth/login \
     -H "Origin: http://localhost:3010" -H "Access-Control-Request-Method: POST"
```

LAN IP দিয়ে গেলে Docker-এই যায় (IDE ওখানে bind করে না)। LAN IP-তে allow করে অথচ `localhost`-এ blocked মানে **forward-টাই আসামি** — IDE-র Ports panel থেকে সরিয়ে দিন।

### `.gitignore`

`backend/.gitignore`-এ `.env.bak*` যোগ করা হয়েছে। `.env` আর `.env.backup` ignored ছিল, কিন্তু `.env.bak.<date>` ধরনের backup নয় — আসল secret সহ commit হয়ে যেতে পারত।

---

## Payroll module → new design: COMPLETE (13 Aug 2026)

৩০/৩০ ফাইল Tailwind-এ। `src/modules/payroll` এখন `TAILWIND_DIRS`-এ — gate **আটটা ডিরেক্টরি** পাহারা দেয় এবং clean।

**নতুন কোনো shared component লাগেনি।** Attendance-এর জন্য বানানো `ui/Dropdown`, `ui/Tabs`, `ui/TextDivider` আর overlay stack-ই যথেষ্ট ছিল। Payroll-এ Bootstrap JS, inline `<style>` বা shared `erpflow.scss` class কিছুই ছিল না, তাই attendance-এর চেয়ে পরিষ্কার রূপান্তর।

**`ConfirmDrawer.tsx` (৫৩ লাইন) মুছে ফেলা হয়েছে।** ফাইলটার নিজের কমেন্টে কারণ লেখা ছিল: shared `ConfirmDialog` ছিল Bootstrap modal (z-index ১০৫৫) আর দ্বিতীয় stacked offcanvas বসত ১০৬৫-এ, তাই dialog যে panel-কে থামাতে চায় তারই পিছনে খুলত। `ui/overlayStack` overlay-গুলোকে **খোলার ক্রমে** সাজায়, তাই shared dialog আবার সঠিক জায়গায় খোলে। এই workaround-টা যে অপ্রয়োজনীয় হয়ে যাবে, সেটা "overlay stacking" অংশে আগেই লেখা ছিল।

**চারটা Modal → Drawer** (payroll configuration): tax slab form, tax preview, deduction form, deduction edit। ফর্ম আর প্যানেল এখন সব module-এ একইভাবে ডানদিক থেকে আসে; `ConfirmDialog` কেবল আসল confirmation-এই থাকে।

**`PaymentSplitPanel` একটা লুকানো অসঙ্গতি ছিল।** ওটা payroll-এ থাকে কিন্তু ব্যবহার করে **employee module** (`EmployeeSalaryForm`)। employee অনেক আগেই Tailwind আর gate-এর ভিতরে, কিন্তু gate কেবল তালিকাভুক্ত ডিরেক্টরি scan করে — তাই employee-র Tailwind drawer-এর ভিতরে একটা Bootstrap panel বসে থাকত আর ধরা পড়ত না। এখন দুটোই এক।

**Drag-reorder টেবিল দুটো (`ComponentBuilder`, `PaymentSplitPanel`) ইচ্ছাকৃতভাবে `ui/DataTable` ব্যবহার করে না** — DataTable-এর row হলো `motion.tr`, আর framer-এর নিজের `onDragStart`/`onDragEnd` gesture prop HTML5 drag handler-এর সাথে সংঘর্ষ করে। DataTable-এ `rowProps` escape hatch যোগ করতে গিয়ে দেখা গেল সেটাই ফাঁদ হতো; তাই ঐ দুই জায়গায় DataTable-এর head/cell class কপি করে হাতে টেবিল লেখা — দেখতে এক, কিন্তু drag আসলে কাজ করে।

**payroll-এ শুরুতে যে ৩টা pre-existing tsc error ছিল, তিনটাই শূন্য:** `BracketEditor`-এর unused `isFirst`, `TaxSlabFormPage`-এর unused `TaxSlab` import, আর `EmployeeDeductionListPage`-এ `number | ''`-কে `=== ''` দিয়ে তুলনা (উপরের falsy check-ই যথেষ্ট ছিল, ওটা guard নয় type error ছিল)।

**যা এখনো Bootstrap-side:** configuration (১০ ফাইল), platform (১৯), onboarding (২) — এগুলোই `shared/components/common/*` ধরে রেখেছে।


---

## Configuration module → new design: COMPLETE (13 Aug 2026)

২১/২১ ফাইল Tailwind-এ। `src/modules/configuration` এখন `TAILWIND_DIRS`-এ — gate **নয়টা ডিরেক্টরি** পাহারা দেয় এবং clean। **Employee · Attendance · Payroll · Configuration — চারটাই migrated**; বাকি platform (১৯ ফাইল) আর onboarding (২)।

**নয়টা Modal → Drawer।** Employment type · designation · document type · division · department · team · branch · employee grade · ID card setting — configuration-এর প্রতিটা ফর্ম এখন ডানদিক থেকে আসে, বাকি module-এর মতোই। `CompanyConfigurationTab`-এর inline "Add Company" ফর্মটাও card থেকে drawer-এ গেছে, তাই কোম্পানি tab-টাও এখন list + drawer।

**`ArchiveDialog` ইচ্ছাকৃতভাবে Modal-ই থাকল।** ওটা একটা প্রশ্ন করে বাধা দেয় আর drawer-এর ভিতর থেকেই ওঠে — drawer-এর উপর drawer নয়, উপরে একটা dialog-ই সঠিক আচরণ। ফাইলে কমেন্ট করে কারণটা লেখা আছে যাতে পরে কেউ "সব drawer" ভেবে বদলে না ফেলে।

**`listTable.tsx` — দুইটা ছোট shared helper।** আটটা list ফাইলেই একই sort-header আর একই client-side pagination meta ছিল; `SortHeader` আর `pageMeta()` সেটা এক জায়গায় এনেছে। এর বেশি abstraction করা হয়নি — column সংজ্ঞা প্রতিটা list-এ আলাদাই থাকে, কারণ সেগুলো আসলেই আলাদা।

**তিনটা পুরনো tsc error root cause-সহ ঠিক হয়েছে, চাপা দেওয়া হয়নি:**
- `EmployeeGradeForm`-এ `grade_code` field-টাই ছিল না। API payload, list column আর submit handler — তিনটাই field-টা support করত, কেবল ফর্ম কখনো মানটা নিত না। ফর্মে field যোগ করা হয়েছে।
- `DivisionList` `record.branch_name` পড়ত, কিন্তু `DivisionRecord`-এ ওটা নেই — শুধু `branch_id` আছে। tab-টা ফর্মের dropdown-এর জন্য branch list এমনিতেই load করে, তাই সেখান থেকেই নাম resolve করা হয়।
- `DepartmentManagementList`-এর অব্যবহৃত `configurationApi` import সরানো হয়েছে।

**Build gate তিনটা attendance leftover ধরিয়ে দিয়েছে** — `AttendanceRecordDetailView`-এর unused `DropdownItem` type import, `RecalculateConfirmationModal`-এর unused `React` import, `CorrectionRequestDetailPage`-এর unused `navigate`। তিনটাই সরানো হয়েছে; `npm run build` এখন পুরো clean (`tsc -b` ০ error, style boundary clean, vite build ✓)।

**একটা lint warning ইচ্ছাকৃতভাবে রাখা:** `listTable.tsx` একই ফাইলে component (`SortHeader`) আর function (`pageMeta`) দুটোই export করে, তাই oxlint-এর fast-refresh warning আসে। দুটোকে আলাদা ফাইলে ভাগ করলে warning যাবে কিন্তু একসাথে-পড়া helper জোড়াটা ভেঙে যাবে — dev-only warning-এর জন্য সেই দাম দেওয়া হয়নি।

---

## Platform › Module Registry → new design + drawer (13 Aug 2026)

Platform module-এর প্রথম screen new design-এ। **পুরো module নয়** — Module Registry-র তিনটা ফাইল, বাকি ~১৮টা platform page আগের মতোই Bootstrap-side।

**ফর্ম page → drawer, URL-driven।** `ModuleForm.tsx` (৩৬০ লাইন, নিজস্ব route) মুছে `components/ModuleFormDrawer.tsx` হয়েছে, registry list-এর উপরেই বসে। Address employee profile drawer-এর মতোই search param — `?mode=create` আর `?mode=edit&module=<slug>` — তাই খোলা ফর্ম share করা যায় আর back button কাজ করে।

**পুরনো দুটো URL redirect হয়ে বেঁচে আছে।** `/admin/modules/create` আর `/admin/modules/:slug/edit` এখন `pages/ModuleFormRedirect.tsx`-এ যায়, যেটা ঠিক drawer-টা খোলা অবস্থায় registry-তে পাঠায়। route-এর `RequirePermissionRoute` wrapper দুটো অক্ষত — redirect-এর আগেই permission check হয়।

**Permission gate route থেকে UI-তে সরেছে।** আগে "Add Module"/"Edit" সবসময় দেখা যেত, click করলে guarded route denied page দিত। এখন button/menu item-ই `platform.create` / `platform.update` দিয়ে gate করা — authorization একই, কেবল আগে জানা যায়। Delete-এর আচরণ অপরিবর্তিত: `is_core` module মুছা যায় না, আলাদা permission আগেও ছিল না।

**`queryClient.invalidateQueries(['modules'])` নতুন করে লাগল।** পুরনো full-page ফর্ম save-এর পর list-এ navigate করত, তাই query এমনিতেই refetch হতো। Drawer mounted থাকে — তাই invalidate না করলে save-এর পর সারি পুরনো দেখাত। এটা page → drawer রূপান্তরের সাধারণ ফাঁদ।

**`utils/moduleIcon.ts` — `modules.icon` column-টা Bootstrap Icons class রাখে** (`bi-people`, `bi-cash-stack`, …; DB-তে এখন ৫টা row)। column-টা এই registry ছাড়া আর কেউ পড়ে না, তাই data migrate না করে render-time-এ lucide-তে map করা হয়েছে — attendance-এর `attendanceTypeIcon.ts`-এর মতোই, অচেনা নামের জন্য neutral fallback সহ। ফর্মের icon field-এ চেনা নামগুলো `<datalist>` suggestion হিসেবে আসে আর পাশে live preview দেখায়।

**style-boundary gate এখন ফাইল-পর্যায়েও কাজ করে।** আগে `[ -d "$d" ]` দিয়ে কেবল directory নিত, তাই আংশিক migrated module-এর কোনো ফাইল পাহারা দেওয়া যেত না — `platform` পুরোটা যোগ করলে বাকি ১৮টা Bootstrap page-এ build ভাঙত। এখন `[ -e "$d" ]`, আর Module Registry-র তিনটা ফাইল আলাদাভাবে তালিকাভুক্ত (মোট ১৩টা path)। platform পুরোপুরি migrate হলে ঐ তিনটা এন্ট্রি সরিয়ে শুধু directory-টা বসবে।

---

## Platform › Roles → new design + drawer (13 Aug 2026)

Roles menu-র তিনটা screen + matrix component new design-এ। Module Registry-র সাথে মিলিয়ে platform-এর **সাতটা ফাইল** এখন gate-এর ভিতরে (মোট ১৭টা path); বাকি ~১৫টা platform page আগের মতোই Bootstrap-side।

**`RoleForm.tsx` page → `components/RoleFormDrawer.tsx`**, registry-র মতোই URL-driven — `?mode=create` আর `?mode=edit&role=<uuid>`। পুরনো `/admin/roles/create` আর `/admin/roles/:uuid/edit` redirect হয়ে বেঁচে আছে; redirect ফাইলটা এখন দুই screen-ই সামলায় বলে `ModuleFormRedirect.tsx` → **`FormRouteRedirects.tsx`** নাম বদলেছে।

**Create-এর পর matrix-এ যাওয়ার hand-off অক্ষত।** নতুন role-এর কোনো permission থাকে না, তাই create success এখনো `/admin/roles/<uuid>/permissions`-এ navigate করে — drawer বন্ধ হয়ে page বদলায়। Edit-এ drawer শুধু বন্ধ হয়, আর `['roles']` invalidate করা হয় (page → drawer রূপান্তরের সেই একই ফাঁদ)।

**Permission matrix-ও drawer** (`?mode=permissions&role=<uuid>`)। প্রথমে page রাখা হয়েছিল — প্রশস্ত workspace বলে — কিন্তু সিদ্ধান্তটা উল্টানো হয়েছে: roles-এর তিনটা কাজই এখন একই list থেকে ডানদিকের panel-এ হয়, আলাদা page-এ ছিটকে যায় না। `PermissionMatrixPage.tsx` মুছে `components/RolePermissionsDrawer.tsx`, আর `/admin/roles/:uuid/permissions` redirect হয়ে বেঁচে আছে। তিনটা `mode` পরস্পর-বিকল্প, তাই একসাথে দুটো drawer খোলে না — edit drawer-এর "Manage permissions" শুধু `mode` বদলায়।

**Row-এর action গুলো menu-র ভিতরে না রেখে সারিতেই।** Permissions একটা নামসহ button, Edit আর Delete icon-button; system role হলে Delete আসে না। মাত্র তিনটা action, আর Permissions-টাই সবচেয়ে বেশি দরকার হয় — একটা "⋯" menu-র পিছনে লুকিয়ে রাখার মানে ছিল না।

**`PermissionMatrix`-এর degenerate table-টা সরানো হয়েছে।** প্রতি module-এর জন্য একটা করে table ছিল যাতে **body row মাত্র একটা**, আর action-গুলো column — module-এ কয়েকটার বেশি action হলেই ওটা পাশে scroll করত। একই data এখন module header (icon · নাম · slug · action count · select-all) + নিচে wrap করা checkbox grid। Selection semantics, props, callback — সব হুবহু আগের মতোই; কেবল উপস্থাপনা বদলেছে। Module icon-এর `bi-*` মানটা Module Registry-র `resolveModuleIcon()` দিয়েই resolve হয় — util-টা দ্বিতীয় ব্যবহারকারী পেল।

**Permission gate এখানেও route থেকে UI-তে।** "Add Role" → `platform.create`; "Edit" আর "Permissions" → `platform.update`। Delete আগের মতোই — `is_system` role মুছা যায় না, আলাদা permission আগেও ছিল না।

**একটা জিনিস ইচ্ছাকৃতভাবে ছোঁয়া হয়নি:** company filter দেখানোর শর্ত `companyFilterOptions.length > 1`, কিন্তু ঐ তালিকায় "All companies" entry-টাও গোনা হয় — তাই একটাই company থাকলেও filter দেখা যায়, যদিও filter করার কিছু নেই। এটা design migration-এর আগেও এমনই ছিল; আচরণ বদলানো এই কাজের অংশ নয়, তাই `> 1`-ই রাখা হলো।

**পরে ধরা পড়া একটা bug, তিন drawer-এই ছিল।** record load করার `useEffect`-গুলো কেবল query data-র উপর নির্ভর করত (`[role]`, `[module]`, `[rolePermissions]`)। react-query একই record-এর জন্য reopen-এ **হুবহু একই object reference** ফেরত দেয়, তাই effect আর চলত না — কেউ drawer খুলে কিছু বদলে Cancel করে আবার একই record খুললে ঐ uncommitted পরিবর্তনগুলোই বসে থাকত। তিনটাতেই dependency-তে `show` যোগ করা হয়েছে, তাই প্রতিবার খোলার সময় stored অবস্থায় reset হয়। Page → drawer রূপান্তরের এটা দ্বিতীয় সাধারণ ফাঁদ (প্রথমটা `invalidateQueries`) — নতুন drawer লিখলে দুটোই মিলিয়ে দেখা দরকার।

**Action checkbox দুই জায়গায় এক চেহারা (13 Aug 2026)।** Module form-এ action-গুলো card হিসেবে দেখাত — label-এর নিচে monospace-এ আসল slug — আর permission matrix-এ ছিল খালি একটা checkbox + label। মানটা গুরুত্বপূর্ণ: দুটো module-ই action-টাকে "Approve" ডাকতে পারে অথচ permission key আলাদা, আর role wire করার সময় admin-কে দেখতে হয় কোনটায় টিক দিচ্ছে। দুটোই এখন `components/ActionCheckboxCard.tsx` ব্যবহার করে — ModuleFormDrawer-এর inline copy-টা সরে গেছে। তবে **code line-টা optional**: module form-এ slug দেখায় (ওখানে admin ঠিক করছে কোন action-গুলো আদৌ থাকবে, তাই আসল string-টাই বাছাইয়ের বিষয়), permission matrix-এ দেখায় না (role-এর action এমনিতেই module heading-এর নিচে সাজানো, key-টা ঐ heading-এরই পুনরাবৃত্তি হতো)। Matrix-এ key-টা `title`-এ থেকে যায়, তাই hover করলে পাওয়া যায়। এক লাইনের card বলে checkbox-টা center-align হয় আর সারিতে বেশি card ধরে। Card টিক দিলে `has-checked:` variant দিয়ে brand tint নেয়, তাই কোনগুলো নির্বাচিত তা এক নজরে বোঝা যায়।

**Roles row-এর action তিনটা একটা overlap করা cluster।** নরম গোল border-ওয়ালা একটা pill-এর ভিতরে তিনটা গোল icon, প্রতিটা আগেরটার প্রায় ৩০% ঢেকে বসে — পুরো সেটটা একটা button-এর চেয়ে সামান্য বেশি জায়গা নেয়, তাই টেবিল data-র জায়গাতেই থাকে। Cluster-এ hover করলে margin animate করে ওরা ছড়িয়ে যায়: কয়টা action আছে সেটাও দেখা যায়, আর click করার আগেই প্রতিটার নিজের hit target পাওয়া যায়।

**Ring-টা cluster-এর নিজের background রঙে আঁকা** — overlap-কে "একটা disc আরেকটার সামনে" দেখানোর কাজটা ঐ ring-ই করে; নইলে দুটো আকৃতি একটার ভিতর আরেকটা মিশে যেত। রং তিনটা আলাদা রাখে (Permissions sky · Edit brand · Delete rose)। Action-গুলো array হিসেবে বানিয়ে map করা হয়, কারণ system role-এ Delete থাকে না — তখন index দেখে overlap-টা বাকি দুটোতেই ঠিকভাবে বসে।

মাঝপথে `buttonStyles`-এ `info`/`success` variant যোগ করা হয়েছিল, ভরাট রঙিন button-এর জন্য। খালি icon-এ যাওয়ায় ঐ দুটোর আর কোনো ব্যবহারকারী থাকল না, তাই **সরিয়ে দেওয়া হয়েছে** — shared design system আগের অবস্থাতেই ফিরেছে। ভবিষ্যতে সত্যিই ভরাট `info` button দরকার হলে তখন যোগ করাই ভালো, একটাও call site ছাড়া রেখে দেওয়ার চেয়ে।

---

## Platform › Users → new design + drawer (13 Aug 2026)

তিনটা page মুছে তিনটা drawer: `UserForm.tsx` · `UserRoles.tsx` · `UserPermissionOverrides.tsx` → `components/UserFormDrawer.tsx` · `UserRolesDrawer.tsx` · `UserOverridesDrawer.tsx`। URL — `?mode=create` · `?mode=roles&user=<uuid>` · `?mode=overrides&user=<uuid>`; পুরনো তিনটা route redirect হয়ে বেঁচে আছে। platform-এর ১১টা ফাইল এখন gate-এর ভিতরে (মোট ২২টা path)।

### Override screen: role যা দেয় সেটুকুই, কেড়ে নেওয়ার জন্য

`PermissionEngine::computeUserPermissions()` অনুযায়ী —

```
granted = (role যা দেয়) ∪ (allow override) − (deny override)
```

Screen-টা **কেবল assigned role-গুলো যা দেয় তাই** দেখায়, সবগুলো ticked — কারণ আজকের অবস্থা সেটাই। Tick তুলে নিলে ঐ permission-এর একটা `deny` override লেখা হয়; আবার tick দিলে override মুছে যায় আর permission role-কে অনুসরণ করতে ফেরে। যেগুলোতে হাত পড়েনি সেগুলো un-overridden থাকে, **তাই পরে role বদলালে সেটা এই user পর্যন্ত পৌঁছায়**। Role যে module-এ কিছুই দেয় না, সে module তালিকা থেকে বাদ পড়ে।

**`allow` override এই screen-এ ইচ্ছাকৃতভাবে নেই।** Engine ওটা সমর্থন করে, কিন্তু তার জন্য পুরো permission catalogue scroll করতে হতো একটা বিরল কাজের জন্য — আর বাড়তি কিছু দেওয়াই তো role-এর কাজ। **তবে থাকা `allow` row মুছে ফেলা হয় না**: এই screen যা দেখাতে পারে না, save-এর সময় সেগুলো হুবহু আগের মতোই পাঠিয়ে দেওয়া হয়। একই আচরণ matrix-এ আর নেই এমন permission-এর override-এর জন্যও (module থেকে action সরে গেলে)।

**`developer` user_type বা `administrator` role** থাকলে permission engine পুরোটা bypass হয় ([PermissionEngine::userHasFullAccess](backend/app/Platform/Services/PermissionEngine.php))। তখন warning দেখানো হয়, কারণ নইলে tick বদলে কিছুই হচ্ছে না দেখে admin বিভ্রান্ত হতো। Slug-টা backend config-এর default-এর প্রতিলিপি, আর সেটা **কেবল warning দেখানোর কাজে** — save-এ ওর কোনো ভূমিকা নেই।

**Baseline আনতে হয় role ধরে ধরে** — অন্য user-এর effective permission ফেরত দেয় এমন কোনো endpoint নেই, তাই `useQueries` দিয়ে প্রতিটা assigned role-এর permission আনা হয়। Role সংখ্যা ছোট আর সবগুলো cached, কিন্তু **একটা `GET /users/{uuid}/effective-permissions` থাকলে এটা এক request-এ নেমে আসত** — role count বাড়লে সেটাই করা উচিত।

**`draft` state pattern।** এই drawer-এ server data state-এ copy করার effect নেই: `draft === null` মানে "এখনো কিছু বদলানো হয়নি", তখন stored অবস্থাটাই দেখানো হয়। এতে reopen bug-টা গঠনগতভাবেই থাকে না — অন্য drawer-গুলোতে যেটা `show` dependency দিয়ে সারাতে হয়েছিল। নতুন drawer লিখলে এই ধরনটাই ভালো।

**`ActionCheckboxCard`-এ `note` slot আর `PermissionMatrix`-এ `noteFor` hook যোগ হয়েছে** — role screen-এ tick মানে শুধুই granted, কিছু বলার নেই; override screen-এ কেবল **কেড়ে নেওয়া** row-গুলো "overridden" লেখে। Roles-এর ব্যবহার অপরিবর্তিত, দুটোই optional।

### `ui/Tooltip` — বাঁ দিকে, উপরে নয়

Icon-only action-এর জন্য CSS-only tooltip। **Trigger-এর বাঁ পাশে বসে, উপরে নয়** — এগুলো table row-এ থাকে আর `ui/DataTable` পাশে scroll করে, যাতে browser উল্লম্ব overflow-ও clip করে; উপরে বসালে প্রথম row-এর আর নিচে বসালে শেষ row-এর bubble কেটে যেত। পাশে বসলে সেটা row-এর নিজের ব্যান্ডেই থাকে, কিছু কাটে না।

Trigger-এ `aria-label` থাকে আর bubble-টা `aria-hidden` — নইলে screen reader একই কথা দুবার পড়ত। Overlap-এর negative margin এখন tooltip wrapper-এ, কারণ layout box এখন ওটাই।

---

## Platform › Approval Workflows → new design + drawer (13 Aug 2026)

`ApprovalWorkflowForm.tsx` (২৬৮ লাইন page) মুছে `components/ApprovalWorkflowFormDrawer.tsx`; `WorkflowStepBuilder` আর list-ও Tailwind-এ। URL — `?mode=create` আর `?mode=edit&workflow=<uuid>`; পুরনো দুটো route redirect হয়ে বেঁচে আছে। platform-এর ১৪টা ফাইল এখন gate-এর ভিতরে (মোট ২৫টা path)। বাকি: Approval Settings · Approval Requests · Actions · Activity logs · OTP।

**Edit mode-এ module lookup-টা পুরো বাদ গেছে — আর সেটাই একটা পুরনো lint warning-এর মূল।** আগের page edit-এ `workflow.module.slug` ধরে module list ঘেঁটে `module_id` বের করত, তাই `useEffect`-এর dependency-তে `modules` array বসাতে হতো — যেটা প্রতি render-এ নতুন, আর oxlint সেটা ধরিয়ে দিত ("it will always cause this hook to re-evaluate")। কিন্তু **update payload-এ `module_id`/`module_action_id` যায়ই না** — workflow তৈরির পর ওগুলো আর বদলায় না। তাই edit-এ দুটোই read-only field হিসেবে `workflow.module.name` / `module_action.label` দেখায়, lookup-এর দরকারই নেই, আর module list-টা কেবল create mode-এ fetch হয়। `oxlint src/modules/platform` এখন **০ warning, ০ error**।

**Step builder-এর multi-select checkbox list হয়েছে।** `custom` resolver-এ approver বাছতে `<select multiple>` ছিল — ctrl-click দিয়ে set বানানো একটা পরিচিত ফাঁদ (এক ক্লিকে আগের সব নির্বাচন চলে যায়), আর drawer-এর ভিতরে scroll করার সময় সেটা আরও খারাপ। এখন scroll করা একটা checkbox তালিকা; নির্বাচন যোগ-বিয়োগ হয়, হারায় না।

**Step-এর move up / move down / remove — icon button + tooltip।** `step_order` প্রতিটা কাঠামোগত বদলের পর নতুন করে গোনা হয়, কারণ backend index নয়, ঐ সংখ্যাটা ধরেই step চালায়।

**Version-এর কথাটা এখন যেখানে দরকার সেখানেই।** "steps বদলালে নতুন version হয়, চলমান request অক্ষত থাকে" — এটা আগে page subtitle-এ ছিল, যেখানে কাজ শুরু করার আগেই চোখে পড়ত; এখন edit drawer-এর ভিতরে alert হিসেবে, ঠিক যেখানে step বদলানো হচ্ছে।

---

## Platform › Approval Settings + Requests → new design (13 Aug 2026)

Approvals menu-র বাকি দুটোই শেষ। `ApprovalRequestDetail.tsx` (৩৩৯ লাইন page) মুছে `components/ApprovalRequestDrawer.tsx`, list থেকেই `?request=<uuid>` দিয়ে খোলে; পুরনো route redirect হয়। Settings আর Requests list দুটোই Tailwind। platform-এর ১৭টা ফাইল এখন gate-এর ভিতরে (মোট ৩০টা path) — বাকি শুধু Actions · Activity logs · OTP।

**`ApprovalStatusBadge` Tailwind-এ এসেছে, আর এটা একটা চুপচাপ ফাঁক বন্ধ করল।** ওটা `modules/core/components`-এ থাকে, gate-এর বাইরে — কিন্তু ব্যবহারকারী attendance (৪), payroll (২) আর platform (৩), অর্থাৎ **সবগুলোই migrated module**। ফলে Tailwind screen-এর ভিতরে `badge bg-label-*` মার্কআপ বসত আর gate সেটা ধরত না, কারণ call site-এ কেবল `<ApprovalStatusBadge>` লেখা থাকে। এখন ওটা `ui/StatusBadge`-এর পাতলা মোড়ক: চেহারা বাকি সব badge-এর সঙ্গে এক, আর কেবল approval-এর শব্দভাণ্ডার — বড় status set আর "Pending approval" ভাষাটা — ওখানে থাকে। `PendingApprovalAction`-এর `d-inline-block`-ও সঙ্গে গেছে। দুটো ফাইলই এখন gate-এ তালিকাভুক্ত। কোনো Bootstrap-side caller নেই, তাই পুরনো মার্কআপ ধরে রাখার দরকারও ছিল না।

**`ui/StatusBadge`-এ `title` prop যোগ হয়েছে** — pending badge-এ hover করলে "Pending approval" দেখানোটা approval badge-এর পুরনো আচরণ, সেটা হারাতে দেওয়া হয়নি।

**Request detail drawer-এ পাঁচটা action একসাথে থাকে** — approve · reject · delegate · cancel · comment। প্রথমে ওগুলো একটা helper দিয়ে বানানো হয়েছিল যেটা ভিতরে `useMutation` ডাকত; hook order স্থির থাকলেও **সেটা rules-of-hooks ভাঙে** (helper-টা component নয়, hook-ও নয়), তাই পাঁচটা `useMutation` আলাদা করে লেখা হয়েছে আর কেবল success/error handler দুটো factory-তে রাখা হয়েছে। কোডটা কিছুটা লম্বা, কিন্তু নিয়ম মানে।

**যে কাজ করা যাবে না, সেটা এখন বলে দেয়।** আগের page-এ approver না হলে বোতামগুলো কেবল উধাও হয়ে যেত। এখন একটা লাইন থাকে — "আপনি এই step-এর approver নন, তাই কেবল comment করতে পারবেন" বা "request বন্ধ, কেবল comment যোগ হবে"।

**Settings-এর workflow select কেন approval চালু থাকলে locked** — লিংকটা toggle-এর সঙ্গেই commit হয়, তাই চালু অবস্থায় নীরবে workflow বদলালে live routing বদলে যেত। আচরণটা আগেও এমনই ছিল; এখন কারণটা কোডে লেখা আছে।

---

## Activity Log + Onboarding Policies → new design (13 Aug 2026)

Gate এখন **৩৪টা path** পাহারা দেয়। platform-এ বাকি কেবল **Actions (list + form) আর OTP log** — ঐ তিনটে হলে পুরো module directory হিসেবে বসানো যাবে, ফাইল ধরে ধরে নয়।

### Activity Log

Detail-এর Modal সরে drawer হয়েছে, আর `ActivityLogPropertiesViewer` Tailwind-এ। Viewer-টার তিন স্তরের গঠন অপরিবর্তিত — employee module যে status-change লেখে সেটা আগে, তারপর যে কোনো before/after diff, শেষে সাদামাটা key/value dump; শেষেরটা থাকার কারণেই নতুন কোনো event type এলে ফাঁকা না দেখিয়ে অন্তত ডেটাটা দেখায়।

**এই drawer-টা ইচ্ছাকৃতভাবে URL-driven নয়** — বাকি সবগুলো যেখানে `?param=<id>` ধরে খোলে। কারণ একটামাত্র log আনার কোনো endpoint নেই (`activityLogsApi`-তে আছে শুধু `list` আর `exportCsv`), তাই share করা লিংক এমন একটা সারিতে গিয়ে পড়ত যেটা list-এর প্রথম page-এ না-ও থাকতে পারে। ফাইলে কারণ লেখা আছে।

**হাতে লেখা pagination সরিয়ে `DataTable`-এর `pagination`/`onPageChange` ব্যবহার করা হয়েছে** — response-এর `meta` সরাসরি পাস হয়, তাই Previous/Next-এর নিজস্ব কোড আর নেই।

### Onboarding Policies

`OnboardingPolicyForm.tsx` (২৩৮ লাইন page) মুছে `components/OnboardingPolicyFormDrawer.tsx`; URL — `?mode=create` আর `?mode=edit&policy=<id>`, পুরনো দুটো route redirect হয়। **onboarding module-এর কেবল এই দুটো ফাইল migrated**, বাকিগুলো (OTP · password · documents — ওগুলো onboarding shell-এর নিজস্ব layout) Bootstrap-side, তাই gate-এ ফাইল ধরে তালিকাভুক্ত।

**Delete-এ confirmation যোগ হয়েছে।** এই list-টাই একমাত্র ছিল যেখানে row-এর Delete সরাসরি চলত, কোনো প্রশ্ন ছাড়াই। Policy ঠিক করে কাকে জোর করে onboarding করানো হবে — বাকি সবার মতো একটা confirm step ওটারও প্রাপ্য।

**Step form-এ চারটে flag ফিরে এসেছে।** `is_required` · `is_blocking` · `allow_skip` · `is_active` — চারটেই API payload-এ যেত আর `defaultStep()`-এ hard-code করা ছিল, কিন্তু ফর্মে কোনো field ছিল না, তাই তৈরির পর আর কখনো বদলানো যেত না। (`EmployeeGradeForm`-এর `grade_code`-এর মতোই ফাঁক।)

**Saved step মোছা যায় না, নিষ্ক্রিয় করা যায়।** API-তে policy step delete করার endpoint নেই, তাই যে step একবার save হয়েছে তাকে UI থেকে "সরালে" সেটা আসলে থেকেই যেত — নীরবে। এখন কেবল যে step এখনো save হয়নি তার Remove বোতাম থাকে; save হওয়া step-এ লেখা থাকে "untick Active to retire it"।

---

## Qbits migration: COMPLETE (13 Aug 2026)

**পুরো frontend Tailwind-এ।** Gate এখন ফাইল ধরে ধরে নয়, **দশটা গোটা ডিরেক্টরি** পাহারা দেয়:

```
src/shared/components/ui   src/shared/context      src/modules/auth
src/modules/employee       src/modules/attendance  src/modules/payroll
src/modules/configuration  src/modules/platform    src/modules/core
src/modules/onboarding
```

### শেষ ধাপে যা হলো

**Platform-এর বাকি তিনটে।** `ActionForm.tsx` page মুছে `components/ActionFormDrawer.tsx` (`?mode=create`) — create-only, কারণ API-তে action update করার endpoint নেই আর catalogue-টার মালিক `config/actions.php`; ফাইলে সেটা লেখা আছে। `ActionList` তিনটে section-সহ Tailwind-এ। `OtpLogPage`-এর হাতে লেখা table `DataTable` হয়েছে আর OTP code-টা বড় monospace-এই থাকল — ঐটাই তো screen-টার পুরো কাজ।

**`OtpLogPage`-এর দুটো dev-leftover সরানো হয়েছে:** `queryFn`-এর ভিতরে একটা `console.log` প্রতিটা refetch-এ OTP সহ পুরো response browser console-এ ফেলত (১০ সেকেন্ড অন্তর), আর একটা "Debug:" strip ঐ একই মান UI-তে দেখাত। "Show raw" বোতামটা ইচ্ছাকৃত feature, সেটা আছে — ঐ দুটোর কাজ ওটাই করে।

**Onboarding।** চারটে step page (mobile OTP · email OTP · password · documents) Sneat-এর `authentication-wrapper` ছেড়ে নতুন `components/OnboardingCard.tsx`-এ বসেছে — **login screen-এর হুবহু একই lockup**, কারণ ব্যবহারকারী sign-in-এর ঠিক পরেই এই পর্দাগুলো দেখে, আর সেখানে অন্যরকম দেখতে পাতা মানে অন্য একটা product। `AppBrand.tsx` মুছে গেছে; ওর একমাত্র কাজ ছিল এই চারটেকে brand দেখানো, এখন `BrandMark` করে। Route guard দুটো (`OnboardingRoute` · `RequirePermissionRoute`) আর `DocumentWarningBanner`-ও সঙ্গে গেছে।

**Password পাতায় একটা ছোট আচরণ যোগ হয়েছে** — দুটো password না মিললে এখন field-এ লেখা ওঠে আর Save নিষ্ক্রিয় থাকে। আগে দুটো আলাদা মান নিয়েই submit হতো, আর ভুলটা জানা যেত server-এর উত্তর আসার পর।

### `shared/components/common/*` এখন সম্পূর্ণ অব্যবহৃত

বারোটা ফাইল — `DataTable` · `Modal` · `Offcanvas` · `FormInput` · `FormSelect` · `PageHeader` · `StatusBadge` · `EmptyState` · `ConfirmDialog` · `TextDivider` · `IconPicker` · `offcanvasStack` — **কোনো ফাইল আর এগুলো import করে না** (`rg "components/common/"` ফাঁকা)। পুরো Bootstrap-side component library এখন মৃত কোড।

**মোছা হয়নি।** cleanup আলাদা কাজ, আর সেটা বলার আগে শুরু না করার নির্দেশ আছে। এখানে কেবল রেকর্ড করা হলো যে ওটা এখন নিরাপদে মোছা যায়: `bootstrap` / `bootstrap-icons` npm dependency, `erpflow.scss`-এর Bootstrap import, `check-bootstrap.sh`, আর `useLayoutHtmlClass('auth')`-এর মতো Sneat-যুগের hook-গুলোও ঐ একই কাজের অংশ হবে।

---

## style-boundary gate কোনোদিন চলেনি — এখন চলে (13 Aug 2026)

Bootstrap সরানোর পরিকল্পনা করতে গিয়ে ধরা পড়ল: **`scripts/check-style-boundary.sh` একটা কাজও করেনি।**

স্ক্রিপ্টটা `rg` (ripgrep) ডাকত, আর **frontend container-এ `rg` ইনস্টল করা নেই** — যেখানে `npm run build` ওটা চালায়:

```
$ docker compose exec -T frontend sh -c 'command -v rg'   →  কিছু না
```

প্রতিটা `rg` call ব্যর্থ হতো, `2>/dev/null` stderr গিলে ফেলত, তাই `report()`-এর `if OUT=$(rg …)` সবসময় false আর rule 3-এর `UNPREFIXED` সবসময় ফাঁকা। এরপর স্ক্রিপ্ট ছাপত `✓ style boundary clean (N path(s) checked)` — **path সংখ্যাটা আসল** (`[ -e "$d" ]` দিয়ে গোনা, তাই migration-এর সঙ্গে সঙ্গে বাড়ত আর বিশ্বাসযোগ্য দেখাত), **চারটে rule-ই ভুয়া**। Host-এও একই: shebang `#!/usr/bin/env sh`, আর `sh`-এর PATH-এ `rg` নেই।

মানে এই migration-এর প্রতিটা ধাপে "gate clean" বলে যা রিপোর্ট করা হয়েছে, তার কোনোটাই প্রমাণ ছিল না।

### ফাঁকটা আসলে কতটা খরচ করাল — প্রায় কিছুই

চারটে rule হাতে চালিয়ে (python, দশটা ডিরেক্টরির ৩০৩টা ফাইলে) যা পাওয়া গেল:

| Rule | হিট |
|---|---|
| Bootstrap class | **০** |
| Hard-coded `tw:z-[…]` | **০** |
| `bi bi-` | ২টা, দুটোই doc comment (`attendance/utils/attendanceTypeIcon.ts:120`, `platform/utils/moduleIcon.ts:58` — "Tolerates both the `bi bi-foo` and bare `bi-foo` forms") |
| Unprefixed token | **১টা আসল bug** |

migration নিজে সত্যিই পরিষ্কার ছিল — gate না থাকা সত্ত্বেও।

**আসল bug-টা:** `last:tw:border-b-0`, `attendance/components/corrections/CorrectionRequestForm.tsx:66` আর `attendance/pages/corrections/CorrectionRequestDetailPage.tsx:35`-এ। Tailwind v4-এ prefix সবার বাইরে বসতে হয় (`tw:last:border-b-0`), নইলে কোনো CSS emit হয় না — শেষ সারির border কখনো যেত না। **নতুন gate ওটা তার প্রথম run-এই ধরেছে**, যেটাই সবচেয়ে ভালো প্রমাণ যে এখন সত্যি চলছে।

### যা বদলাল

**POSIX tool ছাড়া কিছু নয়।** `rg` → `find` + `grep -nHE`। BusyBox grep-এ `-H` · `-o` · `-E` · ERE-র `\b` সবই আছে, container-এ যাচাই করা।

**চুপচাপ ব্যর্থ হওয়ার তিনটে পথ বন্ধ:**
- `require()` — `find grep sed sort tr wc`-এর একটাও না পেলে বার্তা দিয়ে `exit 1`
- `2>/dev/null` সরানো; grep-এর non-zero exit এখন `|| true` দিয়ে সামলানো, তাই "কোনো hit নেই" আর "চলেইনি" আলাদা
- ফাইল তালিকা ফাঁকা হলে **clean বলতে অস্বীকার করে** — `refusing to report clean`, exit 1

**Summary line এখন ফাইল সংখ্যাও বলে** — `10 path(s), 303 file(s) checked`। শূন্য-scan আর চোখ এড়াবে না; আগের লাইনটায় কেবল path গোনা হতো বলেই ভাঙা অবস্থাটা এত দিন বিশ্বাসযোগ্য দেখিয়েছে।

**Rule 2 এখন `className=` দিয়ে anchored** — markup পড়ে, prose নয়। আগে গোটা লাইনে `bi bi-` খুঁজত, তাই দুই icon resolver-এর ব্যাখ্যামূলক comment violation হিসেবে ধরা পড়ত।

### যাচাই

Dashboard-এ একটা করে ভুল class ঢুকিয়ে চারটে rule আলাদাভাবে fire করানো হয়েছে, প্রতিবার ফাইল restore করে (`git diff` ফাঁকা):

| ঢোকানো | কে ধরল |
|---|---|
| `d-flex` | rule 1 |
| `col-md-6` | rule 1 |
| `bi bi-star` | rule 2 |
| `tw:z-[9999]` | rule 3 |
| `flex` | rule 4 |

Missing-tool পথ: `require rg` → বার্তা সহ `exit 1`। শেষে `npm run build` → pass।

### শিক্ষা

একটা গেট যা ব্যর্থ হলে সবুজ দেখায়, সেটা গেট নয় — সাজসজ্জা। নতুন কোনো build check লিখলে **ভাঙা অবস্থাটা আগে দেখে নিতে হবে**: ইচ্ছে করে একটা violation ঢুকিয়ে লাল হতে দেখা, তারপরই সবুজটাকে বিশ্বাস করা।

---

## Bootstrap + Sneat সরানো হয়েছে (13 Aug 2026)

Frontend এখন **কেবল Tailwind**। Branch: `remove-bootstrap`।

### ফল

| | আগে | পরে |
|---|---|---|
| `dist/assets/*.css` | 443,989 B | **50,957 B** (−88%) |
| JS bundle | 1,804,960 B | **1,714,895 B** (−90 KB, `bootstrap.bundle.min.js`) |
| Icon font | 314 KB (woff + woff2) | **০** — একটাও glyph render হতো না |
| CSS-এ Bootstrap selector | ২,৪৮৪ | **০** |

### কী কী গেল

**Dead code:** `shared/components/common/` (১২ ফাইল, ০ importer) · `useLayoutHtmlClass.ts` + ৬টা call site (যে `<html>` class সেট করত তার কোনো CSS selector আর পৌঁছাত না) · `src/assets/sneat/` (৩৯টা unreferenced ছবি) · তিনটে কখনো import না হওয়া `sneat/pages/*.scss`।

**Bootstrap নিজে:** `main.tsx`-এর দুটো import · `app.scss` · `erpflow.scss` · `src/styles/sneat/` (৮৭ ফাইল, ৭,৪৮৫ লাইন) · `index.html`-এর Sneat `<html>` attribute আর bootstrap-icons CDN link · `vite.config.ts`-এর পুরো scss preprocessor block · `package.json`-এর `bootstrap` · `bootstrap-icons` · `@popperjs/core` · `sass` আর `overrides` block · `check-bootstrap.sh` আর `docker-entrypoint.dev.sh`-এ তার call।

### `tw:` prefix গেছে — ১৯০ ফাইলে ৭,০৬২ জায়গা

Prefix-টা **কেবল** Bootstrap-এর সঙ্গে class-name সংঘর্ষের জন্য ছিল (Bootstrap-এর `px-4` = 1.5rem, Tailwind-এর 1rem, আর Bootstrap তার utility-তে `!important` দিত)। সংঘর্ষটাই নেই, তাই prefix-ও নেই। `cn.ts`-এর `extendTailwindMerge({ prefix: 'tw' })` সরিয়ে সাধারণ `twMerge` হয়েছে — নইলে tailwind-merge আর conflicting class চিনত না।

### Reboot → preflight — এটাই ছিল আসল ঝুঁকি

`tailwind.css` preflight import করত না, কারণ Bootstrap Reboot base reset-এর কাজ করত; তার বদলে হাতে লেখা একটা "scoped preflight" block (`element:where([class*='tw:'])`) ঘাটতি পোষাত। এখন `@import 'tailwindcss'` পুরোটা আনে — theme + preflight + utilities, layered — আর ঐ হাতে লেখা block মুছে গেছে। **এতে প্রতিটা element-এর default বদলায়**, তাই manual pass লাগবে (নিচে দেখুন)।

### z-index scale ছোট করা হয়েছে

Token-গুলো Bootstrap-এর stacking range (modal 1055, আর `app.scss` backdrop-কে 1090-এ ঠেলত) পেরোনোর জন্য 1200–1600-এ বসানো ছিল। এখন 100–600। `overlayStack.ts`-এর hard-coded base দুটোও (1320/1325 → 320/325) সঙ্গে নামানো হয়েছে — সম্পর্কগুলো অবিকল একই।

### Gate-টা বদলেছে, ওঠেনি

`check-style-boundary.sh` এখন **পুরো `src`** দেখে (আগে ১০টা ডিরেক্টরির তালিকা), আর rule 3 (prefix) অবসরে গেছে। বাকি তিনটে থাকল কারণ ওগুলোর কাজ বদলায়নি: পুরনো snippet git history · বন্ধ PR · task card থেকে copy হয়ে ফেরে, আর pasted `class="btn btn-primary"` এখন **নীরবে unstyled text** হবে, জোরে ভাঙবে না। z-index rule-ও থাকল।

### বাকি যা ইচ্ছাকৃতভাবে রইল

- `modules.icon` আর attendance type-এর `bi-*` **নাম** DB-তে আছে — render-time-এ lucide-তে map হয় (`moduleIcon.ts` · `attendanceTypeIcon.ts`)। **কোনো data migration লাগেনি**, font সরানোয় কিছু বদলায়নি।
- দুটো doc comment (`useOnClickOutside` · `Dropdown`) বলে ওরা `bootstrap.bundle.js`-এর কোন আচরণের বদলি — কেন component-টা আছে সেটার ব্যাখ্যা, তাই রইল।
- `public/assets/sneat/` → **`public/assets/illustrations/`** নাম বদলেছে; Dashboard-এর একমাত্র ছবিটা ওখানেই।

### যাচাই

`npm run build` pass — `tsc -b` ০ error, style check clean (৩১৫ ফাইল), vite build ✓। `oxlint src` — ০ error, ২৫টা warning, সবগুলোই আগে থেকেই ছিল।

**Manual pass এখনো বাকি** — frontend-এ কোনো test suite নেই, আর preflight পুরো app-এর element default বদলায়। দেখতে হবে: login → onboarding step → dashboard → employee profile drawer → attendance records → payroll salary structure → configuration list + drawer → platform roles + permission drawer। বিশেষ করে typography scale · list bullet · `<hr>` divider · button reset · table spacing। আর ID card preview-র print check (ওর scoped `<style>` Reboot-এর `border-collapse` ধরে লেখা হয়েছিল)।

---

## `POST /leave-requests` → 500: dev DB-তে migration চলেনি (16 Aug 2026)

**উপসর্গ।** একই payload বারবার পাঠালেও প্রতিবার:

```json
{ "success": false, "message": "Failed to submit leave request.", "errors": {} }
```

**কোড bug নয় — dev database-টা পিছিয়ে ছিল।** `Modules/Attendance/database/migrations/2026_08_12_150000_create_leave_request_days_table.php` (5.3a-BE, সারি ৩০-এ যে migration-টার কথা লেখা আছে) কখনো চালানো হয়নি, তাই `erpflow.leave_request_days` টেবিলটাই ছিল না।

### পথটা কীভাবে ওখানে গিয়ে ঠেকে

Company-তে ঐ module action-এর জন্য approval enabled নেই, তাই `ApprovalGateway::submit()` **documented bypass** নেয় — pending request না বানিয়ে `onApproved` সঙ্গে সঙ্গে চালায়, অর্থাৎ `LeaveRequestExecutor::executeLeaveApproval()` **submit request-এর ভেতরেই inline** চলে। ওখানে প্রতিটা counted working day-তে `LeaveRequestDay::create()` হয় → নেই-টেবিলে query → `QueryException` (SQLSTATE 42S02)।

মানে **approval on থাকলে বাগটা লুকিয়ে থাকত** — request শুধু pending হয়ে বসে থাকত, day row লেখার চেষ্টাই হতো না।

### `errors` ফাঁকা কেন, message এত অস্পষ্ট কেন

`ApiResponseTrait::handleException()` চেনে চারটে জিনিস — `ValidationException` (422), `DomainException`, `HttpExceptionInterface`, আর `statusCode()` আছে এমন exception। `QueryException` এর কোনোটাই নয়, তাই শেষ শাখায় পড়ে: `report()` + **fallback message, ফাঁকা `errors`, 500**। ঠিক এই কারণেই response-টা কিছুই বলে না — এটা প্রত্যাশিত আচরণ, ভুল নয়।

আসল বার্তাটা `Log::error` দিয়ে লেখা হয়েছিল, কিন্তু `storage/logs/laravel.log` ইতিমধ্যে খালি করে ফেলা হয়েছিল (০ byte), তাই trace-টা হারিয়ে গিয়েছিল। **logging নিজে ঠিকই আছে** — probe করে দেখা হয়েছে, লেখে।

### লক্ষণটা যেভাবে বদলাল, সেটাই সবচেয়ে বড় সূত্র

nginx access log-এ পুরো গল্পটা আছে:

| সময় (13 Aug) | request | status |
|---|---|---|
| 11:57–11:59 | `POST /leave-requests` × ৯ | **422** |
| 12:03:12 | `POST /attendance/assignments` | 201 |
| 12:03:41 → 12:10:26 | `POST /leave-requests` × ৪ | **500** |

আগের 422-গুলো ছিল `Selected policy is not assigned to the employee.`। 12:03-এ leave policy assign করার পর `evaluate()` পাশ করতে শুরু করল, আর তখনই request প্রথমবার write path-এ ঢুকল — সেখানেই 500। **কোনো leave request কোনোদিন সফল হয়নি**; মাঝের 201-টা leave request ছিল না, ছিল assignment তৈরি।

### data কি নষ্ট হয়েছে

না। `store()` পুরোটা `DB::transaction()`-এর ভেতরে, তাই প্রতিবার সম্পূর্ণ rollback হয়েছে — `leave_requests` ফাঁকা, `leave_balance_ledger` ০ সারি, কোনো balance কাটা যায়নি। শুধু `AUTO_INCREMENT` ৫-এ উঠে আছে (rollback হওয়া id পুড়েছে) — এটা MySQL-এর স্বাভাবিক আচরণ, সমস্যা নয়।

### যা করা হয়েছে

**কোনো code change নেই।** dev DB-তে বাকি পড়ে থাকা দুটো migration চালানো হয়েছে (দুটোই purely additive — একটা নতুন table, একটা nullable column):

```
2026_08_12_140000_add_attachment_history_to_employee_identities_table   DONE
2026_08_12_150000_create_leave_request_days_table                       DONE
```

### যাচাই

- `php artisan test` — `LeaveRequestApiTest` · `LeaveApprovalExecutionTest` · `LeaveRequestServiceTest` → **36 passed (110 assertions)**
- আসল dev DB-তে `LeaveRequestService::store()` সরাসরি ডেকে end-to-end: request তৈরি হয়, bypass path-এ `approved` হয়, `total_days=1.00`, `leave_request_days` ১ সারি। যাচাইয়ের transaction rollback করা হয়েছে, **DB-তে কিছু রেখে আসা হয়নি**।

### শিক্ষা

**test সবুজ থাকা dev DB-র কোনো প্রমাণ নয়।** test suite `RefreshDatabase` দিয়ে প্রতিবার শূন্য থেকে migrate করে, তাই missing-migration শ্রেণির bug ওখানে **কখনোই** ধরা পড়বে না — ৩৬টা test শুরু থেকে শেষ অবধি সবুজই ছিল। schema ছোঁয় এমন কাজের পর `php artisan migrate:status` দেখাটাই একমাত্র জায়গা যেখানে এটা ধরা পড়ে।

**ফাঁকা `errors` + 500 = অচেনা exception**, business rule violation নয়। rule ভাঙলে 422/409 আসত বার্তা সহ। ও দুটো দেখলে response নয়, **log-এ** যেতে হবে — আর log যেন সত্যিই লেখা থাকে।

---

## `5.5-BE` merged — cross-lane gate ভেঙে (16 Aug 2026)

**PR #185** (`att-5-5-be-correction-approval`, Bablu) main-এ আছে, অথচ doc-এ card-টা `pending`/`▶ এখন` লেখা ছিল। এখন `done` করা হয়েছে।

### কী এসেছে

`AttendanceCorrectionExecutor` (+331 লাইন) পাঁচটা request type-ই সামলায় — `missing_in` · `missing_out` · `incorrect_time` · `wrong_status` · `other`। মূল নকশাটা ঠিক আছে: **কোনো punch row কখনো update হয় না, মোছেও না** — শুধু `superseded_by_id` বসে, নতুন manual punch যোগ হয়, আর superseded punch recalculation থেকে বাদ পড়ে। তাই original punch history অক্ষত ও auditable থাকে, যেটাই card-এর মূল দাবি ছিল।

সঙ্গে: `CorrectionDecided` event, locked-month override (`attendance.correction-override-lock` ছাড়া 403), reject-এ বাধ্যতামূলক reason (নইলে 422), finalised request দ্বিতীয়বার decide করলে আটকানো, আর `ApprovalExecutorInterface`-এ `reject()` যোগ হওয়ায় `LeaveRequestExecutor`/`MonthlyAttendanceExecutor`-ও ছোঁয়া হয়েছে।

**Route-এ একটা আসল ফাঁক বন্ধ হয়েছে:** `GET /correction-requests` আর `/{id}` আগে কেবল `attendance.correction-create`-এর ভিতরে ছিল, তাই **HR approver নিজের queue-টাই দেখতে পেত না**। এখন `correction-create|correction-approve` — 5.5-FE এটার উপরেই দাঁড়াবে।

**DoD যাচাই — কার্ডের তিনটে item-ই ঢাকা:**

| DoD | ঢাকা? |
|---|---|
| পাঁচ type-ই implemented · registered · tested | ✅ পাঁচটা আলাদা test |
| punch কেবল `superseded_by_id`-তে বদলায়, কিছু মোছে না | ✅ নির্দিষ্ট test আছে |
| locked-month override — permitted ও forbidden দুই caller | ✅ দুটোই আছে |

`php artisan test Modules/Attendance/tests/Feature/AttendanceCorrectionExecutorTest.php` → **15 passed (64 assertions)**।

### ⚠ Gate ভাঙা হয়েছে

Cross-lane নিয়ম ছিল: **`5.5-BE` merge হবে `9.1-BE` (Ashraful)-এর পরে** — কারণ দুটোই recalculation path ছোঁয়। **`9.1-BE` এখনো লেখাই হয়নি** (কোনো console command নেই, `AttendanceServiceProvider::configureSchedules()` এখনো commented stub, `routes/console.php`-এ কেবল তিনটে approval job), অথচ `5.5-BE` merged।

**ঝুঁকিটা এখন উল্টো দিকে গেছে, তবে ছোট।** গেটটার উদ্দেশ্য ছিল Bablu-কে স্থির recalculation path-এর উপর লিখতে দেওয়া; এখন উল্টে Ashraful `9.1-BE` লিখবেন একটা **ইতিমধ্যে বদলে যাওয়া** path-এর উপরে। বাস্তবে `5.5-BE` `calculateDaily`-র ভিতরটা বদলায়নি — শুধু superseded punch বাদ দিয়ে ওটাকে **ডাকে**। তাই merge conflict নয়, **semantic overlap**-টাই দেখার জিনিস: nightly close যখন গোটা দিনের সব employee-র উপর `calculateDaily` চালাবে, তখন সদ্য-approved correction-এর সাথে সময়ের দৌড় (কে আগে লিখল) নিয়ে ভাবতে হবে।

**Ashraful-এর জন্য:** `9.1-BE` শুরুর আগে `AttendanceCorrectionExecutor::apply()` একবার পড়ে নিন — ওটা কীভাবে punch supersede করে recalculate ডাকে, nightly job-কে ঠিক সেই contract-ই মানতে হবে।

> **পরে যা হয়েছে (16 Aug 2026):** `9.1-BE` এর পরেই merge হয়েছে (PR #187), তাই ক্রমটা কার্যত মিলে গেছে — শুধু উল্টো দিক থেকে। উপরে যে semantic overlap-এর আশঙ্কা করা হয়েছিল সেটা **এখনো পরীক্ষিত নয়**: nightly close প্রতিটা employee-র জন্য `calculateDaily` ডাকে, আর correction approve-ও তাই করে, কিন্তু দুটো একসাথে চললে কী হয় তা নিয়ে একটাও test নেই।

### শিক্ষা

**Gate তখনই কাজ করে যখন merge-এর সময় কেউ তাকায়।** এটা doc-এ লেখা ছিল, তবু ভাঙা গেছে — কারণ গেটটা কেবল কাগজে, PR-এ কোনো check ছিল না। এ ধরনের ordering constraint হয় PR template-এর checklist-এ তুলুন, নয় স্বীকার করুন যে এটা advisory।

---

## `5.3b`-র ঋণ শোধ — PR #186 (16 Aug 2026)

`att-5-3b-be-punch-voids-leave` merged (Munna)। **তিনটে DoD-ঋণই মিটেছে** — বিস্তারিত উপরের "5.3b-এর ঋণ" টেবিলে। সংক্ষেপে: `calculateDaily` এখন transactional, `LeaveDayVoided` একবারই fire হয়, আর `PunchLeaveVoidTest`-এ **7 test green (32 assertion)** DoD-র চারটে case ঢাকে।

নকশার দিক থেকে লক্ষ করার মতো একটা জিনিস: `calculateDaily` transactional করার সাথে সাথেই re-entrancy সমস্যা তৈরি হয় (ওর ভিতর থেকে `voidLeaveDay()` → আবার `calculateDaily`)। সমাধান এসেছে `voidLeaveDay()`-এ **default-`true` তৃতীয় parameter** দিয়ে, তাই বাইরের কোনো caller-এর signature ভাঙেনি — backward-compatible পথটাই নেওয়া হয়েছে।

---

## 🔴 Attendance test suite-এ ২৫টা test লাল — PR #186-এর দোষ নয় (16 Aug 2026)

`5.3b`-র ঋণ যাচাই করতে গিয়ে ধরা পড়ল: **HEAD-এ attendance-সংক্রান্ত ২৫টা test fail করছে।** Doc জুড়ে যে "N test green" লেখা আছে, সেগুলো merge-এর সময় সত্যি ছিল — **আজ আর নয়**।

| Suite | ফল |
|---|---|
| `AttendanceCorrectionExecutorTest` | ✅ 15 |
| `PunchLeaveVoidTest` | ✅ 7 |
| `LeaveRequestServiceTest` · `LeaveApprovalExecutionTest` · `LeaveRequestApiTest` | ✅ |
| **`PunchServiceTest`** | ❌ 12 |
| **`PunchApiTest`** | ❌ 7 |
| **`LeaveBalanceServiceTest`** | ❌ 3 |
| **`AttendanceDailyCalculationTest`** | ❌ 2 |
| **`AssignmentResolutionServiceTest`** | ❌ 1 |

**মোট: 69 passed, 25 failed।**

### PR #186 এটা ঘটায়নি — যাচাই করা হয়েছে

`AttendanceRecordService.php` ফাইলটা সাময়িকভাবে **#186-এর আগের version-এ** ফিরিয়ে (`git show 1f8cf9d2:…`) `AttendanceDailyCalculationTest` চালানো হয়েছে — **একই ২টা test তখনও fail করেছে**। ফাইল সঙ্গে সঙ্গে restore করা হয়েছে (`git diff` ফাঁকা)। এছাড়া #186 `PunchService` · `LeaveBalanceService` · `AssignmentResolutionService` — একটাও ছোঁয়নি। **অর্থাৎ ফাটলটা আগে থেকেই ছিল, শুধু কেউ suite-টা চালায়নি।**

### দুটো আলাদা কারণ

**১. Timezone contract-এর অমিল** (`AttendanceDailyCalculationTest`-এর ২টা)। Test `punch_time` বসায় naive local string হিসেবে — `'2026-08-10 09:45:00'`। `calculateDaily` শিফটের সময়কে company timezone ধরে UTC-তে নেয় (`09:00 Asia/Dhaka → 03:00 UTC`), কিন্তু `punch_time` পড়ে `Carbon::parse()` দিয়ে, অর্থাৎ **UTC ধরে নেয়**। তাই `late_minutes` = `09:45 − 03:00 − 15 grace` = **390**, প্রত্যাশিত 30। ব্যবধানটা হুবহু **৩৬০ মিনিট = Asia/Dhaka-র UTC+6** (companies টেবিলের default)।

> এটা কেবল test-এর দোষ কিনা এখনো নিশ্চিত নয় — **punch UTC-তে জমা হওয়ার কথা**, তাই হয় test ভুল করে local সময় বসাচ্ছে, নয় production path কোথাও একই ভুল করছে। **`9.1-BE` (nightly close) এই গণনার উপরেই দাঁড়াবে, তাই আগে এটা মীমাংসা করা দরকার।**

**২. বাসি test কোড** (বাকি ২৩টা)। `LeaveBalanceServiceTest` `LeaveBalanceService`-কে **১টা constructor argument** দিয়ে বানায়, service এখন **২টা** চায় (`ArgumentCountError`) — service বদলেছে, test বদলায়নি। Punch-এর ১৯টা fail করে `No active shift found for the given date` নিয়ে; ঐ test-গুলো assignment বসায় `effective_date => now()`, তাই যেসব case punch-কে **আগের দিনে** ফেলে সেখানে কোনো active shift মেলে না — **তারিখের উপর নির্ভরশীল, ভঙ্গুর test**।

### কেন এতদিন চোখে পড়েনি

প্রত্যেকে **নিজের card-এর test file** চালিয়েছে, গোটা module নয় — তাই প্রতিটা PR সৎভাবেই "N test green" রিপোর্ট করেছে, অথচ পাশের suite ততক্ষণে লাল। CI-তে গোটা suite চলে না।

**সুপারিশ:** `6.1-BE`/`9.1-BE` শুরুর আগে এটা সারানো হোক। দুটোই `calculateDaily`-র উপরে বসে, আর ভুল `late_minutes` → ভুল status ladder → ভুল মাস বন্ধ → **ভুল payroll**। ২৩টা বাসি test সারানো যান্ত্রিক কাজ; timezone-এরটা আসল সিদ্ধান্ত চায়।

**শিক্ষা:** "আমার test সবুজ" আর "suite সবুজ" এক জিনিস নয়। Card `done` মার্ক করার আগে অন্তত **module-এর গোটা suite** একবার চালান — এখানে ফাঁকটা কয়েকটা PR ধরে জমে ২৫-এ পৌঁছেছে, অথচ প্রতিটা রিপোর্টই আলাদাভাবে সত্যি ছিল।

---

## `9.1-BE` Nightly Attendance Close merged — PR #187 (16 Aug 2026)

`att-9-1-be-nightly-close` (Ashraful) main-এ। **Part J (Automation)-এর প্রথম card** — এতদিন Attendance module-এ একটাও console command বা scheduled job ছিল না।

### যা এসেছে, এবং নকশাটা কেমন

**Timezone-প্রতি tenant।** `ScheduleCloseDayCommand` **hourly** চলে (`routes/console.php`), আর প্রতিটা active company-র জন্য দেখে ওর নিজের timezone-এ এখন `hour === 2` কিনা। হলে আগের দিনের জন্য batch ছাড়ে। এক cron slot দিয়ে বহু timezone সামলানোর সহজ ও সঠিক উপায় — প্রতি company-র জন্য আলাদা cron লাগে না।

**Idempotency নকশাতেই বসানো।** `chunkActiveWithoutAttendance()` কেবল সেই employee-দের তোলে যাদের ঐ তারিখে `attendance_records` row **নেই** (`whereDoesntHave`)। তাই job দুবার চললেও দ্বিতীয়বার কাউকে পায় না — flag বা lock লাগেনি, query-টাই কাজটা করছে। পরিষ্কার সমাধান।

**Batch + chunk।** ২০০-র chunk-এ `ProcessCloseDayChunkJob`, Laravel batch-এর উপরে; `$this->batch()?->cancelled()` দেখে, তাই চলন্ত batch বাতিল করা যায়। Manual trigger `POST /attendance/jobs/close-day`, অগ্রগতি `GET /attendance/jobs/close-day/{batchId}`। Locked period হলে dispatch-ই হয় না।

### ⚠ তিনটে DoD-ঋণ

Card তিনটে জিনিস চেয়েছিল, **তিনটেই বাকি**:

| DoD | অবস্থা |
|---|---|
| দুই ভিন্ন timezone-এর দুই tenant নিয়ে scheduler test | ❌ **PR-এ একটাও test ফাইল নেই** |
| Job দুবার চালিয়ে identical state assert করা idempotency test | ❌ নকশা ঠিক মনে হয়, কিন্তু **অপ্রমাণিত** |
| Unassigned employee HR-এর পড়ার মতো report-এ | ❌ কেবল `Log::info('attendance.day_closed')`-এ — card স্পষ্ট বলেছিল "not only in logs" |

তৃতীয়টা নিছক ঋণ নয়, **`9.4-FE`-র নির্ভরতা** — ঐ card-এর "unassigned report" screen-টা পড়ার মতো কোনো উৎস এখনো নেই।

### দুটো আলাদা ঝুঁকি, নজরে রাখুন

**১. মিস করলে catch-up নেই।** Command শুধু দেখে "এখন কি 2টা বাজে"। কোনো কারণে ঐ ঘণ্টার run মিস হলে (queue বন্ধ, deploy, worker restart) ঐ company-র ঐ দিনটা **নিঃশব্দে বন্ধ হবে না** — পরের দিন আর ফিরে দেখে না। DST-তে spring-forward হলে 2টা ঘণ্টাটাই থাকে না, তখনও একই ফল।

**২. লাল ভিতের উপরে বসেছে।** Nightly close প্রতিটা employee-র জন্য `calculateDaily` ডাকে — ঠিক যে function-এর timezone আচরণ নিয়ে উপরের নোটে সন্দেহ তোলা হয়েছে (`late_minutes` 390 বনাম 30)। **Suite এখনো ২৫টা লাল নিয়েই দাঁড়িয়ে** (#187-এর পরেও অপরিবর্তিত), অর্থাৎ যে gate-টা "`6.1-BE`/`9.1-BE`-র আগে সবুজ করুন" বলেছিল, সেটা মানা হয়নি।

### Ashraful-এর পরের কাজ

**`6.1-BE` Monthly Attendance Approval (Seq 37, 8 pts)** — payroll run-এর গেট। তবে শুরুর আগে দুটো gate:

- 🔴 **২৫টা লাল test** — `6.1-BE` মাস বন্ধ করে, আর সেটা দাঁড়ায় প্রতিদিনের status ladder-এর উপরে। ভুল `late_minutes` → ভুল status → ভুল মাস → ভুল payroll।
- **`3.2-BE`-র টেস্ট-ঋণ** — session-pairing ও visibility-scoping, আগে থেকেই `6.1-BE`-র আগে শোধ করার শর্ত।

`9.1-BE`-র তিনটে ঋণও ওর নিজের খাতায় রইল; `9.4-FE` unblock করতে অন্তত unassigned report-টা লাগবে।

---

## Employee contact tab-এ user-এর email/mobile default (16 Aug 2026)

Attendance/Payroll lane-এর বাইরের ছোট কাজ, তবু নথিতে থাকুক। User create-এর সময় দেওয়া `email` ও `mobile` এখন Employee profile-এর **Contact info** tab-এ default হয়ে আসে।

### যা বদলেছে

- `Modules/Employee/app/Http/Resources/EmployeePersonalInfoResource.php` — `user` block-এ `mobile` যোগ (additive)।
- `frontend/src/modules/employee/components/profile-tabs/ContactInfoTab.tsx` — contact record না থাকলে `official_email` ← `user.email`, `mobile_no` ← `user.mobile`।
- `frontend/src/modules/employee/api/employeeCoreApi.ts` — type-এ `mobile` যোগ।

### নকশার সিদ্ধান্ত

Default-টা **শুধু prefill**, backend-এ চাপিয়ে দেওয়া নয়। কারণ official email আর login email এক না-ও হতে পারে; `EmployeeContactService`-এর uniqueness ও validation অপরিবর্তিত রইল। টাইপ-করা মান কখনো overwrite হয় না, আর contact save হয়ে গেলে সব সময় DB-র মানই দেখায়।

### যাচাই

`tsc --noEmit` পরিষ্কার; `php -l` পরিষ্কার। Employee module-এ contact/profile-এর কোনো test ফাইল নেই — এই আচরণের কোনো automated coverage নেই।

---

## Emergency phone ≠ employee-এর নিজের নম্বর (16 Aug 2026)

Employee module-এর contact validation শক্ত করা হলো। Emergency contact phone এখন `mobile_no` **ও** `alternative_mobile_no` — কোনোটার সাথেই মিলতে পারবে না।

### কেন

Emergency contact-এর গোটা উদ্দেশ্যই হলো employee-কে না পেলে অন্য একজনকে পাওয়া। ঐ নম্বরটা যদি employee-রই দ্বিতীয় নম্বর হয়, record-টা দেখতে পূর্ণ লাগে কিন্তু জরুরি মুহূর্তে অকেজো — validation ছিল অর্ধেক, `mobile_no` ঢাকত, `alternative_mobile_no` ঢাকত না।

### যা বদলেছে

- `Modules/Employee/app/Services/EmployeeContactService.php` — create/update-এ ছড়ানো দুই copy সরিয়ে একটাই `assertEmergencyPhoneIsIndependent()`; দুই পথে নিয়ম আলাদা হয়ে যাওয়ার সুযোগ বন্ধ।
- `frontend/.../ContactInfoTab.tsx` — client-side-এ একই নিয়ম, তাৎক্ষণিক field-error।
- `tests/Feature/EmployeeContactValidationTest.php` — নতুন, ৫টা test।

### যে দিকটা খেয়াল রাখা হয়েছে

Update-এ collision **দুই দিক থেকেই** ধরা পড়ে: emergency phone বদলে stored alternative-এর সমান করলে যেমন 422, তেমনি alternative বদলে stored emergency-র সমান করলেও 422। Payload-এ না-আসা field-এর জন্য DB-র মান নেওয়া হয়, নইলে দ্বিতীয় ক্ষেত্রটা ফাঁক গলে বেরিয়ে যেত।

পুরনো `employee.contacts.emergency_phone_must_differ` flag-ই দুটো check নিয়ন্ত্রণ করে — নতুন config যোগ হয়নি, তাই backward compatible।

### যাচাই

`php artisan test tests/Feature/EmployeeContactValidationTest.php` → **5 passed (15 assertions)**। `tsc --noEmit` পরিষ্কার।

Employee import path (`EmployeeImportService`) service নয়, সরাসরি repository ব্যবহার করে এবং emergency/alternative field লেখে না — import-এ কোনো প্রভাব নেই।

---

## Address tab-এ geo cascade — division/district/upazila/postal code (16 Aug 2026)

Attendance/Payroll lane-এর বাইরের কাজ, নথিতে রইল। Employee profile-এর Address tab-এ চারটে ক্ষেত্র এখন cascading dropdown।

### আগে কী ছিল

Division আর District — প্রতিটায় **একটা করে hard-coded option** (`{ value: '1', label: 'Dhaka' }`)। Upazila আর Postal Code — খালি text box। অর্থাৎ `employee_addresses`-এর `division_id`/`district_id` কলামে যা জমত তার কোনো মানেই ছিল না, কারণ ওগুলো কোনো টেবিলকে নির্দেশ করত না।

### এখন

**Division → District → Upazila → Post office**, প্রতিটা ধাপ আগেরটার উপর নির্ভরশীল। Post office বাছলে postal code বসে; যে upazila-তে একটাই post office, সেখানে নিজে থেকেই বসে যায়।

### ডেটার উৎস ও দুটো সংশোধন

Root-এর `DatabaseSeeder.php` (Laravel 4 আমলের, এই app-এ চলত না) → `backend/database/data/bangladesh-geo.json`।

| সমস্যা | কী করা হয়েছে |
|---|---|
| Meherpur/Narail/Satkhira — Khulna **ও** Sylhet দুই জায়গায়, ৩৯টা row হুবহু copy | Sylhet-এর কপি বাদ; তিনটেই Khulna-র |
| ১৫টা নাম ছোট হাতের অক্ষরে শুরু | শুধু প্রথম অক্ষর capital (dropdown label বলে) |

ফল: **৭ / ৬৪ / ৪৭৯ / ১৩৩০** (বিভাগ / জেলা / উপজেলা / post office), প্রতিটা জেলা ঠিক একটা বিভাগে।

**⚠ Dataset ২০১৫-র আগের — Mymensingh বিভাগ নেই**, ওর জেলাগুলো Dhaka-র নিচে বসে আছে। উৎসে যা ছিল তাই রাখা হয়েছে।

### নকশার তিনটে সিদ্ধান্ত

**১. `geo_` prefix বাধ্যতামূলক ছিল।** `divisions` নামটা আগে থেকেই দখলে — ওটা branch-এর নিচের **org unit** (`Modules\Configuration\Models\Division`), ভৌগোলিক বিভাগ নয়। তাই `geo_divisions` / `geo_districts` / `geo_post_offices`, আর model-ও `GeoDivision`।

**২. Upazila-র আলাদা টেবিল নেই।** `employee_addresses` upazila-কে string হিসেবেই রাখে, কোনো id লাগে না। এক upazila-র অনেক post office (Demra → Demra/1360, Matuail/1362, Sarulia/1361), তাই এক row = এক post office, upazila পুনরাবৃত্ত label। বাড়তি টেবিল বানালে সেটাকে কেউ reference-ই করত না।

**৩. Tenant-scoped নয়।** ডাকঘরের ভূগোল সব company-র জন্য এক, তাই `company_id` নেই আর endpoint-এ permission gate নেই — শুধু auth।

### যা খেয়াল রাখা হয়েছে

- **পুরনো data মুছে যায় না।** আগে হাতে লেখা `upazila_or_city` / `postal_code` lookup-এ না থাকলে select-এ blank দেখিয়ে পরের save-এ মুছে যেত। তাই stored মান list-এ না থাকলে সেটাকে extra option হিসেবে যোগ করা হয় (`withStoredValue`)।
- **District তার বিভাগের বাইরের হতে পারে না** — `ValidatesGeoLocation` trait-এ `Rule::exists(...)->where('geo_division_id', ...)`, store ও update দুটোতেই। UI কখনো ভুল জোড়া বানাবে না, কিন্তু হাতে বানানো request পারত।
- **Seeder idempotent** — সব unique key-তে upsert, ৫টা statement-এ ১৩৩০ row। দুবার চালালে count বদলায় না (test আছে)।

### যাচাই

`GeoLocationApiTest` ৮টা, `EmployeeAddressGeoValidationTest` ৫টা — সব সবুজ। Feature suite: **186 passed, 8 failed**; ঐ ৮টাই আগের থেকে লাল (geo seeder বন্ধ করে চালিয়ে যাচাই করা — একই ৮টা লাল থাকে), সবগুলো Attendance/Correction-request-এর। `Modules\Payroll\Approval\*Executor`-এ `reject()` না থাকায় suite fatal দেয় — সেটাও আগের থেকেই, এই কাজের সাথে সম্পর্কহীন।

---

## 🐛 নিজের contact info update-এ "already in use" (16 Aug 2026)

**লক্ষণ:** Employee profile → Contact info tab-এ নিজের তথ্য save করলেই 422 — `This official email is already in use. (and 4 more errors)`।

### কারণ

`UpdateEmployeeContactRequest`-এর পাঁচটা `Rule::unique(...)` (official_email, personal_email, mobile_no, alternative_mobile_no, emergency_contact_phone) — **একটাতেও `->ignore()` ছিল না**। Update-এ unique rule গোটা টেবিলে খোঁজে, edit হতে থাকা row-টাসহ। তাই অপরিবর্তিত মান পাঠালেই সেটা **নিজের সাথেই** সংঘর্ষ করত, আর পাঁচটা rule একসাথে জ্বলে উঠত ("and 4 more errors" — ওটাই সূত্র ছিল)।

### যে জায়গাটা বিভ্রান্তিকর

**Service layer কখনোই ভুল ছিল না।** `EmployeeContactService::update()` ঠিকঠাক `$contact->id` কে exclude করে দেয়:

```php
$this->employeeContactRepository->existsOfficialEmail($companyId, $officialEmail, $contact->id)
```

Repository-ও `->when($excludeId, fn ($q) => $q->where('id', '!=', $excludeId))` করে। অর্থাৎ ভুলটা ধরার জন্য service-এ যত খোঁজাখুঁজি করা হোক, পাওয়া যেত না — request কখনো service পর্যন্ত পৌঁছাতই না।

### Fix

পাঁচটা rule-এ `->ignore($this->route('id'))`। Route হলো `PUT /contacts/{id}`।

### Test

`EmployeeContactValidationTest`-এ ২টা যোগ:

- `resaving_a_contact_unchanged_is_allowed` — regression test, fix-এর আগে লাল।
- `a_contact_detail_already_held_by_another_employee_is_still_rejected` — **guard test**। এটা না থাকলে unique rule গুলো তুলে দিলেই প্রথম test সবুজ হয়ে যেত, অথচ duplicate ধরা বন্ধ হয়ে যেত।

### বাকি Update request গুলো

`Rule::unique` ব্যবহার করা সব request দেখা হয়েছে — বাকি সব Update-এ (`UpdateAttendanceTypeRequest`, `UpdateDocumentTypeRequest`, `UpdateCompanyRequest`, `UpdateDepartmentRequest`, `UpdateDesignationRequest`, `UpdateSalaryStructureRequest`, `GradeUpdateRequest`) `ignore()` আছে। বাকিগুলো Store request, ওখানে লাগে না। **এই বাগ শুধু contact-এই ছিল।**

### ⚠ একই rule গুলোতে আরও দুটো সমস্যা — ঠিক করা হয়নি

**১. Company-scoped নয়।** DB constraint `unique(['company_id','official_email'])`, service-ও company ধরে খোঁজে, কিন্তু FormRequest গোটা টেবিলে। ফলে অন্য tenant-এর employee-র email এখানে block করে — এবং error message দিয়ে অন্য tenant-এ ঐ মান আছে তা ফাঁস হয়। §15 অনুযায়ী এটা tenant-isolation-এর ফাঁক।

**২. Emergency/alternative নম্বর globally unique।** দুই ভাইবোন একই কোম্পানিতে থাকলে একই অভিভাবককে emergency contact দিতে পারবে না। DB-তে এই কলামগুলোর কোনো unique constraint নেই — নিয়মটা শুধু FormRequest-এ, আর সেটা বাস্তবসম্মত নয়।

দুটোই আচরণ বদলায়, তাই আলাদা সিদ্ধান্ত হিসেবে রেখে দেওয়া হলো।

---

## Salary tab-এ mandatory field-এ লাল `*` (16 Aug 2026)

Employee → Salary tab (`EmployeeSalaryForm`, drawer) — কোন field আবশ্যক তা আগে থেকে বোঝার উপায় ছিল না, save চেপে error দেখে জানতে হতো।

### কিছু বানাতে হয়নি

Shared `Field` component আগে থেকেই `required` পেলে লাল `*` দেখায়:

```tsx
{required ? <span className="ml-1 text-rose-600" aria-hidden="true">*</span> : null}
```

`ContactInfoTab` আর `AddressTab` এভাবেই করে। Salary form-এ শুধু prop-টা কেউ pass করেনি — তাই নতুন CSS/component নয়, শুধু prop যোগ।

### কোনগুলো আবশ্যক — অনুমান নয়, উৎস থেকে

`StoreEmployeeSalaryRequest`-এর rules আর form-এর নিজের `validate()` — দুটোই একই তালিকা দেয়:

| Field | `*` |
|---|---|
| salary_structure_id, salary_type, gross_salary, currency_code, payment_frequency, effective_date | ✅ সবসময় |
| basic_salary | ⚠ `required={requiresBasic}` — structure basic দাবি করলে তবেই |
| status, tax_applicable, overtime_eligible, remarks | ❌ optional |

Basic salary-র label-এ আগে থেকেই `(optional)` suffix ছিল যা কেবল `!requiresBasic`-এ দেখায়, আর `*` কেবল `requiresBasic`-এ — দুটো পরস্পরবিরোধী অবস্থায় কখনো একসাথে দেখায় না।

### একটা পার্শ্বপ্রতিক্রিয়া, ইচ্ছাকৃত

`Input`/`Select` `required` কে DOM control-এও forward করে, তাই native HTML5 validation-ও চালু হয়। `ContactInfoTab`/`AddressTab`-এ ঠিক এই আচরণই আগে থেকে চলছে, তাই আলাদা করে কিছু করা হয়নি — view mode-এ field গুলো `disabled`, আর disabled control native validation-এর বাইরে।

### যাচাই

`tsc --noEmit` পরিষ্কার। Drawer-এর নিচের `PaymentSplitPanel` payroll module-এর, ওটা ছোঁয়া হয়নি।

---

## 🐛 Dropdown একবার select করলে আর deselect করা যেত না (16 Aug 2026)

**লক্ষণ:** Employee module-এ যেকোনো dropdown-এ একবার কিছু বাছলে আর খালি করা যেত না।

### কারণ — employee module-এ নয়, শেয়ার্ড `Select`-এ

`shared/components/ui/form/Select.tsx`-এ placeholder option হার্ডকোড `disabled` ছিল, আর প্রকাশ্য comment-ও সেটাকে ইচ্ছাকৃত বলে দাবি করত:

```tsx
/** Rendered as a disabled first option, so "nothing chosen" is visible but unselectable. */
<option value="" disabled>{placeholder}</option>
```

Required field-এর জন্য যুক্তিটা ঠিক ছিল। কিন্তু নিয়মটা **সব** field-এ চাপানো হয়েছিল, ফলে optional field আর filter-ও এক-মুখী দরজা হয়ে গিয়েছিল।

### Fix

```tsx
<option value="" disabled={required}>{placeholder}</option>
```

Required হলে আগের আচরণ অবিকল, optional হলে খোলা।

### সবচেয়ে খারাপ কেসটা filter

Timeline tab-এর "Event Type" filter-এ `placeholder="All Types"` — একবার কোনো type বাছলে **আর সব event-এ ফেরার উপায় ছিল না**, tab ছেড়ে ফিরে আসা ছাড়া। Employee-তে আরও ঠিক হলো: Address-এর Country/Division/District/Upazila/Postal Code, Personal Info-র Blood Group ও Marital Status, Employment-এর Type/Work Mode/Probation Unit, Education-এর Board।

(IdCard tab-এর status filter এতে পড়ে না — ওটার option list-এ নিজেরই "All Status" entry আছে, placeholder ব্যবহার করে না।)

### ⚠ পরিধি ও ঝুঁকি

`<Select>` ৭৬টা ফাইলে ব্যবহৃত (attendance ২০, configuration ১৯, employee ১৪, platform ১১, payroll ৯) — অর্থাৎ সব module-এ প্রযোজ্য।

**নতুন ঝুঁকি নেই**, কারণ `''` অবস্থাটা আগে থেকেই পৌঁছনো সম্ভব: form খোলার সময় প্রতিটা select-ই খালি থাকে এবং সেভাবেই submit করা যেত। এই পরিবর্তন কোনো নতুন মান payload-এ ঢোকায় না, শুধু ঐ অবস্থায় **ফেরার** পথ খুলে দেয়।

Placeholder ছাড়া যেসব select (যেমন Status = Active/Inactive) — ওগুলোতে খালি option-ই নেই, তাই অপরিবর্তিত। ওটা বৈধ নকশা: ঐ field-গুলোর "unset" অবস্থা নেই।

### যাচাই

`tsc --noEmit` পরিষ্কার। Frontend-এ কোনো test runner নেই (`package.json`-এ test script নেই), তাই automated coverage যোগ করা যায়নি।

---

## `5.3-FE` Leave Approvals UI merged — PR #188 (17 Aug 2026)

`att-5-3-fe-leave-approval` (Munna) main-এ। **Wave 4-এ leave track শেষ** — এখন ওখানে কেবল `5.5-FE` বাকি।

### Card-এর কঠিন rule গুলো সত্যিই মানা হয়েছে

এই card-টার DoD অন্যগুলোর চেয়ে বেশি নির্দিষ্ট ছিল — "deliberately absent" অংশে লেখা ছিল _"an Approve button while the balance panel is still loading. Approving against a stale figure is the failure this screen exists to prevent."_ যাচাই করে দেখা গেল প্রতিটাই আছে:

| Card rule | কোথায় |
|---|---|
| Balance detail খুললে **নতুন করে** fetch, list থেকে বয়ে আনা নয় | `fresh-leave-balance` query + `refetchBalance()` |
| Stale figure-এ approve করা যাবে না | `disabled={isBalanceLoading \|\| isInsufficient \|\| approveMutation.isPending}`, সাথে দৃশ্যমান কারণ-টেক্সট |
| 422 **inline**, toast নয় | `setInlineError()` → পাতার উপরে `<Alert>`; approve-এর `onError` toast ডাকেই না |
| Reject reason খালি হলে আটকাবে, default text থাকবে না | `reason.trim().length > 0`, নইলে button disabled |
| Approved request-এ per-day status | `voided-by-punch` badge + void তারিখ + কারণ |
| Queue filter: employee · policy · status · date range | চারটেই আছে (৫টা control) |
| `<ApprovalStatusBadge />` পুনঃব্যবহার · nav entry | দুটোই আছে |

এটা তুলনায় পরিচ্ছন্ন ডেলিভারি — `9.1-BE`-র মতো তিনটে DoD ফাঁকা রেখে আসেনি।

### 🔴 কিন্তু build ভেঙে গেছে

`package.json` → `"build": "sh scripts/check-style-boundary.sh && tsc -b && vite build"`।

**`tsc -b` এখন ১৬টা error দেয় — প্রতিটাই এই PR-এর নিজের তিনটে নতুন ফাইলে।** অর্থাৎ `npm run build` main-এ চলবে না।

| ধরন | সংখ্যা | নমুনা |
|---|---|---|
| Unused import/variable (TS6133) | ৯ | `Search`, `Button`, `navigate`, `AlertCircle`, `Clock`, `FileText`, `LEAVE_STATUS_LABELS` … |
| `ApiMeta` নামে কোনো export নেই | ১ | `leaveApprovalApi.ts:2` — `shared/types/api`-তে ঐ type-টা নেই |
| `Alert`-এ ভুল prop | ১ | `LeaveApprovalDetailPage.tsx:179` |
| `LeaveRequest`-এ `days` নেই | ৩+১ | `:320`, `:322` + implicit `any` |
| `DataTable` generic মেলেনি | ১ | `Column<LeaveRequest>[]` → `Column<unknown>[]` |

### ⚠ একটা card rule চুপচাপ কাজ করছে না

উপরের `Alert` error-টা নিছক type-ঝামেলা নয়। Detail page লিখেছে:

```tsx
<Alert variant="danger" title="Balance Insufficient…" action={<Button…>Refresh Balance</Button>}>
```

কিন্তু শেয়ার্ড `AlertProps`-এ আছে কেবল `tone` · `children` · `actions` · `className`। তিনটে prop-ই অচেনা:

- `variant="danger"` → উপেক্ষিত, banner **default `info` tone**-এ render হয় (লাল নয়)
- `title=` → উপেক্ষিত, শিরোনাম দেখায় না
- `action=` (একবচন) → উপেক্ষিত, **"Refresh Balance" button একেবারেই render হয় না**

Card-এর rule ছিল: _"A 422 … renders inline on the detail view with the current figure and a refresh action."_ Inline টেক্সট আসে ঠিকই (ওটা `children`), কিন্তু **refresh action-টা আসে না**। এক শব্দের ভুল — `actions`/`tone` লিখলেই মিটে যেত।

### ~~`days`-টা কিন্তু runtime-এ ঠিকই আছে~~ — ❌ এই দাবিটা ভুল ছিল

উপরে লেখা ছিল "day breakdown বাস্তবে কাজ করে, শুধু frontend type পিছিয়ে"। **যাচাই করে দেখা গেল উল্টো।** Backend দিকটা ঠিকই — `LeaveRequestResource:35` `whenLoaded('days')` করে `leave_date`/`day_value`/`status`/`voided_reason`/`voided_at` পাঠায়, আর controller-এর `show()` `days` eager-load করে।

কিন্তু frontend-এ detail page `leaveApprovalApi.getDetail()` ডাকে, যেটা `leaveRequestApi.get()`-এ delegate করে, আর ওটা `normalizeRequest()`-এর ভেতর দিয়ে যায় — **একটা explicit object literal যেখানে `days` field-ই নেই**। অর্থাৎ payload-এ `days` এলেও normalizer সেটা ফেলে দিত, `request.days` সবসময় `undefined` থাকত, আর `{request.days && request.days.length > 0}` branch **কখনোই render হতো না**। voided-by-punch panel চুপচাপ fallback branch-এ পড়ে যেত।

তাই type যোগ করাই যথেষ্ট ছিল না — normalizer-কেও field-টা বইতে হয়েছে, নইলে type-টা মিথ্যে বলত।

### ✅ সমাধান (17 Aug 2026)

| কাজ | কোথায় |
|---|---|
| ৯টা unused import/variable মোছা | `LeaveApprovalDetailPage.tsx`, `LeaveApprovalQueuePage.tsx` |
| `ApiMeta` type রপ্তানি — `ApiResponse`-এর inline meta shape বের করে আনা | `shared/types/api.ts` |
| `Alert`-এ `variant`/`title`/`action` → `tone`/`actions` + শিরোনাম children-এ — **আচরণ fix, Refresh button এখন render হয়** | `LeaveApprovalDetailPage.tsx` |
| `LeaveRequestDay` type + `LeaveRequest.days?` | `types/leaveRequest.ts` |
| `normalizeDay()` + `normalizeRequest`-এ `days` carry — **এটাই আসল runtime fix** | `api/leaveRequestApi.ts` |
| `DataTable` contract মেলানো — explicit `<LeaveRequest>` generic, `keyField="id"`, `emptyMessage`; error আলাদা `<Alert tone="danger">`-এ (sibling `LeaveRequestListPage`-এর প্যাটার্ন) | `LeaveApprovalQueuePage.tsx` |

**ফল:** `npm run build` সবুজ — style check clean (321 file), `tsc -b` ০ error, `vite build` ✓ 2504 modules। `oxlint` ৬টা warning দেয়, সবগুলোই পুরনো ও অন্য ফাইলে।

`5.5-FE` এখন পরিষ্কার build-এর উপরে বসতে পারে।

---

## 🐛 Platform drawer-এ দুটো infinite render loop সারানো (17 Aug 2026)

Browser console-এ `UserRolesDrawer.tsx:39` থেকে **"Maximum update depth exceeded"** আসছিল। Attendance/Payroll card নয়, কিন্তু একই defect দুই জায়গায় ছিল বলে একসাথে লিখে রাখা হলো।

### কারণ — react-query-র destructuring default

```tsx
const { data: userRoles = [], isLoading } = useQuery({ ... });   // ← এখানেই ফাঁদ

useEffect(() => {
  if (!show) return;
  setSelected(userRoles.map((r) => r.uuid));
  setError('');
}, [show, userRoles, uuid]);
```

`data` যখন `undefined` — query loading, বা `uuid` null বলে `enabled: false` — তখন `= []` default **প্রতিটা render-এ নতুন array** বানায়। লুপ:

1. Render → `userRoles` টাটকা `[]` (নতুন reference)
2. Effect-এর dep বদলেছে মনে হয় → effect চলে
3. `setSelected([])` — `Object.is` তুলনায় নতুন array ≠ পুরোনো, তাই React bail out করে না → re-render
4. → ধাপ ১। React ~50টা nested update-এর পর throw করে।

`if (!show) return;` guard drawer বন্ধ থাকলে বাঁচায়, কিন্তু **drawer খোলা অবস্থায় fetch চলাকালীন ঠিক এই লুপেই পড়ে** — অর্থাৎ প্রতিবার drawer খুললেই।

মূল কথা: **react-query-র নিজের `data` reference-stable** (structural sharing করে, resolve না হওয়া পর্যন্ত স্থিরভাবে `undefined`) — `= []` default সেই stability-টাই নষ্ট করে। `setError('')` দায়ী নয়; `''` → `''` একই string, ওখানে React bail out করে।

### ফিক্স

Default সরিয়ে fallback-টা effect-এর ভেতরে:

| ফাইল | পরিবর্তন |
|---|---|
| `platform/components/UserRolesDrawer.tsx` | `data: userRoles` (default বাদ) · `setSelected(userRoles?.map((r) => r.uuid) ?? [])` |
| `platform/components/RolePermissionsDrawer.tsx` | `data: rolePermissions` (default বাদ) · `setSelectedKeys(rolePermissions ?? [])` |

দুটোতেই comment বসানো হয়েছে কেন default-টা ইচ্ছাকৃতভাবে নেই — নইলে কেউ "পরিষ্কার" করতে গিয়ে bug ফিরিয়ে আনবে।

### গোটা codebase sweep

শুধু এই দুটোই কিনা যাচাই করতে script দিয়ে খোঁজা হয়েছে — literal default (`= []`/`= {}`) সহ destructure করা query result যা কোনো **setState-করা `useEffect`**-এর dependency array-তে আছে (multi-line dep array সহ)।

- Codebase-এ এমন default **৩৭টা** আছে (২৭ ফাইলে), কিন্তু effect-dependency হিসেবে ছিল **কেবল এই দুটোই**।
- তিনটে `useMemo` ঐ প্যাটার্নে আছে (`EmployeeDeductionListPage`, `CorrectionRequestForm`, `CompanyConfigurationTab`) — `useMemo` setState করে না, তাই লুপ হয় না, কেবল প্রতি render-এ অকারণে recompute হয়। **ছোঁয়া হয়নি।**
- Detector-টা `git show HEAD:` দিয়ে পুরোনো (ভাঙা) দুটো ফাইলের উপরে চালিয়ে যাচাই করা হয়েছে — দুটোই ধরা পড়ে, আর সারানোর পরের tree-তে ০।

### যাচাই

`npm run build` সবুজ (style check clean 321 file · `tsc -b` ০ error · `vite build` ✓) · `oxlint src/modules/platform` → **০ warning, ০ error** (`exhaustive-deps` সহ)।

---

## 🐛 Super admin অন্য company-র user-কে permission দিতে পারত না (17 Aug 2026)

Platform → Users → Roles/Overrides drawer-এ super admin অন্য company-র user save করতে গেলে:

```
User '7fb15f27-25d0-4922-89f1-dd398cd7f05a' not found in this company.
```

Attendance/Payroll card নয়, কিন্তু tenant scoping-এর মূল জায়গায় বলে এখানেই লিখে রাখা হলো — সব module-ই এই resolver-এর উপরে বসে আছে।

### কারণ — company context কখনো বদলাতই না

Chain-টা `CompanyContextResolver::resolveForUser()`-এ ভাঙত:

1. Frontend `X-Company-Id` header-এ selected company-র uuid পাঠায়।
2. Resolver ঐ uuid খুঁজত `whereHas('users', user->id)` দিয়ে — অর্থাৎ **caller ঐ company-র member কিনা**। Developer/super admin কোনো company-র member নয় (`user_type = developer` হলে `company_id = null` দিয়ে তৈরি হয়), তাই header **নিঃশব্দে ফেলে দেওয়া হতো**।
3. Fallback → caller-এর নিজের default company (seeded admin-এর ক্ষেত্রে `erpflow-demo`), সেটাও না থাকলে `Company::where('status','active')->orderBy('id')->first()`।

ফল: super admin-এর tenant context **সবসময় একটাই company-তে আটকে থাকত**, navbar থেকে switch করলেও। তারপর `RoleEngine::resolveUser()` target user-কে ঐ ভুল company-র ভেতরে খুঁজে না পেয়ে উপরের message দিত।

দ্বিতীয় অসঙ্গতি: `GET /users` super admin-কে `paginateAll()` দিয়ে **সব company-র user** দেখাত, কিন্তু assign endpoint tenant context-এ locked — অর্থাৎ list যা দেখায় তার বেশিরভাগই assign করা যেত না।

### ফিক্স

| ফাইল | পরিবর্তন |
|---|---|
| `Core/Tenancy/CompanyContextResolver.php` | `hasPlatformScope()` (user_type ∈ `bypass_user_types`) · platform user-এর জন্য header যেকোনো **active** company-তে resolve হয়, membership লাগে না · `accessibleCompanies()` · **requested company resolve না হলে এখন `PermissionDeniedException` (403)** |
| `Core/Auth/Services/AuthService.php` | `formatCompanies()` → `accessibleCompanies()` (super admin navbar-এ সব company পায়) · `switchCompany()` → resolver-এর মধ্য দিয়ে (আগের `findForUserByUuid` membership চাইত, তাই super admin switch করলে 404) |
| `Core/Users/Http/Controllers/Api/UserController.php` | `index()` সবসময় current company-তে scoped — `paginateAll()` branch বাদ |
| `platform/components/UserRolesDrawer.tsx` | role list এখন current company-তে filter করা (`company_id`), নইলে super admin অন্য company-র role বেছে ফেলত → "Role not found" |

**Silent fallback-টা ইচ্ছাকৃতভাবে হার্ড fail করানো হয়েছে।** আগে ভুল/stale header দিলে চুপচাপ caller-এর default company-তে নেমে যেত — অর্থাৎ user ভাবত company B-তে কাজ করছে, কিন্তু write হতো company A-তে। এটাই ধরা পড়েছে `EmployeeDeductionTest`-এ: ও header-এ uuid-র বদলে company **id** পাঠাচ্ছিল, আগে accidentally pass করত। Test-টা uuid-এ ঠিক করা হয়েছে।

Super admin/developer বাদে **প্রতিটা role আগের মতোই নিজের company-তেই বাঁধা** — non-platform user-দের membership check অপরিবর্তিত।

### যাচাই

নতুন `tests/Feature/CompanyContextScopingTest.php` — ৬টা test, সব সবুজ (cross-company role assign · override assign · company list + switch · অন্য company-তে 403 · silent fallback হয় না · user list scoped)। `EmployeeDeductionTest` ৭/৭ সবুজ। Feature suite-এ বাকি failure গুলো baseline-এর সাথে হুবহু মেলে (attendance grace/timezone ৮টা + `SalaryAdvanceExecutor::reject` missing fatal — কোনোটাই এই change-এর নয়)। Pint clean · frontend `tsc --noEmit` clean।

---

## Navbar-এ company tab strip — dropdown সরিয়ে (17 Aug 2026)

SaaS multi-company setup: super admin কোনো company-তে ঢুকলে ঐ company-র সবকিছু দেখে। Switching-টা তাই লুকোনো dropdown-এ থাকা উচিত নয় — navbar-এ **প্রতি company-র জন্য একটা করে tab**, একসাথে ঠিক একটাই active।

### কী হলো

| ফাইল | পরিবর্তন |
|---|---|
| `core/components/layout/CompanySwitcher.tsx` (নতুন) | Company tab strip · `role="radiogroup"` + `role="radio"` · একটাই selected · switch চলাকালীন সবগুলো disabled, target tab-এ spinner · **শুধু super admin/developer দেখে** |
| `core/components/layout/Header.tsx` | `<select>` বাদ, `<CompanySwitcher />` বসানো · বাঁ পাশের company title block-এ `max-w-[14rem]` যাতে লম্বা নাম strip-টা খেয়ে না ফেলে |
| `auth/context/AuthContext.tsx` | `can_switch_company` state-এ ধরা হয় · `switchCompany()` — ব্যর্থ হলে **আগের company uuid ফিরিয়ে দেয়**, আর সফল হলে `queryClient.clear()` |
| `Core/Auth/Services/AuthService.php` | auth payload-এ নতুন `can_switch_company` |

**Strip কে দেখবে:** শুধু platform user (super admin / developer)। বাকি সবাই একটাই company-র ভেতরে কাজ করে, তাদের বাছার কিছু নেই।

Flag-টা **backend থেকে** আসে (`CompanyContextResolver::hasPlatformScope()` → auth payload-এর `can_switch_company`), UI-তে `user_type === 'developer'` লেখা হয়নি — কোন user type company boundary পার হতে পারে সেটা `config('platform.permissions.bypass_user_types')`-এ থাকে, ঐ নিয়মের একটার বেশি copy রাখার মানে নেই। Server যাই হোক enforce করে; flag-টা শুধু বলে দেয় control-টা দেখানোর মানে আছে কিনা।

**`role="tablist"` নয়, `radiogroup` কেন:** tab গুলো panel বদলায় না, একটা **মান** বাছে — active company। ঐ মানটা প্রতিটা request-এর `X-Company-Id` header-এ যায়, অর্থাৎ পুরো app কোন tenant দেখছে সেটাই ঠিক করে। Screen reader-এ radio group-ই সঠিক জিনিস বলে।

**দুটো bug সাথে সারানো হয়েছে:**

1. `switchCompany()` API call-এর **আগেই** localStorage-এ নতুন uuid বসাত (header-টা ঐ call-এই লাগে বলে বসাতেই হয়), কিন্তু call ব্যর্থ হলে আর ফেরাত না। এখন resolver reject করলে **403** আসে, তাই এটা এখন সত্যিকারের ফাঁদ — 403-এর পর গোটা app একটা নিষিদ্ধ company চাইতেই থাকত। এখন catch-এ পুরোনো uuid restore হয়।
2. Switch-এর পরে **react-query cache পরিষ্কার হতো না** — অর্থাৎ আগের company-র employee/role/attendance row গুলো নতুন company-র নামের নিচে বসে থাকত যতক্ষণ না refetch হয়। `queryClient.clear()` বসানো হয়েছে; tenant বদলালে আগের tenant-এর কিছুই memory-তে থাকা উচিত নয়।

### যাচাই

`CompanyContextScopingTest` ৭/৭ সবুজ (নতুন test: platform user-এর `can_switch_company` true, ordinary user-এর false এবং তার company list-এ একটাই company) · Pint clean · `npm run build` সবুজ (style check clean 322 file · `tsc -b` ০ error · `vite build` ✓) · `oxlint src/modules/{core,auth,platform}` → **০ error**, ৫টা warning সবই পুরোনো ও অন্য ফাইলের।

---

## 🐛 Company switch করলে page-এর data বদলাত না, reload দিতে হতো (17 Aug 2026)

Tab strip বসানোর পরেও switch করে যে page-এ ছিলেন সেখানেই থাকলে পুরোনো company-র row-ই দেখা যেত — F5 না দিলে বদলাত না।

### কারণ দুটো, আলাদা আলাদা

**১. `queryClient.clear()` refetch করায় না।** `clear()` cache থেকে query গুলো **মুছে ফেলে**, কিন্তু mounted observer গুলো ঐ মৃত query ধরেই বসে থাকে — নতুন করে fetch হয় না। অর্থাৎ cache খালি হতো, screen-এর data হতো না। ঠিক জিনিসটা `resetQueries()`: data-ও মোছে, **আর যেগুলোর active observer আছে সেগুলো refetch-ও করে**।

**২. সব page react-query দিয়ে চলে না।** `employmentTypes` আর `employeeAssets` redux slice-এ থাকে, thunk গুলো page **mount-এ** একবার dispatch হয়। Cache যাই করি, ঐ page গুলো নিজে থেকে আর fetch করত না।

### ফিক্স

| ফাইল | পরিবর্তন |
|---|---|
| `auth/context/AuthContext.tsx` | `queryClient.clear()` → `await queryClient.resetQueries()` · সাথে `dispatch(companyChanged())` |
| `app/store.ts` | `companyChanged` action · root reducer company বদলালে **সব slice-কে initial state-এ ফিরিয়ে দেয়** |
| `core/components/layout/AppLayout.tsx` | `<Outlet key={currentCompany?.uuid} />` — switch হলে routed page remount হয় |

**Slice ধরে ধরে reset না করে root-এ কেন:** পরে নতুন slice যোগ হলে কাউকে এই নিয়মটা মনে রাখতে হবে না, এমনিতেই ঢেকে যাবে।

**Remount আর reset — দুটোই কেন লাগে:** reset না করলে remount-এর মুহূর্তে page পুরোনো company-র cached row এক ঝলক দেখাত; remount না করলে redux/effect-চালিত page গুলো কখনো refetch-ই করত না। ক্রমটাও গুরুত্বপূর্ণ — reset **আগে**, তারপর `me()` আর state update, নইলে key বদলানোর সময় cache-এ আগের tenant-এর data বসে থাকত।

**দুবার fetch হয় না:** reset-এর refetch শেষ হওয়ার পরে remount হয়, আর তখন data-টা `staleTime`-এর (৩০s) ভেতরে টাটকা, তাই remount করা observer আবার fetch করে না।

Route guard গুলো নিরাপদ — `RequirePermissionRoute` আর `PermissionGuard` দুটোই `isLoading` দেখে অপেক্ষা করে, তাই refetch চলাকালীন কাউকে `/`-এ ছুঁড়ে ফেলে না। নতুন company-তে সত্যিই permission না থাকলে তখন redirect হয়, যেটা সঠিক।

### যাচাই

`npm run build` সবুজ · `oxlint src/{app,modules/core,modules/auth}` → **০ error**, ৫টা warning পুরোনো ও অন্য ফাইলের।

---

## 🐛 Stale company uuid session-টাই মেরে ফেলত (17 Aug 2026)

PR #191-এর কোড রিভিউ করতে গিয়ে ধরা পড়েছে — নিজেরই আনা regression, merge হয়ে গিয়েছিল।

**যা হতো:** company uuid `localStorage`-এ থাকে আর প্রতিটা request-এর header-এ যায়। ঐ uuid যদি পরে অচল হয়ে যায় — membership তুলে নেওয়া হলো, বা **company deactivate হলো** — server এখন 403 দেয় (আগে চুপচাপ default company-তে fall back করত)। কিন্তু `AuthContext.hydrate()`-এর `catch` যেকোনো ব্যর্থতাকে মৃত session ধরে **token + company দুটোই মুছে দিত**। ফলে app খুললেই লগআউট, অথচ ভুলটা ছিল কেবল company বাছাইয়ে।

SaaS-এ এটা কাল্পনিক নয় — একটা company deactivate করলেই যারা শেষবার ঐ company-তে ছিল সবাই লগআউট।

**ফিক্স:** `loadSession()` — `me()` 403 দিলে আর company uuid stored থাকলে ওটা ফেলে **একবার retry** করে, server নিজের default বেছে নেয়। দ্বিতীয়বার ব্যর্থ হলে সেটা সত্যিই session-এর সমস্যা, caller-এর হাতে যায়।

Retry-টা নিজেই সঠিক signal: header ছাড়া পাঠিয়ে সফল হলে company-ই দোষী ছিল; আবার ব্যর্থ হলে যেভাবেই হোক লগআউট হতো, তাই company key মুছে ফেলায় কিছু হারায় না। কোনো error code বা API shape বদলানো লাগেনি।

| ফাইল | পরিবর্তন |
|---|---|
| `shared/api/client.ts` | `isForbidden(error)` helper — HTTP status চেনা transport-এর কাজ, context-এর নয় |
| `auth/context/AuthContext.tsx` | `loadSession()`; `hydrate()` এখন `me()`-র বদলে ওটা ডাকে |
| `tests/Feature/CompanyContextScopingTest.php` | নতুন test: deactivated company **refused**, আর ঐ header ছাড়া session **টিকে যায়** |

> `withHeader()` টেস্ট কেসে জমতে থাকে — একই test-এ "header ছাড়া" request পাঠাতে হলে আগে `flushHeaders()` ডাকতে হয়। প্রথম বার এটা মিস করে test মিথ্যা লাল দিয়েছিল।

**যাচাই:** `CompanyContextScopingTest` **8/8 সবুজ**।

---

## 🔒 Tenant scoping model-এ নামানো হলো — ৪৭টা model (17 Aug 2026)

**যা ধরা পড়ল:** এক company-র configuration অন্য company দেখতে পাচ্ছিল। খুঁজতে গিয়ে দেখা গেল এটা একটা বাগ নয়, একটা architectural gap।

Core tenancy layer ঠিকই ছিল — `SetCompanyContext` header থেকে company resolve করে, `CompanyContextResolver` অনধিকার company চাইলে reject করে, controller গুলোও `TenantContext` থেকেই companyId নেয়, client-এর পাঠানো `company_id` বিশ্বাস করে না।

সমস্যা তার পরে। Audit করে দেখা গেল **৫৪টা tenant-owned model, একটাতেও global scope নেই**। মানে প্রতিটা query-তে ডেভেলপারকে নিজে `where('company_id', ...)` লিখতে হতো। যে জায়গায় কেউ ভুলে গেছে, সেখানেই leak।

### Audit-এ যা পাওয়া গেল

| ধরন | পরিমাণ |
|---|---|
| tenant-owned table (live DB থেকে) | ৫৫ |
| tenant-owned model | ৫৪ — **০টায় global scope** |
| unscoped `find*ById()` (IDOR) | ৩২ method, ১৭ ফাইল |
| `$companyId` নেয় কিন্তু filter করে না | EmploymentType repository (list পুরো leak) |

Leak শুধু Configuration-এ ছিল না — Attendance (`ShiftService`), Payroll (`TaxSlabService`), Employee (`EmployeeTaxProfileService`), Core (`OnboardingPolicyService`) সবখানে একই প্যাটার্ন: `findById($id)` company filter ছাড়া, তারপর service-ও ownership মেলায় না।

সবচেয়ে স্পষ্ট প্রমাণ `DesignationService`-এ — `deleteDesignation()`-এ company check আছে, ঠিক উপরের `updateDesignation()`-এ নেই। একই ফাইল, একই দিন। হাতে হাতে scoping টেকে না।

> Attendance আর Payroll একটা `scopeForCompany()` convention চালু করেছিল (৭টা model)। ভালো, কিন্তু opt-in — ভুলে গেলে আগের মতোই leak।

### ফিক্স

| ফাইল | পরিবর্তন |
|---|---|
| `app/Core/Tenancy/CompanyScope.php` | **নতুন** — global scope, current company-তে query আটকায় |
| `app/Core/Tenancy/Concerns/BelongsToCompany.php` | **নতুন** — trait: scope বসায় + create-এ `company_id` auto-fill |
| `app/Core/Tenancy/TenantContext.php` | `enforce()` / `isEnforced()` |
| `app/Core/Http/Middleware/SetCompanyContext.php` | user থাকলে enforcement চালু |
| ৪৭টা model | `use BelongsToCompany;` |
| `Repositories/EmploymentTypeRepository.php` | `where('company_id')` — নাম বলত scope করে, করত না |
| `Http/Requests/StoreEmploymentTypeRequest.php` | `$this->user()->company_id` → `TenantContext` |

**Default এখন secure** — ভুলে যাওয়া `where` আর leak করাতে পারে না, কারণ constraint টা query-তে নেই, model-এ।

### তিনটে সিদ্ধান্ত

**১. Permission layer বাদ।** `Role`, `UserPermissionOverride`, `ActivityLog`, `Approval*`, `Setting` trait পায়নি। Permission-এর নিয়ম আলাদা, আর approval job গুলো ইচ্ছাকৃতভাবে সব company জুড়ে চলে।

**২. Enforcement request-এ বাঁধা, console-এ নয়।** `SetCompanyContext` user পেলে `enforce()` ডাকে। ফলে scheduled command (`attendance:schedule-close-day`), queue job, seeder — যেগুলো সত্যিই সব company নিয়ে কাজ করে — অক্ষত থাকে। `runningInConsole()` ব্যবহার করিনি ইচ্ছা করেই: PHPUnit-ও console, তাহলে feature test-এ scope নিষ্ক্রিয় থাকত আর test গুলো মিথ্যা সবুজ দিত।

**৩. company resolve না হলে fail-closed।** User আছে অথচ company নেই — সেটা bug, সব tenant পড়ার অনুমতি নয়। তখন query কিছুই ফেরত দেয় না।

ইচ্ছাকৃত cross-company কাজের জন্য `withoutGlobalScope(CompanyScope::class)` — explicit, তাই review-এ চোখে পড়ে।

`find*ById()` গুলোর signature বদলাইনি — global scope ওগুলো এমনিতেই ঢেকে দেয়, আর ১৫+ interface ভাঙা এই PR-এর ঝুঁকি অকারণে বাড়াত।

### যাচাই

`CompanyDataScopingTest` — **১১/১১ সবুজ**। Test গুলো সত্যিই leak ধরে কিনা প্রমাণ করতে fix সরিয়ে চালানো হয়েছে: **৬টা fail করে**।

Feature suite baseline-এর সাথে **হুবহু এক** — নতুন কোনো failure নেই। আগে থেকেই লাল ছিল `AttendanceDailyCalculation`, `CorrectionRequestApi`, `AttendancePolicyApi`, `AttendanceRecalculationApi`, `AttendancePayroll*` এবং `PayrollRunExecutor`/`SalaryAdvanceExecutor`-এর fatal (`ApprovalExecutorInterface::reject` implement করা নেই) — এগুলো এই কাজের বাইরে, আলাদা করে ধরা দরকার।

---

## 🐛 Required dropdown বাছাই করলে আর ফেরত যাওয়া যেত না (17 Aug 2026)

**যা হতো:** reporting manager assign, education-এ degree, timeline-এ event type, document upload-এ document type, organization tab-এ department — এই dropdown গুলোতে একবার কিছু বাছাই করলে আর "Select …"-এ ফেরত যাওয়া যেত না। ভুল করে ভুল জিনিস বাছলে undo করার উপায় ছিল না।

**কারণ:** `Select.tsx`-এ placeholder option-টা `disabled={required}` ছিল। আগের রাউন্ডে (commit `54427ee`) optional field-এর জন্য এটা selectable করা হয়েছিল, কিন্তু **required field গুলো one-way door থেকে যায়** — অর্ধেক ফিক্স।

যুক্তিটা ছিল "required field-এ খালি করা বৈধ শেষ অবস্থা নয়"। কিন্তু ওটা ভুল জায়গায় প্রয়োগ করা — খালি অবস্থা আটকানোর কাজ validation-এর, option disable করার নয়। ব্যবহারকারী তো ফর্ম খোলার সময় এমনিতেই খালি অবস্থায় থাকে; সেখানে ফেরত যাওয়া নতুন কোনো invalid অবস্থা তৈরি করে না।

**ফিক্স:** placeholder সবসময় selectable। `required` control-এ থেকেই যায়, তাই খালি করলে field আবার invalid হয় আর submit আটকায় — ঠিক যেমন কিছু বাছার আগে আটকাত।

| ফাইল | পরিবর্তন |
|---|---|
| `shared/components/ui/form/Select.tsx` | `<option value="" disabled={required}>` → `<option value="">` |

**পরিধি:** ২৭টা required dropdown, ১৮টা ফাইল। বাকি required Select গুলোতে placeholder-ই নেই (Status/Orientation-এর মতো default-সহ closed enum) — ওগুলো অপরিবর্তিত, কারণ ওখানে "কিছু বাছা হয়নি" বলে কোনো অবস্থা নেই।

**খালি submit হয়ে যাবে না তা যাচাই করা হয়েছে:** `noValidate` ছাড়া ফর্মে browser নিজেই আটকায়; `noValidate` দেওয়া ফর্ম গুলোর প্রতিটাতে নিজস্ব validation আছে বলে scan করে দেখা হয়েছে। একমাত্র সন্দেহজনক `RecordPaymentDrawer`-এ placeholder-ই নেই, তাই সেটা এই পরিবর্তনের বাইরে।

### যাচাই

`oxlint src/shared/components/ui/form/Select.tsx` → **০ error, ০ warning**।

> `npm run build` **main-এ আগে থেকেই লাল** — ৭টা TS error, দুটো ফাইলে: `CorrectionApprovalDetailPage.tsx` (`../../../shared/types/platform` — relative depth একধাপ কম, `../../../../` হওয়ার কথা; সাথে unused import আর implicit any) আর `ActivityLogPropertiesViewer.tsx` (`unknown` → `ReactNode`)। এই পরিবর্তনের আগে-পরে সংখ্যাটা হুবহু এক। আলাদা করে ধরা দরকার।

> **পরবর্তী merge-এ ধরা:** `att-6-1-be-monthly-approval` থেকে আসা `MonthlyAttendanceApproval` model-টা `company_id` নিয়ে এসেছিল কিন্তু trait ছাড়া। যোগ করা হয়েছে। নতুন tenant-owned model বানালে `use BelongsToCompany;` লাগবে — `company_id` column থাকা মানেই scope পাওয়া নয়।

---

## 🐛 Organization assignment-এ static dropdown আর "Master data not available" (17 Aug 2026)

**যা হতো:** Employee → Organization tab-এ ৯টা dropdown, কিন্তু Department আর Designation ছাড়া অন্য কিছু বাছলেই save-এ 422 — *"Master data for this field is not available yet."*

**দুটো আলাদা কারণ একই message-এর নিচে ঢাকা পড়েছিল:**

**১. ৬টা dropdown static ডেটা দেখাত।** Division, Team, Section, Grade, Cost Center, Work Location — সবগুলো `organizationDemoData.ts`-এ হাতে লেখা array থেকে আসত (`{ id: 1, name: 'Corporate Division' }`)। ঐ id গুলো ডেটাবেসের কোনো row-র সাথেই মিলত না।

**২. Branch-এর ডেটা আসল ছিল, তবু আটকানো ছিল।** `listBranches()` থেকে সত্যিকারের branch আসত, id-ও সঠিক — কিন্তু backend-এর `UNAVAILABLE_MASTER_FIELDS`-এ `branch_id` থাকায় বৈধ branch বাছলেও reject হতো। আর `mapAssignmentPayload()` ঐ ৭টা field সবসময় `null` লিখত।

### যা পাওয়া গেল audit-এ

| Field | Table | API | অবস্থা |
|---|---|---|---|
| Branch, Division, Team, Grade | ✅ | ✅ full CRUD | **শুধু আটকানো ছিল** |
| Department, Designation | ✅ | ✅ | আগে থেকেই কাজ করত |
| Section, Cost Center, Work Location | ❌ | ❌ | সত্যিই নেই |

`employee_organization_assignments` table-এ ন'টা column-ই আগে থেকে আছে, আর form request-ও সব `nullable` হিসেবে গ্রহণ করত — শুধু service স্তরে আটকে ছিল।

### ফিক্স (Phase 1 — যেগুলোর ডেটা প্রস্তুত)

| ফাইল | পরিবর্তন |
|---|---|
| `Employee/Services/EmployeeOrganizationAssignmentService.php` | `UNAVAILABLE_MASTER_FIELDS` এখন শুধু ৩টা; নতুন `OPTIONAL_STRUCTURE_MASTERS` map; payload-এ branch/division/team/grade persist |
| `Employee/Repositories/EmployeeOrganizationAssignmentRepository.php` | `activeMasterExists()` — চারটে প্রায়-অভিন্ন method না লিখে একটাই lookup |
| `employee/profile-tabs/OrganizationTab.tsx` | Division/Team/Grade → `listDivisions()` / `listTeams(1,100)` / `listEmployeeGrades()`; history-র label-ও আসল master থেকে |

**কেন generic `activeMasterExists(class-string $master, ...)`:** চারটেরই যাচাই হুবহু এক — active, আর এই company-তে। চারটে আলাদা method লিখলে একই query চারবার, আর পরে নতুন master যোগ হলে পঞ্চমবার।

**Company boundary বজায়:** প্রতিটা optional master যাচাই হয় `where('company_id', $companyId)` দিয়ে, অর্থাৎ অন্য company-র branch/division/team/grade-এর id পাঠালে 422।

### যাচাই

`EmployeeOrganizationMastersTest` — **৮/৮ সবুজ**: চারটে master round-trip করে, optional থাকে, অন্য company-র master reject হয় (৪টা data set), inactive master reject হয়, আর যে ৩টার table নেই সেগুলো এখনো refused।

`tsc -b` — OrganizationTab পরিষ্কার; মোট error সংখ্যা baseline-এর সমান (৭, পুরোনো দুটো ফাইলে)।

### বাকি

Section, Cost Center, Work Location — এখনো static, কারণ এদের table/API কিছুই নেই। এগুলো নতুন master data হিসেবে বানাতে হবে (migration + model + repo/service/controller + admin CRUD tab)। shape-টা product সিদ্ধান্ত, তাই আলাদা ধাপে।

### Phase 2 — Section, Cost Center, Work Location (নতুন master)

এই তিনটার table, API, UI কিছুই ছিল না। সিদ্ধান্ত: **তিনটেই flat master** (`company_id + name + code + status`), সাথে **admin CRUD** এখনই।

**Backend — ৩০টা ফাইল, একটাই template থেকে generate করা:**

migration · model (`BelongsToCompany` সহ) · repository + contract · service + contract · store/update request · resource · controller — প্রতিটার জন্য। সাথে ১৮টা route (বিদ্যমান `permission:configuration.*` group-এর ভেতরেই, নতুন permission লাগেনি) আর provider binding।

**Frontend:**

| ফাইল | কাজ |
|---|---|
| `api/flatMasterApi.ts` | একটাই factory → `sectionApi` / `costCenterApi` / `workLocationApi` |
| `components/FlatMasterList.tsx` | এক CRUD screen, api + copy prop হিসেবে নেয় |
| `components/FlatMasterForm.tsx` | এক drawer form |
| `SystemConfigurationPage.tsx` | ৩টা নতুন tab |
| `OrganizationTab.tsx` | ৩টা dropdown আসল API-তে |
| `api/organizationDemoData.ts` | ৬টা static তালিকা **মুছে ফেলা** (৯২ লাইনে নেমেছে) |

**তিনটে আলাদা ফাইল না লিখে একটা করে কেন:** তিনটার shape হুবহু এক। আলাদা লিখলে একটায় bug ঠিক করলে বাকি দুটোয় থেকে যেত। Backend-এ template থেকে generate করা হয়েছে (module-এর per-entity convention রক্ষা করতে), frontend-এ runtime parameter দিয়ে (ওখানে convention component-based)।

**Grade-এর দুটো বাগ copy করা হয়নি:** `grades.grade_code`-এ **global** unique index আর `'unique:grades,grade_name'` rule — দুটোই company-wise নয়, অর্থাৎ এক company "G1" নিলে অন্য company আর নিতে পারে না। নতুন তিনটেয় `unique(['company_id', 'code'])`। *Grade-এর ওটা আলাদা করে ঠিক করা দরকার।*

**ফলাফল:** ন'টা dropdown-ই এখন backend থেকে আসে, company-scoped, আর save হয়। `UNAVAILABLE_MASTER_FIELDS` আর `rejectUnavailableMasters()` মুছে গেছে — যে message দিয়ে এই তদন্ত শুরু হয়েছিল, সেটার আর অস্তিত্ব নেই।

### যাচাই (Phase 2)

- `ConfigurationFlatMastersTest` — **১৫/১৫**: তিনটে master × (CRUD round-trip · অন্য company-তে একই code মুক্ত · একই company-তে duplicate code refused · cross-company row অদৃশ্য · `active` endpoint inactive বাদ দেয়)
- `EmployeeOrganizationMastersTest` — **৯/৯**
- Feature suite baseline-এর সাথে **হুবহু এক**
- `tsc -b` নতুন error নেই (৭ = পুরোনো baseline) · `oxlint` ০ error

---

## 🐛 Grade-এর uniqueness company-wise ছিল না (17 Aug 2026)

নতুন master তিনটে বানাতে গিয়ে template হিসেবে Grade পড়তে গিয়ে ধরা পড়েছিল, তখন note রেখে দেওয়া হয়েছিল। এখন শেষ করা হলো।

**যা হতো:** `grades` table-এ `grade_code`-এ **global** unique index, আর request rule-এ `unique:grades,grade_name` — company ছাড়াই। ফলে **প্রথম যে company "Grade 1 - Entry Level" বানাত, সেটা বাকি সব company-র জন্য চিরতরে দখল হয়ে যেত।** Multi-tenant ERP-তে এটা কেবল অসুবিধা নয় — একটা tenant অন্য tenant-এর নামকরণ আটকে দিতে পারত।

### একই জায়গায় আরও তিনটে বাগ

**১. Update করতে গেলে নিজের নামেই duplicate বলত।** `GradeUpdateRequest`-এ `$this->route('grade')` পড়া হতো, কিন্তু route হলো `/grades/{id}` — তাই মান সবসময় `null`, `->ignore(null)` কিছুই বাদ দিত না। ফলে নাম না বদলে শুধু status বদলাতে চাইলেও 422।

**২. Generate করা code সংঘর্ষ করত।** `generateGradeCode()` নাম থেকে দুই অক্ষর + সংখ্যা বানায় — "Senior Engineer" আর "Senior Executive" দুটোই `SE-001`। Repository-তে `gradeCodeExists()` লেখাই ছিল, কিন্তু **কখনো ডাকা হয়নি**। Global index থাকায় এটা ধরা পড়ত DB constraint violation হিসেবে (500), form error হিসেবে নয়।

**৩. `FIELD()` MySQL-only।** `listByCompany()`-তে `orderByRaw("FIELD(status, 'active', 'inactive')")` — sqlite-এ `no such function: FIELD`. এই কারণেই grades list endpoint-এর কোনো test কখনো লেখা যায়নি।

### ফিক্স

| ফাইল | পরিবর্তন |
|---|---|
| `migrations/2026_08_17_130000_scope_grade_uniqueness_to_company.php` | **নতুন** — global `grade_code` unique বাদ; `unique(['company_id','grade_code'])` + `unique(['company_id','grade_name'])` |
| `GradeStoreRequest.php` | `unique:grades,grade_name` → `->where('company_id', $companyId)` |
| `GradeUpdateRequest.php` | company scope + `route('grade')` → `route('id')` |
| `GradeService.php` | `generateGradeCode()` এখন companyId নেয়, আর free না পাওয়া পর্যন্ত sequence এগোয় (`gradeCodeExists()` অবশেষে ব্যবহৃত) |
| `GradeRepository.php` | `FIELD(...)` → portable `CASE WHEN status = 'active' THEN 0 ELSE 1 END` |

**Migration নিরাপদ ছিল:** আগে global unique থাকায় duplicate তৈরি হওয়াই সম্ভব ছিল না, তাই composite index বসাতে কোনো সংঘর্ষ হয়নি — চালানোর আগে `(company_id, grade_code)` আর `(company_id, grade_name)` দুটোতেই duplicate গুনে যাচাই করা হয়েছে (০)।

### যাচাই

`GradeCompanyScopingTest` — **৫/৫ সবুজ**:

- দুই company একই নামের grade রাখতে পারে, **এবং একই code-ও পায়** (sequence per company)
- একই company-তে duplicate নাম এখনো refused
- একই code-এ নামানো দুটো নাম আলাদা code পায়
- নাম না বদলে save করা যায়
- এক company-র grade অন্য company-র list-এ নেই

Index যাচাই: `grades_company_id_grade_code_unique` + `grades_company_id_grade_name_unique`, global-টা নেই।

Feature suite baseline-এর সাথে **হুবহু এক**।

> **এখনো বাকি:** `FIELD(...)` একই ভাবে `DivisionRepository`, `TeamRepository`, `BranchRepository`, `EmployeeIdCardSettingRepository`-তেও আছে (৪টা)। একই এক-লাইন ফিক্স, কিন্তু এই কাজের বাইরে বলে হাত দেওয়া হয়নি — ওগুলোর list endpoint-ও sqlite-এ test করা যায় না।

---

## 🔍 Company-wise কাজটার post-merge review (18 Aug 2026)

PR #196 merge হওয়ার পর পুরো change set আবার পড়া হয়েছে। যা নিশ্চিত হলো, আর যা এখনো খোলা আছে — দুটোই নিচে।

### যাচাই হয়েছে

| কী | ফলাফল |
|---|---|
| Tenant scope ৪৭ + ৩ + ১ = **৫১টা model**-এ | সবগুলোয় `CompanyScope` registered |
| খালি query-র আসল SQL | `where company_id = 1` নিজে থেকেই বসে |
| Company resolve না হলে | `where 1 = 0` — fail-closed |
| নতুন তিনটে master-এর unique index | `(company_id, code)` + `(company_id, name)` |
| `grades`-এর index | global-টা নেই, composite দুটো আছে |
| Repository + scope একসাথে | SQL-এ `where company_id = 2 and grades.company_id = 2` — দুই স্তর |
| Test | **৪৮টা সবুজ** (FlatMasters ১৫ · OrgMasters ৯ · Grade ৫ · DataScoping ১১ · ContextScoping ৮) |
| Feature suite | baseline-এর সাথে হুবহু এক, নতুন failure নেই |
| `tsc -b` / `oxlint` | ৭ (পুরোনো baseline) / ০ error |
| Scratch ও generator ফাইল | কিছুই commit হয়নি |
| Dead demo helper | `mapDemoToOptions`, `resolveDemoLabel`, `getActiveOptions`, ৬টা `DEMO_*` তালিকা — সব মুছে গেছে |
| `useToast` identity | `useMemo`-করা, তাই `FlatMasterList`-এর effect loop করে না |
| N+1 | নতুন Resource গুলো কোনো relation ছোঁয় না |

### Review-তে যা বেরিয়েছে (এখনো খোলা)

**১. Assignment history এখনো ভুয়া row দেখাতে পারে.** `OrganizationTab`-এ `showDemoHistory` — history API **ব্যর্থ হলে বা খালি ফেরত দিলে** `DEMO_ASSIGNMENT_HISTORY` দেখায়। অর্থাৎ যে employee-র কোনো assignment নেই, সে একটা পূর্ণ বানানো ইতিহাস দেখে। Dropdown-এর মতো নীরব নয় (উপরে "Showing sample records as a preview" banner আছে), কিন্তু এটাই ঐ একই demo-data প্যাটার্নের শেষ অবশিষ্টাংশ। *সঠিক আচরণ হবে — খালি হলে empty state, error হলে error।*

**২. নতুন তিনটে master hard-delete করে.** Branch/Division/Team soft-delete (`archived_at`) করে, কিন্তু Grade আর নতুন Section/CostCenter/WorkLocation করে না। আর `employee_organization_assignments`-এ **কোনো FK constraint নেই**, তাই ব্যবহৃত section মুছে ফেললে assignment row-তে dangling id থেকে যায় — history-তে নাম না দেখিয়ে `ID 5` দেখায় (`formatOrgName`-এর fallback)। ডেটা হারায় না, কিন্তু নামটা চিরতরে যায়।

**৩. `FIELD(...)` আরও ৪ জায়গায়.** `DivisionRepository:39`, `TeamRepository:38`, `BranchRepository:62`, `EmployeeIdCardSettingRepository:49` — MySQL-only, sqlite-এ ভাঙে, তাই ওদের list endpoint test করা যায় না। Grade-এরটা ঠিক করা হয়েছে, বাকি চারটে নয়।

**৪. `GradeService::update()` নিয়ম মানে না.** `$data['grade_name']` সরাসরি পড়ে, অথচ `GradeUpdateRequest`-এ ওটা `nullable` — নাম না পাঠিয়ে শুধু status বদলাতে চাইলে undefined key। Tenancy-র বাইরে, আলাদা বাগ।

**৫. Structure FK গুলোয় DB-স্তরে constraint নেই.** Service প্রতিটা optional master company-র বিপরীতে যাচাই করে, তাই application পথে নিরাপদ। কিন্তু seeder/console/সরাসরি insert-এ অন্য company-র id বসানো আটকাবে না — defence in depth হিসেবে FK যোগ করা যেত।

> ১–৫ কোনোটাই tenant leak নয় — scoping-এর দিকটা পুরো বন্ধ। এগুলো ডেটা-মান আর portability-র বিষয়, আলাদা করে ধরা দরকার।

---

## 🧹 Attendance nav থেকে "Punches" সরানো হলো (18 Aug 2026)

**যা হতো:** Attendance menu-তে **Punch** আর **Punches** — দুটো আলাদা entry পাশাপাশি বসে ছিল। প্রথমটা কাজ করত (`/attendance/punch`, নিজের check-in/out), দ্বিতীয়টার `path: "#"` — ক্লিক করলে কিছুই হতো না।

**কেন ছিল:** task card-এ Attendance-এর ১৫টা menu সংজ্ঞায়িত, তার একটাও "Punches" নয় — শুধু `Attendance › Punch` (3.1-FE), যেটা বানানো হয়ে গেছে। entry-টা পরিকল্পনার বাইরে, আগেভাগে বসানো হয়েছিল।

**কেন page বানানো হলো না, সরানো হলো:** punch ডেটা ইতিমধ্যে দুই জায়গায় পৌঁছানো যায় — কর্মী নিজেরটা দেখে `/attendance/punch`-এ, আর record detail-এ পুরো timeline আসে `attendance-records/{id}/punches` থেকে। তৃতীয় একটা global list একই ডেটার আরেকটা দরজা হতো।

| ফাইল | পরিবর্তন |
|---|---|
| `core/config/navigation.ts` | `attendance-punches` entry বাদ (৬ লাইন) |

**Backend-এ হাত দেওয়া হয়নি** — `GET /attendance/punches` endpoint-টা `punchApi.ts` হয়ে Punch page-ই ব্যবহার করে, তাই ওটা থেকে যাচ্ছে।

**যাচাই:** `attendance-punches` id পুরো repo-তে আর কোথাও ছিল না (backend-এ DB-driven menu-তেও নয়), তাই সরানো নিরাপদ। `tsc -b` baseline-এর সমান (৭), `oxlint` ০ error, আর কাজ করা **Punch** entry অক্ষত।

### Nav-এ dead link এখন ৪টা

`path: "#"` থাকা leaf entry — অর্থাৎ যেগুলোয় child-ও নেই, page-ও নেই:

| Menu | Backend | Frontend | Task card |
|---|---|---|---|
| **Monthly Approval** | ✅ ৯টা endpoint | ❌ | ✅ |
| Payroll Runs | ❌ | ❌ | ✅ |
| Payslips | ❌ | ❌ | ✅ |
| Disbursements | ❌ | ❌ | ✅ |

> **Corrections আর Leave-এর `#` ভুল নয়** — ওদের ভেতরে সঠিক path-সহ child আছে (Requests / Approvals / Balances), তাই ওরা Attendance-এর মতোই group header। এক নজরে দেখে ভাঙা মনে হয়েছিল, আসলে নয়।

**পরের সবচেয়ে মূল্যবান কাজ: Monthly Approval UI.** ৯টা endpoint (list · show · breakdown · build · approve · bulk-approve · unlock) তৈরি হয়ে অব্যবহৃত পড়ে আছে, আর এটাই attendance থেকে payroll-এ যাওয়ার দরজা — মাস approve না হলে payroll run শুরুই করা যায় না।

---

## 🧾 সব ঋণ শোধ — এক পাসে (18 Aug 2026)

এতদিন যে ঋণগুলো জমছিল, আজ সবগুলো ধরা হয়েছে। এক লাইনে ফল: **backend suite 452 passed / 0 failed**, `tsc -b` **০ error**, `npm run build` সবুজ, `oxlint` **০ error**।

শুরুতে suite চালানোই যেত না। এখন যায়, আর পুরোটা সবুজ।

### ক. ভিতের চারটে ঋণ

| ঋণ | কী করা হলো |
|---|---|
| তিনটে executor-এ `reject()` নেই → `php artisan test` fatal | তিনটেতেই যোগ। দুটো (`PayrollRunExecutor`, `EmployeeBankAccountExecutor`) সত্যিকারের no-op — bank account-এর লেখা approval-এর আগে হয়ই না, তাই reject-এ ফেরানোর কিছু নেই। **কিন্তু `SalaryAdvanceExecutor`-এ no-op রাখলে আসল bug থাকত** — reject করা advance `pending_approval`-এই আটকে থাকত, মাসের ceiling খেয়ে বসে থাকত, কর্মী আর নতুন request দিতে পারত না। তাই `SalaryAdvanceService::reject()` লেখা হয়েছে (`approve()`-এর মতোই idempotent) |
| `phpunit.xml`-এ module testsuite নেই → ৬০টা test কখনো চলত না | `<testsuite name="Modules">` যোগ (`Modules/*/tests`), সাথে coverage-এ `Modules/*/app`। এখন `php artisan test` **৪৫২টা** test চেনে |
| ২০টা লাল test — `No active shift found for the given date` | নিচে "আসল কারণ" দেখুন |
| `tsc -b` ৭ error | সাতটাই সারানো — ছয়টা `CorrectionApprovalDetailPage.tsx`-এ (unused import, ভুল import depth, `mutate()`-এ argument), একটা `ActivityLogPropertiesViewer.tsx`-এ (`unknown` JSX-এ) |

### 🔍 ঐ ২০টা লাল test-এর আসল কারণ — timezone নয়

ডকে এতদিন সন্দেহ ছিল "timezone অমিল"। **আসল কারণ date-এর storage format:**

Laravel-এর `date` cast লেখে `Y-m-d H:i:s` ফরম্যাটে। MySQL-এর `DATE` column সেটা কেটে `2026-08-18` করে রাখে, কিন্তু **sqlite হুবহু স্ট্রিংটাই রাখে**। ফলে `AssignmentRepository`-র তুলনা —

```
'2026-08-18 00:00:00' <= '2026-08-18'   →  false
```

— sqlite-এ স্ট্রিং তুলনা হয়ে মিথ্যা হয়ে যেত, তাই shift কখনো resolve হতো না। **Production (MySQL)-এ bug ছিল না**, কিন্তু test কখনো চলতে পারত না।

ফিক্স: `whereDate()` — দুই engine-এই কাজ করে। `AttendancePunchRepository`-তে ঠিক এই প্যাটার্নটা আগেই ছিল, একই কমেন্ট সহ; assignment-এরটা বাদ পড়ে গিয়েছিল।

### খ. পথে যা আসল bug বেরোল

| # | Bug | কেন গুরুতর |
|---|---|---|
| ১ | 🔴 **`attendance:schedule-close-day` কোথাও register করা নেই** | `routes/console.php`-এ `Schedule::command(...)->hourly()` লেখা আছে, কিন্তু module-এর command auto-discover হয় না আর provider-এ `$commands` comment-out করা ছিল। `php artisan list`-এ command-টা ছিলই না — অর্থাৎ **9.1-BE-র nightly close কোনোদিন চলতে পারত না**। ঠিক যে test-ঋণটা বাকি ছিল, সেটাই এটা ধরল |
| ২ | **`sequence_no` superseded punch-কে গোনে না** | 5.5-BE correction করলে নতুন punch আগের নম্বরটাই আবার পেত — একই দিনে দুটো punch-এর sequence 1, timeline-এর ক্রম নষ্ট। এখন `maxSequenceNo()` সব punch ধরে গোনে, আর consecutive-check আগের মতোই non-superseded দেখে (দুটো আলাদা প্রশ্ন, আলাদা query) |
| ৩ | **Correction submit-এর response মিথ্যা status দিত** | approval configure করা না থাকলে gateway সাথে সাথেই execute করে (documented bypass), কিন্তু response-এ in-memory model যেত — client দেখত `pending`, DB-তে `approved`। এখন `refresh()` করে ফেরে |
| ৪ | **`GradeService::update()` partial update-এ ভাঙত** | `$data['grade_name']` সরাসরি পড়ত অথচ rule-এ `nullable` — status-only update-এ undefined key, আর name-only update-এ status **null হয়ে যেত**। এখন যেটা পাঠানো হয়েছে কেবল সেটাই লেখা হয় |
| ৫ | **`include_common` নিজের docblock ভাঙছিল** | config-এর নিজের নোট বলে attendance/payroll হলো curated registry-র উদাহরণ, অথচ দুটোরই `include_common => true` ছিল — ফলে `attendance.view`, `payroll.create` ধরনের ১৫টা করে permission তৈরি হতো যা **কোনো route যাচাই করে না**। যাচাই করে দেখা গেছে ঐ key গুলো backend/frontend কোথাও ব্যবহার হয় না, তাই `false` করা হয়েছে |
| ৬ | `ProcessCloseDayChunkJob`-এ leftover `info($employee->first_name)` | per-employee loop-এ context-হীন debug log |

### গ. Card-এর DoD-ঋণ — সবগুলো

| Card | ঋণ | এখন |
|---|---|---|
| **9.1-BE** | দুই-timezone scheduler test · idempotency test · unassigned report | ✅ `ScheduleCloseDayTest` — **৪টা test**। এক cron থেকে কেবল যে company-র স্থানীয় সময় ২টা সেটাই dispatch হয় (Dhaka vs New York), দুবার চালালে record একটাই থাকে (`updated_at` পর্যন্ত অপরিবর্তিত), আর unassigned কর্মী `attendance.day_closed` activity log-এ নাম সহ যায় |
| **3.2-BE** | session-pairing test | ✅ `AttendanceSessionPairingTest` — **৩টা**। lunch break বাদ পড়ে (last-minus-first হলে ৯ ঘণ্টা দেখাত, সঠিক ৮), অজোড়া trailing in-punch সময় যোগ করে না, superseded punch pairing-এ ঢোকে না |
| **3.2-BE** | visibility-scoping test | ✅ `AttendanceRecordVisibilityTest` — **৫টা**। own / team / all তিন স্তরই মাপা, আর **`show` ও `punches` endpoint list-এর মতোই সীমা মানে** — অন্যের record-এর id জানলেও 403 |
| **7.4-BE** | idempotency + negative-net-pay test | ✅ `EmployeeDeductionTest`-এ **৪টা** নতুন। একই run-এ দুবার apply করলে balance একই থাকে আর entry একটাই, আলাদা run নিজের installment নেয়, net pay ঋণাত্মক হলে skip হয়ে HR-এর জন্য flag বসে, শেষ installment-এ status `completed` |

**9.1-BE-র তৃতীয় ঋণ নিয়ে সংশোধন:** ডকে লেখা ছিল unassigned কেবল `Log::info`-তে যায়। **সেটা বাসি** — `ProcessCloseDayChunkJob` ওটা `attendance.day_closed` activity log-এ পাঠায়, `GET /api/v1/activity-logs?event=attendance.day_closed` দিয়ে পড়া যায়, আর `ActivityLogPropertiesViewer` frontend-এ render-ও করে। **`9.4-FE` এখন অপেক্ষা করছে না।**

### ঘ. 18 Aug-এর review-এ ওঠা পাঁচটা

| # | কী | এখন |
|---|---|---|
| ১ | Assignment history খালি/error হলে বানানো row দেখাত | ✅ `showDemoHistory` সরানো। খালি হলে empty state, error হলে error alert। `organizationDemoData.ts` **মুছে ফেলা হয়েছে** (আর কোনো consumer ছিল না), সাথে demo/real union type-এর `'x' in record` narrowing গুলোও |
| ২ | Grade + নতুন তিন master hard-delete করত | ✅ চারটেতেই `SoftDeletes` + `DELETED_AT = 'archived_at'` (Branch-এর প্যাটার্ন), migration `2026_08_18_090000_add_archived_at_to_org_masters_tables`। মুছে ফেলা master-এর নাম এখন history-তে থাকে, `ID 5` দেখায় না |
| ৩ | `FIELD(...)` আরও ৪টা repo-তে | ✅ চারটেই `CASE WHEN` — `BranchRepository`, `DivisionRepository`, `TeamRepository`, `EmployeeIdCardSettingRepository`। Grade-এর ফিক্সের সাথে এক প্যাটার্ন |
| ৪ | `GradeService::update()` | ✅ উপরের খ-৪ |
| ৫ | Structure FK-তে DB-স্তরে constraint নেই | ⏸ **করা হয়নি** — নিচে দেখুন |

### 🔧 যেসব test সারানো হয়েছে (কোড নয়, test-ই ভুল ছিল)

- **`AttendanceDailyCalculationTest`** — punch helper local wall-clock সময় সরাসরি লিখত, অথচ column UTC। company timezone Asia/Dhaka (+6) হওয়ায় গোটা offset-টাই lateness হয়ে যেত — এটাই কুখ্যাত **`late_minutes` 390 বনাম 30**। **Production কোড ঠিকই ছিল**, helper এখন UTC-তে রূপান্তর করে
- **`PunchServiceTest`** — তিনটে overnight test `seedAssignedShift()`-এর **পরে** `Carbon::setTestNow()` ডাকত, তাই assignment "আজকের" তারিখে বসে ঘড়ি পিছিয়ে যেত আর কখনো effective হতো না। ক্রম উল্টে দেওয়া হয়েছে
- **`PunchApiTest`** — `User::factory()` দিয়ে role-হীন user বানাত, tenancy header পাঠাত না। এখন seeded demo company + আসল `employee` role, আর permission blanket-mock না করে **সত্যিকারের permission যোগ করে** মাপা হয় (নইলে 403 assert-এর কোনো মানে থাকে না)
- **`AssignmentResolutionServiceTest`** — `Company::factory()` ডাকত, যেটার অস্তিত্বই নেই
- **`MonthlyAttendanceApprovalServiceTest`** — hardcoded `companyId = 1`, `actorId = 1`; activity log-এর তিনটে FK ভাঙত। আর override-এর প্রমাণ `Log::info` mock দিয়ে খুঁজত, অথচ কোড `activityLogService` ব্যবহার করে — এখন activity log row-ই assert করা হয়
- **`AttendancePolicyApiTest`** — stub `leave_balances` table বানাত, আসল migration-এর সাথে সংঘর্ষ
- **`AttendanceRecalculationApiTest`** — পুরো বাসি: ভুল URL (`attendance-records/42` বনাম `records/{id}`), বানানো payload key, আর যে response shape endpoint কখনো দেয় না। আসল আচরণে নতুন করে লেখা, সাথে cross-company 404 কেস
- **`AttendancePayrollApprovalIntegrationTest`** — platform wiring মাপে, অথচ অস্তিত্বহীন leave request id-তে executor চালাত। এখন stub executor (`payroll_run`) ব্যবহার করে, তাই leave domain-এর fixture টানতে হয় না — ঐ পথ `LeaveApprovalExecutionTest` আগেই ঢাকে
- **`CorrectionRequestApiTest`** — `pending` আশা করত, কিন্তু ঐ company-তে approval configure করা নেই মানে gateway-র documented bypass, অর্থাৎ সাথে সাথে approved। assertion আসল আচরণে আনা হয়েছে (আর response-এর মিথ্যা status-টাও সারানো — খ-৩)
- **`EmploymentTypeRepositoryTest`** — constructor-এ argument দিত না

### ⏸ যেটা ইচ্ছাকৃতভাবে করা হয়নি

**`employee_organization_assignments`-এ FK constraint (review item ৫)।** চলমান table-এ FK বসানো মানে আগের ডেটায় একটাও অবৈধ id থাকলে migration production-এ ফেল করবে। Service স্তরে প্রতিটা optional master ইতিমধ্যে company-র বিপরীতে যাচাই হয়, আর **soft-delete (ঘ-২) আসল ক্ষতিটা — নাম হারানো — বন্ধ করে দিয়েছে**। FK-টা defence in depth, আলাদা card হিসেবে ডেটা যাচাই সহ নেওয়া উচিত।


---

## 📋 `9.4-FE` Attendance Jobs UI — merge-পরবর্তী review (18 Aug 2026, PR #197 · Munna)

Branch `att-9-4-fe-attendance-jobs`, ১৯টা ফাইল (+1726 / −13)। **Card-টা done** — screen live, তিনটে ট্রিগারই আসল endpoint-এ যায়। নিচে যা যাচাই হয়েছে আর যা ঋণ থেকে গেল।

### যা সত্যিই এসেছে

| Card যা চেয়েছিল | কোডে |
|---|---|
| Route `/attendance/jobs` + nav entry | ✅ `attendance/index.tsx` (`attendance.menu-view` gate-এর ভিতরে) + `core/config/navigation.ts` — **Attendance › Jobs** |
| `attendanceJobApi.ts` + batch-status polling | ✅ চারটে endpoint wrap করা; `useAttendanceJobs` ২ সেকেন্ডে poll করে, `finished`/`failed`/`cancelled`-এ থামে |
| Batch progress — page ছেড়ে ফিরলেও টিকবে | ✅ batch id `sessionStorage` **আর** `?batch=` দুটোতেই, তাই reload ও নতুন tab দুটোতেই ফেরে |
| Manual run — permission না থাকলে **hidden, disabled নয়** | ✅ `ManualRunPanel.tsx:25` — দুটো permission-ই না থাকলে `null` return; tab গুলোও আলাদা করে gate করা |
| Re-run accrual = "0 applied" success, error নয় | ✅ BE `batchId` না দিলে `applied: 0` আসে, hook ওটাকে `toast.success` করে |
| Unassigned report primary content | ✅ screen-এর মূল card, খালি হলে "All Clear" state |
| Range enforce (close-day ≤ 90 দিন, accrual ±5 বছর) | ✅ date/number input-এর `min`/`max`-এ |

**BE-তেও ছোঁয়া লেগেছে** (card-এ ছিল না, কিন্তু যুক্তিসঙ্গত): `LeaveBalanceService::processMonthlyAccrualForEmployee()` এখন **annual entitlement cap** মানে — `entitled_days` কখনো `entitlement_days` ছাড়ায় না, আর যা সত্যিই লেখা হয়েছে সেটাই `true` ফেরায় (আগে সবসময় `true`)। `ProcessLeaveAccrualChunkJob` ও `ProcessCarryForwardChunkJob` এখন `leave_accrual.completed` / `leave_carry_forward.completed` activity log লেখে — **status panel-এর একমাত্র উৎস এটাই**।

### যাচাই

| কী | ফল |
|---|---|
| `php artisan test Modules/Attendance/tests` | **79 passed / 233 assertions** |
| `php artisan test` (পুরো) | **453 passed / 1438 assertions** |
| `npx tsc -b --force` | **০ error** |
| `npx vite build` | ✅ সবুজ (1.81 MB bundle, warning কেবল chunk size) |
| Route ↔ FE call মিল | ✅ `POST /jobs/close-day` · `/jobs/accrue-leave` · `/jobs/carry-forward` · `GET /jobs/{batchId}` · `GET /jobs/close-day/{batchId}` — response key `batchId` / `applied` দুই দিকেই মেলে |

### ⚠ চারটে ঋণ

**১. "Fix Assignment" button কিছুই filter করে না।** `UnassignedEmployeesReport.tsx:113` লিংক করে `/attendance/config/assignments?search=<নাম>`, কিন্তু `AssignmentListPage.tsx` শুধু `assignable_type` · `assignable_subtype` · `scope_type` · `scope_id` · `history` · `drawer` · `end` পড়ে — **`search` নামে কোনো param ওখানে নেই**, তাই HR গোটা assignment list-এ নামে। Card-এর AC ছিল "one click থেকে ঐ employee-তে পৌঁছানো"। হয় list-এ employee search যোগ করতে হবে, নয় `employee_id` দিয়ে drawer খুলতে হবে।

**২. Company timezone hardcoded।** `AttendanceJobsPage.tsx:103,118,133,203` — তিন job-এই `timezone: 'Asia/Dhaka'`, আর `lastRunAt` আসে `new Date(...).toLocaleString()` থেকে, অর্থাৎ **browser-এর timezone**-এ render হয়ে "Asia/Dhaka" লেবেল পায়। Card স্পষ্ট বলেছিল company-local সময় zone সহ, কারণ job গুলো per-company timezone-এ চলে। এক-দেশীয় tenant-এ চোখে পড়বে না, বিদেশি company বা ভ্রমণরত HR-এর কাছে ভুল সময় দেখাবে।

**৩. Status panel unfiltered activity log পড়ে।** `AttendanceJobsPage.tsx:30-38` — `module_slug: 'attendance'`, `per_page: 30`, **কোনো `event`/`action_key` filter নেই** (`activityLogsApi` দুটোই support করে)। ব্যস্ত tenant-এ সাম্প্রতিক ৩০টা attendance log-এ job entry না থাকলে panel "Never run" আর unassigned report "All Clear" দেখাবে — অথচ job চলেছে। এটা false negative, আর এই screen-টাই ঐ employee-দের একমাত্র জানালা। সাথে ছোট দুটো: unassigned নাম regex দিয়ে `"নাম (ID: 12)"` string থেকে ছেঁড়া হয় (log format বদলালেই ভাঙবে), আর `employee_no` বানানো হয় `EMP-{id}` (`AttendanceJobsPage.tsx:67`) — ওটা আসল employee number নয়।

**৪. `JobStatusPanel.tsx` (109 লাইন) dead code** — কেউ import করে না, `JobAnalyticsOverview` ওর জায়গা নিয়েছে। এছাড়া chunk job প্রতি একটা করে activity log লেখা হয় বলে page-এর `logs.find(...)` **একটা chunk-এর** processed/created দেখায়, পুরো run-এর নয় (একাধিক chunk হলে সংখ্যা কম দেখাবে; `9.1-BE`-তেও একই আচরণ)। আর `getBatchStatus` আগে generic endpoint ধরে — কেবল `attendance.record-recalculate` আছে এমন user-এর ক্ষেত্রে প্রতি poll-এ একটা 403 খেয়ে তবেই close-day endpoint-এ fallback করে।

**ছোট দুটো নোট:** `ActivityLogPropertiesViewer.tsx` ও `CorrectionApprovalDetailPage.tsx`-এ merge conflict সামলাতে `as any` cast বসেছে — type debt, দুই লাইনের। আর manual trigger button (`AttendanceJobsPage.tsx`-এর header) permission-gated নয়; panel `null` ফেরায় বলে permission-হীন user একটা **খালি modal** পায় — card বলেছিল control গুলো hidden থাকবে।


---

## 📋 `6.1-FE` Monthly Approval UI — merge-পরবর্তী review (18 Aug 2026, PR #198 · Bablu)

Branch `att-6-1-fe-monthly-approval`, ২২টা ফাইল (+1447 / −59)। Screen live — **Attendance › Monthly Approval** এখন `#` নয়, আসল route-এ যায়। কিন্তু **দুটো gate ভেঙে merge হয়েছে**, আর card-এর কয়েকটা মূল rule আসেনি।

### যা এসেছে

| Card যা চেয়েছিল | কোডে |
|---|---|
| Route + nav entry | ✅ `/attendance/monthly-approval` ও `/{id}`, দুটোই `attendance.monthly-view` gate-এ; nav ও `AttendanceHomePage` দুটোতেই dead `#` link সরানো হয়েছে |
| `monthlyAttendanceApi.ts` | ✅ সাতটা call — build · list · show · breakdown · approve · bulk-approve · unlock · batch-status |
| Monthly grid + filter | ✅ month/year/status filter, employee search (BE-তে নতুন `search` filter সহ), row checkbox + "select all visible", pagination |
| Bulk approve → per-employee result | ✅ queued batch, ১ সেকেন্ডে poll, progress bar, শেষে succeeded/failed আলাদা table (failed-এ নাম + কারণ) |
| Build / rebuild month | ✅ drawer — এখন department-wide বা whole-company bulk build-ও পারে |

**BE-তে যা বদলেছে** (card FE-only ছিল, কিন্তু PR-এ বড় BE কাজ আছে): `POST /monthly-attendance/build` `employee_id` ছাড়া দিলে এখন **202 queued** ফেরায় (`BuildBulkMonthlyAttendanceJob`), `POST /bulk-approve`-ও **202 queued** (`ProcessBulkMonthlyApproveJob`), আর নতুন `GET /monthly-attendance/batch/{batchId}` cache থেকে progress পড়ে। `list()` Eloquent ছেড়ে `paginateByCompany()` repository-তে গেছে, department filter `employee.designation` ছেড়ে `employee.currentOrganizationAssignment`-এ (এটা ঠিক দিকেই বদল)।

### 🔴 দুটো gate লাল

**১. `npm run build` লাল — `tsc -b` ১৫ error**, সবই নতুন ফাইলে:

```
BuildMonthlyAttendanceDrawer.tsx(108,7)  TS2322  Drawer-এ 'description' prop নেই
BulkApproveDrawer.tsx(100,7)             TS2322  একই
UnresolvedIssuesPopover.tsx(10,51)       TS6133  'summary' অব্যবহৃত   ← নিচের ঋণ ৩-এর প্রমাণ
MonthlyApprovalDetailPage.tsx(8,3)(20,9) TS6133  'buttonStyles' · 'navigate'
MonthlyApprovalListPage.tsx(8,3)         TS6133  'buttonStyles'
MonthlyApprovalListPage.tsx ×9           TS7006  Parameter 's' implicitly has an 'any' type
```

`Drawer`-এ `description` prop নেই মানে **bulk drawer-এর "You are about to approve N records" লাইনটা render-ই হয় না** — card-এর pre-flight তথ্যটা তাই পর্দায় নেই।

**২. `php artisan test` — 1 failed / 452 passed।** `MonthlyAttendanceApprovalServiceTest:48` এখনো `$results['success']` পড়ে, কিন্তু `bulkApprove()` এখন `['mode' => 'queued', 'batch_id' => …, 'succeeded' => [], 'failed' => []]` ফেরায়। **6.1-BE-র নিজের test, contract বদলে আপডেট করা হয়নি** — অর্থাৎ merge-এর আগে suite চলেনি।

### ⚠ ছয়টা DoD-ঋণ

**১. 🔴 Approve সবসময় `override: true` পাঠায়।** `MonthlyApprovalDetailPage.tsx:33` — `approve(summaryId, { override: true })`, কোনো checkbox নেই। Card action #3/#4 স্পষ্ট: override **default-এ off**, আর "unresolved issues will be approved as-is" লেখা থাকতে হবে। এখন যা হচ্ছে — `6.1-BE`-র unresolved guard UI থেকে কার্যত নিষ্ক্রিয়, HR না জেনেই override করে ফেলছে। Banner-টাও তাই বলছে: "Approving will override these warnings"। **এটাই এই PR-এর সবচেয়ে গুরুতর জিনিস।**

**২. Unlock reason hardcoded।** `MonthlyApprovalDetailPage.tsx:41` — `unlock(id, { reason: 'Requested by admin' })`; modal-টা সাধারণ `ConfirmDialog`, reason input নেই। Card action #7 reason বাধ্যতামূলক করেছিল, আর BE ওটা audit-এ লেখে — এখন প্রতিটা unlock-এ একই বাক্য জমা হচ্ছে, অর্থাৎ audit trail-টা অর্থহীন।

**৩. Unresolved popover decorative।** `UnresolvedIssuesPopover.tsx` কেবল একটা সাধারণ বাক্য দেখায় — "There are pending corrections, leave requests, or missing check-outs" — **কোন blocker, কোন deep link কিছুই নেই** (`summary` prop নেওয়া হয়েছে কিন্তু ব্যবহারই হয়নি, tsc সেটাই ধরেছে)। Card-এর UI rule আলাদা করে বলেছিল "actionable, not decorative"। **মূল কারণ BE-তে**: `getUnresolvedIssuesList()` কেবল approve-এর সময় exception-এ যায়, কোনো resource `unresolved_issues` expose করে না — অর্থাৎ FE-র হাতে দেখানোর মতো ডেটাই নেই। সারাতে হলে `MonthlyAttendanceApprovalResource`-এ list-টা আনতে হবে।

**৪. Day-by-day breakdown নেই।** `MonthlyApprovalDetailPage.tsx`-এ placeholder লেখা — "Day-by-day breakdown grid goes here"। `getBreakdown()` API-তে আছে, কেউ ডাকে না। Card-এর AC ছিল "no number is a dead end"; এখন প্রতিটা summary সংখ্যাই dead end।

**৫. Reject action নেই।** Card action #6 (`POST /approval-requests/{id}/reject`, reason বাধ্যতামূলক) — পুরো screen-এ reject বলে কিছু নেই, filter-এ শুধু `rejected` status দেখা যায়।

**৬. Permission gating নেই, আর frozen অবস্থায় Unlock লুকোয় না।** দুটো page-এর কোথাও `usePermissions` নেই — `attendance.monthly-view` থাকলেই Approve ও Unlock button দেখা যায়, যদিও card `monthly-approve` / `monthly-unlock` আলাদা করে চেয়েছিল (BE অবশ্য 403 দেবে, তাই ফাঁস নয় — কিন্তু UX ভুল)। আর Unlock দেখানো হয় শুধু `is_locked` দেখে; **`frozen_at` কোথাও চেক হয় না**, অথচ card বলেছিল frozen হলে action-টা disable নয়, **সম্পূর্ণ hide** হবে।

### 🔍 BE-তে দুটো জিনিস আলাদা করে দেখার

**ক. `getIdsByCompany()`-তে active filter নেই।** `EmployeePersonalInfoRepository.php:198` — company-র **সব** employee তোলে; পাশের `chunkActiveWithoutAttendance()` (একই ফাইল, লাইন 186) `where('status', 'active')` করে। ফলে bulk build **terminated employee-দের জন্যও** monthly summary বানাবে, আর ওরা approval grid-এ এসে মাস বন্ধ করা আটকাবে।

**খ. Batch progress প্রতি employee-তে একবার cache-এ লেখা হয়।** `processBulkBuildBatch()`/`processBulkApproveBatch()` লুপের ভিতরে `putBatch()` ডাকে, আর `CACHE_STORE=database` — অর্থাৎ ৫০০ employee মানে **৫০০টা বাড়তি DB write**, আসল কাজের উপরে। Chunk-প্রতি একবার লিখলেই progress bar-এর জন্য যথেষ্ট। সাথে: batch state কেবল cache-এ (TTL ২৪ ঘণ্টা), cache flush হলে চলমান run-এর status হারিয়ে যায় — 404 ফেরে।

**গ. নতুন কিছুরই test নেই** — `bulkBuild` · `processBulkBuildBatch` · `bulkApprove`-এর queued path · `processBulkApproveBatch` · `getBatchStatus`, একটাও নয়। উল্টো পুরনো test-টা লাল হয়ে পড়ে আছে।

---

## ✅ Policy Group — PHASE 1: Assignment Foundation (18 Aug 2026)

Policy Group feature-এর ভিত্তি। Group এখনো বানানো হয়নি — আগে assignment layer-টা এমন করা হলো যাতে একই employee-তে একাধিক leave policy বসতে পারে আর একই কাজ বারবার চললেও duplicate না হয়।

### কেন লাগল

`assertNoActiveOverlap()` overlap খুঁজত `(scope, assignable_type, assignable_subtype)` দিয়ে — `assignable_id` **key-তে ছিল না**। ফলে একজন employee-কে Casual Leave দেওয়ার পর Sick Leave দিতে গেলেই `AssignmentOverlapException`। Policy Group-এর গোটা ধারণাটাই এখানে আটকে যেত।

### যা বদলাল

**১. Cardinality — hack নয়, declared rule.** শুধু `assignable_id` key-তে জুড়ে দিলে **shift ভেঙে যেত** (একজন একসাথে দুই shift-এ থাকতে পারে না, resolver-ও একটাই shift slot ধরে)। তাই `Assignment::cardinalityFor()`:

| assignable | cardinality | overlap key |
|---|---|---|
| `shift` | exclusive | scope + type |
| `policy/holiday` | exclusive | scope + type + subtype |
| `policy/leave` | **multi** | scope + type + subtype + **assignable_id** |

**২. `assignOrGet()` — একমাত্র লেখার পথ।** Identity = *এক assignable + এক scope + এক effective_date*। একই identity-তে আবার ডাকলে আগের row ফেরে, event fire হয় না (তাই balance দুইবার seed হয় না)। `create()` এখন এরই alias, তাই manual/bulk/auto সবাই একই guard পায়। Race-এর জন্য DB unique index-ই সালিশ — `UniqueConstraintViolationException` ধরে winner row পড়ে ফেরত দেয়।

**৩. Company scope normalize।** আগে write-এ `scope_id = NULL`, কিন্তু তিন জায়গায় read হতো `scope_id = companyId` — অর্থাৎ **company-wide assignment কারো জন্যই resolve হতো না**। এখন storage-এ `company_id` বসে (API contract অপরিবর্তিত, `scope_id` না পাঠালেও চলে)। এটা ছাড়া unique index-ও কাজ করত না — MySQL NULL গুলো distinct ধরে।

**৪. `section` scope যোগ।** `sections` table আর `Section` model আগে থেকেই ছিল, কিন্তু `Assignment::allowedScopeTypes()`-এ ছিল না। Division/Team অক্ষত।

**৫. Scope hierarchy এক জায়গায়** — `Assignment::SCOPE_HIERARCHY`: `company < branch < division < department < section < team < employee`। Resolution precedence, scope set, validation map — তিনটাই এখান থেকে পড়ে, তাই ভবিষ্যতে Grade/WorkLocation যোগ করা এক লাইন।

**৬. `EmployeeScopeResolver` (নতুন)।** তিনটে service একই scope-OR query আলাদা করে লিখত — সেভাবেই `section` তিন জায়গা থেকেই বাদ পড়েছিল। এখন এক implementation, আর সেটা `findActiveOnDate()` ব্যবহার করে বলে **date-aware**: জুন-এ Sales → Support গেলে মে মাসের প্রশ্ন এখনো Sales পড়ে।

**৭. Balance seeding দুটো bug।** `CreateLeaveBalanceOnAssignment` employee খুঁজত org chart ঘুরে — নতুন joiner-এর org assignment না থাকলে **balance-ই তৈরি হতো না** (auto-assign-এ ঠিক এই অবস্থাই হয়)। এখন employee-scope assignment নিজের `scope_id` ব্যবহার করে। আর year আসত `Carbon::now()->year` থেকে; এখন `effective_date`-এর বছর থেকে, তাই backdated/future-dated ঠিক বছরে বসে। `createForEmployeeIfMissing()` find-then-create ছেড়ে `firstOrCreate`-এ গেছে (existing unique key-ই race guard)।

### Migration

`2026_08_19_100000_normalize_assignment_scope_and_add_identity_constraint` — `source` column (`manual`/`auto`/`bulk`), company-scope backfill, আর unique index `assignments_identity_unique (company_id, scope_type, scope_id, assignable_type, assignable_id, effective_date)`। আগে থেকে duplicate থাকলে migration **fail করে duplicate গুলোর নাম বলে দেয়** — নিজে কিছু delete করে না, কারণ merge/end করা business decision। MySQL-এ apply + rollback + re-apply তিনটাই যাচাই করা।

### Gate

`php artisan test` — **480 passed, 1 failed**। একমাত্র failure `MonthlyAttendanceApprovalServiceTest::bulk_approve_partial_failure` — উপরে PR #198-এর ঋণ হিসেবে যেটা আগে থেকেই লেখা আছে (`['success']` vs `['succeeded']`)। এই phase-এর কোনো ফাইল ওটা ছোঁয়নি।

### বাকি রইল

`getActiveEmployeePolicyPairs()` এখনো employee-প্রতি ২টা query করে (আগেও করত, খারাপ হয়নি)। বড় tenant-এ accrual dispatch ভারী — chunk-ভিত্তিক scope resolve করলে কমবে, কিন্তু সেটা আলাদা কাজ।

---

## ✅ Policy Group — PHASE 2: Policy Group Backend (19 Aug 2026)

Group, eligibility আর resolver তৈরি। **এই phase-এ একটাও assignment লেখা হয়নি** — সেটা Phase 3।

### গঠন

```
PolicyGroupResolver::plan()          → list<ResolvedPolicyAssignment>
  └─ PolicyGroupEligibilityEvaluator
       ├─ EligibilityContextBuilder     (৩ query, employee সংখ্যা যাই হোক)
       ├─ EligibilityAttributeRegistry  (attribute → context field)
       └─ GenderNormalizer
```

Resolver `bool` না, **plan ফেরায়** — `{ policyId, policyGroupId, effectiveDate, policySubtype }`। effective_date হিসাবটা এখানেই শেষ, তাই Phase 3-এর `AutoAssignmentService` নিছক loop → `assignOrGet()` হবে, ভিতরে eligibility logic থাকবে না।

### Migration (৩টা)

`attendance_policy_groups` (eligibility JSON, `attendance_policies.config`-এর ধারা) · `attendance_policy_group_policy` pivot · `assignments.policy_group_id`।

**Pivot-এ `company_id` রাখিনি** — group নিজেই tenant-scoped, আর duplicate column মানে drift-এর আরেকটা সুযোগ।

**FK দুই রকম, ইচ্ছাকৃত:** pivot-এ `cascadeOnDelete` (group ছাড়া pivot row অর্থহীন), কিন্তু `assignments.policy_group_id`-তে **`nullOnDelete`** — group মুছলে assignment ও তার balance/ledger বেঁচে থাকবে। MySQL-এ apply + rollback + re-apply যাচাই করা।

### প্রতি attribute-এ আলাদা class বানাইনি

`EligibilityAttributeRegistry` একটা table — প্রতিটা entry একই কাজ করে (context থেকে scalar তুলে comparator-কে দেয়)। ৯টা প্রায়-অভিন্ন class মানে দশম attribute এলে ভুলে যাওয়ার আরেকটা জায়গা। `gender` (normalize) আর `probation_status` (derived) — শুধু এই দুটোর আলাদা handler। নতুন attribute = array-তে এক লাইন।

### Gender

`GenderNormalizer` — trim + lowercase, তারপর `m`→`male`, `f`→`female`, `o`→`other`। এই mapping অনুমান নয়: [EmployeeImportService.php:276](backend/Modules/Employee/app/Services/EmployeeImportService.php#L276) spreadsheet-এর মান শুধু `strtolower()` করে, তাই `M`/`F` কলাম `m`/`f` হয়ে বসে। **তুলনার দুই পাশেই normalize হয়** — rule-এ `F` লিখলেও চলে। কোনো data migration নেই। অচেনা মান (`non-binary`) lowercase হয়ে যেমন আছে থাকে — ভুল rule-এ match করার চেয়ে না-match করা নিরাপদ।

### Probation

`confirmation_date` → নাহলে `probation_end_date` → **দুটোই null হলে `confirmed`**। `employee_employments.status` থেকে নয়, কারণ ওটা কারো মনে করে বদলানোর উপর নির্ভর করে, আর dates গুলো effective-dated সত্য — module-এর বাকি সব তারিখ-প্রশ্ন এভাবেই মীমাংসা হয়।

### API (৮টা route, ২টা permission)

`policy-group-view` / `policy-group-manage` — `config/actions.php` sort_order 22, 23।

CRUD + status toggle ছাড়াও দুটো:
- `POST /policy-groups/eligibility/preview` — **unsaved** eligibility-র matching count, UI-র live counter-এর জন্য
- `GET /policy-groups/attributes` — attribute/operator vocabulary, যাতে frontend তালিকাটা hardcode না করে

`/attributes` wildcard-এর **আগে** declare করা, নাহলে `{id}`-তে ধরা পড়ত।

### Service-level guard

`policy_ids` সব একই company-র ও Active হতে হবে · eligibility-র reference (department/grade/branch…) company-তে আসলেই আছে কিনা — **অন্য tenant-এর বা মুছে যাওয়া id দিলে 422**। এটা না থাকলে group নীরবে save হয়ে কাউকেই match করত, যেটা ধরা সবচেয়ে কঠিন ভুল।

### Review-এ তিনটে জিনিস সারানো হলো

**১. Registry প্রতি lookup-এ rebuild হচ্ছিল** — closure সহ। Batch preview হাজারবার পড়ে, তাই memoize করা।

**২. Resolver-এ N+1** — `planFor()` প্রতি employee-তে group query করত। Phase 3-এর backfill পুরো company ঘোরাবে, অর্থাৎ ৫০০০ employee = ৫০০০ group query। এখন instance-level cache, সাথে `forgetCachedGroups()` (queue worker job-এর মাঝে clear করবে)। **Regression test আছে** — ১৫ employee × ২ pass = ২ query।

**৩. delete()-এর comment মিথ্যে বলছিল** — "say what will be orphaned" লেখা ছিল, কোড কিছুই বলত না, আর `assignmentCount()` dead ছিল। এখন provenance হারানো assignment-এর সংখ্যা log হয়।

### Gate

`php artisan test` — **542 passed, 1 failed**। ওই একটাই পুরনো `MonthlyAttendanceApprovalServiceTest` (PR #198-এর ঋণ, উপরে লেখা)। Phase 1-এর ২৭টা test অবিকল সবুজ।

### বাকি

`policy_group_id` column আছে কিন্তু এখনো কেউ লেখে না — Phase 3-এ auto-assign লিখবে। Group cache instance-scoped, তাই Phase 3-এর listener প্রতি job-এ resolver নতুন করে resolve করবে (বা `forgetCachedGroups()` ডাকবে)।

---

## ✅ Policy Group — PHASE 3: Events + Auto-Assignment (19 Aug 2026)

Phase 2-এর plan এখন assignment-এ পরিণত হয়। Phase 2-এর কিছুই বদলায়নি।

```
Event → Listener → AutoAssignmentService → resolver->planFor() → assignOrGet()
```

### Event — ৩টা নতুন, ১টা পুরনো reuse

| Event | কী বদলাল | কেন eligibility-তে প্রভাব ফেলে |
|---|---|---|
| `EmploymentCreated` | employment record তৈরি | joining_date · employment_type · probation date — **এর আগে eligibility হিসাব করাই যায় না**, তাই personal-info creation থেকে কিছুই trigger হয় না |
| `EmploymentUpdated` | employment edit | Contract→Permanent, confirmation তারিখ, joining সংশোধন |
| `OrganizationAssignmentCreated` | প্রথমবার org-এ বসানো | branch/division/department/section/team/grade জানা গেল |
| `EmployeeTransferred` *(পুরনো)* | দপ্তর বদল | **নতুন event বানাইনি** — যেটা আছে সেটাতেই listener যোগ |

**Group create/update-এ কোনো event নেই** — ইচ্ছাকৃত। Group edit কাউকে ছোঁয় না, backfill আলাদা ও স্পষ্ট কাজ।

### 🔴 একটা সত্যিকারের race ধরা পড়ল

চারটে event-ই **transaction-এর ভিতরে** fire হয় (`EmployeeTransferred` আগে থেকেই তাই করত), আর `config/queue.php`-তে **প্রতিটা connection-এ `after_commit => false`**। মানে queued listener commit-এর আগেই চলতে পারে — যে row-এর কথা বলছে সেটা তখনো দেখা যায় না, বা transaction rollback হলেও কাজ হয়ে গেছে।

দুটো listener-এ `public bool $afterCommit = true;` বসানো হলো:
- নতুন `ReevaluatePolicyGroupsForEmployee`
- **`CreateLeaveBalanceOnAssignment`** — এটা Phase 1 থেকেই latent ছিল (`AssignmentChanged` `assignOrGet`-এর transaction-এর ভিতরে fire হয়)। Test-এ `QUEUE_CONNECTION=sync` বলে ধরা পড়েনি; redis queue-তে production-এ পড়ত।

### Idempotency — কী দিয়ে নিশ্চিত

কোনো নতুন ব্যবস্থা নয়, Phase 1-এর unique index:

```
assignments_identity_unique
  (company_id, scope_type, scope_id, assignable_type, assignable_id, effective_date)
```

`assignOrGet()` আগে identity খোঁজে (lockForUpdate), না পেলে লেখে, আর `UniqueConstraintViolationException` ধরে winner পড়ে ফেরত দেয়। তাই — event দুবার · backfill + event একসাথে · queue retry · manual assignment আগে থেকে থাকা — সব কটাই একই row-তে মেশে। `wasRecentlyCreated` বলে দেয় created না existing, সেটাই observability-র ভিত্তি।

### AutoAssignmentService ইচ্ছাকৃতভাবে পাতলা

Eligibility, policy বাছাই, effective_date, subtype — কিছুই এখানে হিসাব হয় না, সব resolver-এর। এখানে শুধু **লেখা**। একটা policy overlap-এ আটকালে বাকিগুলো তবু বসে (per-policy try/catch), নাহলে একটা conflict পুরো entitlement আটকে দিত।

**Additive only** — eligibility হারালে পুরনো assignment অক্ষত থাকে (test আছে)। End করা balance/ledger-সহ সিদ্ধান্ত, তাই সেটা explicit HR action-ই থাকল।

### Evaluation date

`now()` — "এখন কী সত্য"। Joining date নয়, কারণ joining-এর দিন সবাই on_probation, তাতে confirmed-দের group কোনোদিনই match করত না। Probation-এর জন্য **rule নয়, `effective_date_strategy`** ব্যবহার করতে হবে — সেটা row এখনই লেখে কিন্তু তারিখ পরে দেয়।

### Backfill

`BatchProgressStore` **extract** করা হয়েছে, `BulkAssignmentService` সেটাই ব্যবহার করছে এখন — দ্বিতীয় bulk framework বানাইনি। `BulkAssignmentApiTest`-এর ৯টা test refactor-এর পরেও সবুজ।

`preview` → `{eligible, already_assigned, new, conflicts, skipped, sample}`, কিছু লেখে না। `apply` → ১০০ employee-র chunk প্রতি একটা job। **হাজার employee ঘিরে একটাও বড় transaction নেই**; প্রতিটা chunk আলাদা করে retry-safe। Progress chunk-প্রতি একবার লেখা হয় (PR #198-এ employee-প্রতি লেখার যে সমস্যা ধরা পড়েছিল, সেটা এড়িয়ে)।

Resolver `scoped` binding — listener আর backfill এক worker-এ একই instance ভাগ করে, তাই `forgetCachedGroups()` একবার ডাকলেই দুজনের কাছেই নতুন group দেখা যায়। Listener প্রতি event-এ, backfill job প্রতি chunk-এ ডাকে।

### ⚠ দুটো পরিচিত ফাঁক (নতুন নয়, এখন লিখিত)

**১. তারিখ পেরোলে কেউ পুনর্মূল্যায়ন করে না।** `probation_status = confirmed` rule দেওয়া group, বা **ভবিষ্যতের তারিখে করা transfer** — তারিখ এসে গেলেও কিছু ঘটে না, পরের কোনো event পর্যন্ত অপেক্ষা করে। দুটোরই test আছে (behaviour pin করা)। সারানোর উপায় একটা nightly sweep, যেটা Phase 3-এর brief-এ নেই।

**২. `probation_status`-এ দুই date-ই null = `confirmed`** — Phase 2-এর অনুমান অপরিবর্তিত, **HR confirmation এখনো বাকি**।

### Gate

`php artisan test` — **580 passed, 1 failed**। ওই একটাই পুরনো `MonthlyAttendanceApprovalServiceTest` (PR #198)। Phase 1 ও Phase 2-এর সব সবুজ।

---

## ✅ Policy Group — PHASE 4: UI (19 Aug 2026)

Backend তিন phase-এর কাজ এখন পর্দায়।

### নতুন page (৩টা)

```
/attendance/config/policy-groups            list
/attendance/config/policy-groups/new · /:id form
/attendance/config/policy-groups/:id/apply  backfill preview → confirm → progress
```

সবগুলো `attendance.policy-group-view` guard-এর ভিতরে, manage action আলাদা করে চেক হয়।

### Rule builder — JSON কেউ দেখবে না

`Attribute ▾ | Condition ▾ | Value (multi-select) | 🗑` সারি, উপরে `every rule / any rule` toggle। Attribute-এর তালিকা **backend-এর `/attributes` endpoint থেকে আসে**, frontend-এ hardcode করা নেই — registry-তে নতুন attribute যোগ করলে UI-তে আপনিই আসবে।

Reference attribute-এর মান আসে master API থেকে (`reference_table` → client-এর map)। যে attribute-এর client এখনো নেই, সেটা খালি dropdown না দেখিয়ে **স্পষ্ট warning** দেয় — খালি dropdown "ডেটা নেই" বলে ভুল বোঝায়।

Operator বদলালে মান হারায় না (single↔multi রূপান্তর হয়), কিন্তু attribute বদলালে মান মুছে যায় — পুরনো attribute-এর id নতুনটায় অর্থহীন।

### Live counter

Form-এ **"১৪২ of ২০০ active employees match these rules right now"**, ০ হলে লাল করে বলে "Nobody matches — this group would assign nothing."

এটাই সবচেয়ে দামি অংশ: কাউকে না-ধরা group save হয়ে যায়, কোনো error দেয় না, আর assignment কোনোদিন আসে না — ধরার আর কোনো উপায় নেই।

### 🔴 Probation-এর ফাঁদটা UI-তে বলা আছে

Form-এ স্থায়ী info alert:

> confirmed-দের group-এর জন্য **Probation end date** option ব্যবহার করুন, `Probation status is Confirmed` **rule নয়** — rule নতুন joiner-কে যোগদানের দিনই বাদ দেবে, আর probation শেষ হলে কেউ ফিরে দেখে না।

Phase 3-এ ধরা পড়া ফাঁকটা এখানেই ব্যবহারকারীর সামনে।

### Apply page

Preview → ৫টা tile (`Eligible · Will be created · Already assigned · Conflicts · Not eligible`) + sample table → confirm dialog (কতগুলো assignment ও balance তৈরি হবে বলে) → progress bar + failure list।

Conflict থাকলে আলাদা warning — একই policy ভিন্ন তারিখ থেকে চলছে, ওগুলো তৈরি হবে না।

### পুরনো screen-এ যা যোগ হলো

- **Config nav-এ "Policy Groups" tab**
- **ScopePicker-এ Section** (Phase 1-এ backend scope যোগ হয়েছিল, UI-তে ছিল না)
- **Assignment list-এ Source column** — Manual/Auto/Bulk badge, group থেকে এলে group-এ link, সাথে source filter
- **Employee profile-এ "Assigned Policies" tab** — policy, scope, effective from/to, state, source, group link। `attendance.menu-view` দিয়ে gate করা (employee permission নয়, কারণ ডেটা attendance-এর)

### 🔴 দুটো contract ফাঁক ধরা পড়ল

UI লিখতে গিয়ে বেরোলো backend দুটো জিনিস দিচ্ছেই না:

১. **`AssignmentResource`-এ `policy_group_id` ছিল না** — অথচ source badge-এর "from policy group" link ওটার উপরেই দাঁড়ানো। Browser-এ নীরবে কাজ না করত।
২. **`IndexAssignmentRequest`-এ `source` filter whitelist করা ছিল না** — repository support করত (Phase 1), কিন্তু `validated()` param-টা ফেলে দিত।

দুটোই সারানো, আর **regression test যোগ করা** (`the_assignment_api_exposes_the_provenance_the_ui_reads`) যাতে resource থেকে field ফেলে দিলে test লাল হয়।

### Gate

- `tsc -b` — **আমার একটাও error নেই**। ১৩টা error বাকি, সবই `monthly-approval` ও `monthlyAttendanceApi`-তে — PR #198-এর সেই পুরনো ঋণ (উপরে লেখা)।
- `npm run build` — style-boundary check-এ আটকায় `JobStatusPanel.tsx`-এ, যেটা **dead code** আর `9.4-FE`-র ঋণ #৪ হিসেবে আগে থেকেই লেখা। ফাইলটা আমি ছুঁইনি।
- `php artisan test` — **581 passed, 1 failed** (সেই পুরনো `MonthlyAttendanceApprovalServiceTest`)।

⚠ অর্থাৎ Phase 4-এর কোড সবুজ, কিন্তু **repo-র build এখনো লাল পুরনো কারণে** — merge-এর আগে ওই ১৫টা tsc error আর JobStatusPanel সারানো দরকার, নাহলে পঞ্চমবার লাল build merge হবে।

---

## ✅ UI Design Pass — Chrome, Density & Feedback (20 Aug 2026)

Card নয়, points অপরিবর্তিত। branch: `design-change-based-on-gemini`। **62 ফাইল · +226 / −278।**

Attendance/Payroll-এর কোনো business logic ছোঁয়া হয়নি — পুরোটাই shared chrome আর presentation। তবু এখানে লিখে রাখা হলো, কারণ `PageHeader` আর `DataTable` **অ্যাটেনডেন্স ও পেরোলের প্রায় প্রতিটা স্ক্রিনে** বসে, তাই যে কেউ ঐ পেজগুলোর screenshot মেলাতে গেলে এই বদলগুলো জানা দরকার।

### ১. Breadcrumb বাদ

`AppLayout`-এর `<Breadcrumb />` আর `Breadcrumb.tsx` (৫৭ লাইন) দুটোই গেছে — অন্য কোনো consumer ছিল না।

⚠ ওটা `mb-4` বহন করত। কোনো পেজ ঐ ফাঁকটার ওপর ভর করে থাকলে এখন ~1rem টাইট বসবে।

### ২. PageHeader-এর subheadline বাদ

`subtitle` prop + তার `<p>` render বাদ, সাথে **৫২টা call site**।

`ActionList.tsx`-এর `ActionSection`-এরও নিজের `subtitle` prop আছে (`text-xs`, section caption — page subheadline নয়)। ওর ৩টা usage **ইচ্ছাকৃতভাবে রাখা হয়েছে**; শুধু ঐ ফাইলের PageHeader-এরটা গেছে। ভবিষ্যতে ঢালাও grep করলে এই পার্থক্যটা মাথায় রাখতে হবে।

subtitle ছাড়া অকেজো হয়ে পড়া ৩টা local-ও সরানো হয়েছে — `EmployeeProfilePage`-এর `subtitle`, `CorrectionApprovalDetailPage`-এর `employeeName`, আর `OtpLogPage`-এর `lastUpdated` (এর সাথে react-query `queryFn`-এর ভেতরের অনাথ `setLastUpdated(new Date())` side-effect-টাও)।

⚠ `TaxSlabFormPage` আর `SalaryStructureDetailPage`-এর title এখন একাই বসে, subtitle-টাই বেশি কাজ করত।

### ৩. PageHeader-এর উচ্চতা কমানো

| | আগে | এখন |
| --- | --- | --- |
| padding / margin | `pb-5` · `mb-6` | `pb-3` · `mb-4` |
| icon chip | `size-9` `rounded-xl` | `size-8` `rounded-lg` |
| glyph | `size-4.5` | `size-4` |
| alignment | `items-start` | `items-center` |

`items-start` কেবল subtitle-ওয়ালা দুই-লাইন হেডারের জন্য ঠিক ছিল। title `text-xl`-এই আছে — `text-lg`-এর line-height একই (28px), তাই ছোট করলে hierarchy যেত, উচ্চতা যেত না।

### ৪. Header-এর অ্যাকশন বাটন `md` → `sm`

`h-10 px-4 text-sm` → `h-8 px-3 text-xs`। **৫৪ জায়গায়** — ৩৫টা `<Button size="sm">`, ১৯টা `buttonStyles(variant, 'sm')`। `AttendanceJobsPage`-এর দুটোতে আগেই `sm` ছিল।

`AttendanceExportButton`-এর ভেতরের `Dropdown`-ও `sm`, নইলে Records পেজে Export বাটনটা একা বড় থাকত।

কোডমডটা `actions` slot-এ scope করা, তাই **header-এর বাইরের বাটন অপরিবর্তিত** — EmptyState-এর CTA, drawer, page body সব `md`-তেই।

সব মিলিয়ে হেডলাইন বার **~82px → ~60px**।

### ৫. Table density — row + font

`DataTable` (৬৫টা পেজ এখান থেকে পায়):

| | আগে | এখন |
| --- | --- | --- |
| body cell | `py-2.5 text-sm` (14px) | `py-1.5 text-xs` (12px) |
| head cell | `py-2.5` | `py-2` |
| টেক্সট row | 40px | **28px** |
| action row | 48px | **40px** |

action row ৪০-এর নিচে নামে না — ওখানে `size-7` (28px) আইকন-বাটনই height ঠিক করে, padding নয়।

হাতে-লেখা যে ৩টা টেবিল `DataTable`-এর `px-4 py-2.5` হুবহু কপি করেছিল, সেগুলোও সাথে বদলানো হয়েছে (নইলে অ্যাপে দুই রকম density থাকত): `ComponentBuilder` (`HEAD_CELL`/`BODY_CELL`), `OrganizationTab` (৭ `<td>`), `UnassignedEmployeesReport`।

⚠ শেষেরটায় **`<td>` নয়, `<table>`-এর ক্লাস** বদলাতে হয়েছে — ওর দুটো cell-এ size class নেই, `<table>`-এর `text-sm` থেকে inherit করত। শুধু `<td>` ধরে বদলালে ওরা 14px-এ থেকে যেত। `<thead>` নিজে `text-xs` দেয়, তাই header অক্ষত।

যাদের নিজস্ব scale আছে সেগুলো ছোঁয়া হয়নি: `TaxPreview` (`px-3 py-2`), `BracketEditor` (`px-2 py-1.5`), `AccrualImpactModal` (`px-3.5`)।

### ৬. Employee list — select bar আর টেবিল ঠেলে না

আগে row টিক করলেই একটা `<Alert>` render হতো আর **টেবিল নিচে নেমে যেত**। এখন search form আর bulk-selection controls একই `Card`-এ, আর card সবসময় থাকে:

```
[🔍 Search…] [Search]        3 of 120 selected  [Generate ID Cards] [Clear]
```

card-এর উচ্চতা ঠিক করে search input (~38px), ডানের বাটন `sm` (32px) — তাই selection এলে-গেলে কিছু নড়ে না।

count এখন `N of M`, `M` আসে `data.meta.total` থেকে। এটা দরকারি: selection **ইচ্ছাকৃতভাবে page-এর বাইরেও** থাকে, আগের লেখা ছিল "across all pages"।

`aria-live="polite"` হাতে দিতে হয়েছে — Alert-এ ওটা এমনিতেই ছিল।

⚠ মোবাইলে ডান পাশ wrap করে, তখন সামান্য shift হয়। ডেস্কটপে নয়।

### ৭. Company switch — overlay + toast

Switch করলে full-screen blocking overlay ("Switching company / Loading X's data…"), শেষে `toast.success("Switched to X")`।

`switchCompany` **এমনিতেই** পুরো কাজ শেষ হলে resolve করে — header বদলায়, `resetQueries()` দিয়ে পুরনো tenant-এর cache মোছে, mounted query refetch হওয়া পর্যন্ত অপেক্ষা করে, তারপর `me()`। তাই overlay কোনো আন্দাজি timer নয়।

`MIN_OVERLAY_MS = 1500` একটা **floor, যোগ নয়** — `Promise.all([switchCompany(...), delay(...)])`। ডেটায় ২s লাগলে ২s, ৩০০ms লাগলে ১.৫s। উদ্দেশ্য ঝিলিক ঠেকানো আর নিচের পেজ remount হয়ে প্রথম fetch করার সময়টুকু ঢাকা।

⚠ company-র নাম `await`-এর **আগে** ধরা হয়, কারণ `switchCompany` শেষে `me()` পুরো list বদলে দেয় — পরে `find()` করলে সরে-যাওয়া ডেটায় খোঁজা হতো। তাই `handleSelect` এখন পুরো `company` object নেয়।

Blocking রাখা ইচ্ছাকৃত: ঐ সময় cache খালি আর পেজ remount হচ্ছে, সাথে মাঝপথে দ্বিতীয় switch শুরু হওয়াও আটকায়।

### Gate

| চেক | ফল |
| --- | --- |
| `tsc -b --force` | **১৫ error — baseline-এর সাথে হুবহু এক**, একটাও নতুন নয় |
| `oxlint src` | 34 warnings / 8 errors — সংখ্যা ও ফাইল লিস্ট অপরিবর্তিত |
| `vite build` | ✅ সবুজ |
| `check-style-boundary.sh` | 🔴 **ফেল — তবে আগে থেকেই** |

দুটো লাল **এই পাসের নয়**, দুটোই যাচাই করা:

- ঐ ১৫টা error `6.1-FE` (PR #198)-এর, `monthly-approval`-এ। উপরে এই ফাইলেই লেখা আছে।
- style boundary ফেল করে `JobStatusPanel.tsx:87`-এর hard-coded `text-[10px]`-এ — এই পাসে ঐ ফাইল ছোঁয়াই হয়নি। কাজ stash করে clean HEAD-এ চালিয়ে মিলিয়ে দেখা হয়েছে: একই ফেল, exit 1। মনে রাখতে হবে **`npm run build` ওখানেই আটকায়, `tsc`-এ পৌঁছানোর আগেই**।

### বাকি / follow-up

- `JobStatusPanel.tsx`-এর `text-[10px]` সরালে `npm run build` আবার চলবে (`9.4-FE`-র dead-code ঋণের সাথেই ধরা যায়)।
- 28px row + 12px টেক্সট বেশ ঘন। লম্বা লেখার কলামে (যেমন correction-এর "Issue Reason") ঠাসা লাগলে `py-2` (32px) করলেই হয়, font 12 রেখেই।
- `CompanySwitcher`-এর tab-এর ভেতরের ছোট spinner-টা এখন overlay-র পেছনে ঢাকা পড়ে, আর দুটোরই `role="status"` বলে screen reader দুইবার announce করতে পারে।
- "select করলে Alert এসে টেবিল ঠেলে" — এই প্যাটার্ন Monthly Approval-এর bulk approve-এও থাকতে পারে, দেখা হয়নি।

### ⚠ এই branch-এ আগে থেকেই ছিল (এই পাসের কাজ নয়)

`CompanySwitcher`-এর active-state restyle (emerald + ping dot + "Active" badge) আর `Header`-এ company-name/"Enterprise Core Platform" block বাদ — দুটোই কাজ শুরুর আগে uncommitted অবস্থায় ছিল, অক্ষত রাখা হয়েছে।

---

## ✅ Wave 5 বন্ধ, Wave 6 চলছে (23 Aug 2026)

শেষ যাচাই ছিল 20 Aug (PR #207)। তারপর ১২টা PR merge হয়েছে, তার মধ্যে **তিনটে card**; বাকি সব Employee/Platform-এর কাজ ও bug fix। **একই দিনে দ্বিতীয় পাস:** `8.0-BE` Attendance Snapshot কোডবেসে আছে ও done মার্ক। **তৃতীয় পাস:** `8.0-FE` Snapshot UI কোডবেসে আছে ও done মার্ক। **২৪ Aug 2026 কোডবেস recheck:** Seq Wave 6 ফুটার **13/46 → 15/44** (`8.0-FE` বাদ পড়েছিল); freeze list `employee_id`; diagnostic copy + contrast কোডে আছে; `Payslip`/`payroll_payslips` নেই।

### যেগুলো done হলো

| Card | Pts | প্রমাণ (কোডে যাচাই করা) |
|---|---|---|
| **`6.2-FE`** Freeze UI | 2 | `payroll/pages/monthly-freeze/MonthlyFreezePage.tsx` — commit `1a93334f` "attendance freez", পরে `36d66df6` "freeze account list not showing" fix। nav-এ Payroll › Monthly Freeze, gate `payroll.month-freeze` |
| **`8.1-BE`** Payroll Run Creation | 5 | commit `6fcd9218` "Payroll Run Creation — Backend" — `PayrollRun.php` · `PayrollRunService.php` · migration `2026_08_19_112100_create_payroll_runs_table.php` · `payroll-runs` index/store/readiness/show। PR #215 follow-up fix |
| **`8.1-FE`** Payroll Run UI | 3 | commit `2010f597` (PR #209) — `PayrollRunListPage` · `CreatePage` · `DetailPage` + `payrollRunApi.ts`; nav Payroll › Payroll Runs |
| **`8.0-BE`** Attendance Snapshot | 5 | `attendance_snapshots` migration · `AttendanceSnapshotService` · build/list/show/divergence routes · `getPayrollAttendanceForRun()` · `AttendanceSnapshotTest` (১৭ test) |
| **`8.0-FE`** Snapshot UI | 2 | `attendanceSnapshotApi.ts` · list/detail · run Quick Link · freeze `month`/`year`/`employee_id` query · diagnostic copy + snapshot/live contrast · **কোনো আলাদা nav item নেই** |

**Wave 5 বন্ধ (31/31)** — `6.2-FE` ছিল শেষ card। **Wave 6: 23/59** (`8.1-BE` · `8.1-FE` · `8.0-BE` · `8.0-FE` · `8.2a-BE`)।
**মোট: 192 pts / 48 cards done · বাকি 36 pts / 6 cards** (সবটাই Wave 6)।

### নতুন unlock

**`8.3a-BE`** Run Approval (5) — `Start after` = `8.2b-BE`। **critical path।** `paid → settled` stamp। **`8.2-FE` done.**

### ⚠ দুটো gate — অবনতি বড়

| Gate | 20 Aug | 23 Aug |
|---|---|---|
| `php artisan test` | 1 failed / 452 passed | 🔴 **36 failed / 609 passed** |
| `tsc -b --force` | ১৫ error | 🔴 **৩৫ error** |

দুটো সংখ্যাই এই পাসে সত্যিই মেপে নেওয়া, অনুমান নয়।

**ভাঙা test — ১০টা class:** `CorrectionRequestApiTest` 13 (PR #212, ValidationException) · `PayrollRunControllerTest` 7 · `EmployeePersonalInfoIdAutoGeneratedTest` 4 (PR #213) · `EmployeePersonalInfoUpdateTest` 3 (PR #213) · `AttendancePayrollApprovalIntegrationTest` 2 · `PayrollRunStatusTest` 2 · `MonthlyAttendanceApprovalServiceTest` 2 (পুরনো `6.1-BE` ঋণ, আগে ১ ছিল) · `SalaryAdvanceApiTest` · `EmployeeSalaryPaymentAccountsTest` · `AttendanceCorrectionExecutorTest` ১ করে।

**এর মধ্যে ৯টা `8.1-BE`-র নিজের দুটো test ফাইল থেকেই আসছে, আর দুটোরই কারণ card-টার সাথে merge হওয়া কোড নয় — পরিবেশ:**

- `PayrollRunControllerTest` (৭) — `Company::factory()` ডাকে, কিন্তু `App\Core\Companies\Models\Company`-তে `HasFactory` trait নেই আর `database/factories/`-এ কেবল `UserFactory.php` আছে। **ফাইলটা কোনোদিন একবারও চলেনি।**
- `PayrollRunStatusTest` (২) — `@dataProvider` docblock annotation ব্যবহার করে, অথচ `composer.json`-এ **`phpunit/phpunit: ^12.5.12`**; PHPUnit 12-তে doc-comment metadata সমর্থন সরানো হয়েছে, তাই provider সংযুক্তই হয় না আর `ArgumentCountError` আসে। `#[DataProvider]` attribute-এ নিলেই যাবে।

অর্থাৎ **`8.1-BE` কোড হিসেবে আছে কিন্তু কার্যত অপরীক্ষিত** — তাই উপরে `done ⚠` লেখা।

**tsc-র ৩৫টার ভাগ:** `monthly-approval` ১২ (পুরনো `6.1-FE` ঋণ) · **`8.1-FE` ১০** (`PayrollRunCreatePage` 4 · `List` 2 · `Detail` 2 · `payrollRunApi.ts` 2) · `UserFormDrawer.tsx` 4 (PR #210 — `Property 'id' does not exist on type 'Company'`) · `DocumentsTab` 3 · `ImportUserModal` 3 · বাকি ছড়ানো।

> **পঞ্চমবার একটা FE card লাল build নিয়ে merge হলো** — `5.3-FE` ১৬ · `5.5-FE` ৬ · `6.1-FE` ১৫ · এখন `8.1-FE` ১০। প্যাটার্নটা আর ব্যতিক্রম নয়, নিয়ম হয়ে গেছে।

### সবচেয়ে সস্তা মেরামত

১. একটা `CompanyFactory` + `Company`-তে `HasFactory` → **৭টা test একসাথে সবুজ**
২. `PayrollRunStatusTest`-এ `#[DataProvider]` → আরও **২টা**

দুটো মিলে ৩৬ থেকে ২৭-এ নামে, আর `8.1-BE` প্রথমবারের মতো সত্যিই যাচাই হয়।

### Card নয় (points অপরিবর্তিত)

PR #208 asset/timeline tab position · #210 user create-এ company restrict · #211 profile picture live preview · #212 attendance correction fix · #213 employee ID uniqueness + DOB validation · #214 delete log · #216 bank dropdown আলাদা করা · #217 BD degrees dropdown · #218 nav-এ selected employee name · #219 duplicate reporting manager validation।

---

## ✅ `8.0-BE` Attendance Snapshot for Payroll (23 Aug 2026)

Wave 6-এর দ্বিতীয় card। উদ্দেশ্য একটাই — **payroll run পুনরুৎপাদনযোগ্য রাখা**। run তৈরির মুহূর্তে প্রতিটি employee-র frozen monthly attendance কপি হয়ে যায়; পরে correction এলে সেই কপি বদলায় না, তাই ইতিমধ্যে হিসাব হওয়া payslip-এর ভিত্তি নীরবে সরে যেতে পারে না।

```
Frozen Monthly Attendance → attendance_snapshots → Payslip Generation
```

### Table

`attendance_snapshots` — ১৬ কলাম, **`created_at`/`updated_at` নেই** (একবার লেখা হয়, `snapshot_taken_at`-ই একমাত্র অর্থবহ সময়)। `unique(payroll_run_id, employee_id)` + `index(company_id, month, year)`। FK constraint নয়, `unsignedBigInteger` + index — `payroll_runs`-এর কনভেনশন।

### Endpoints

| Method | Path | Permission |
|---|---|---|
| POST | `payroll-runs/{id}/build-snapshots` | `payroll.run-create` |
| GET | `attendance-snapshots` (filter: run/employee/month/year) | `run-create\|run-override-readiness` |
| GET | `attendance-snapshots/{id}` | ঐ |
| GET | `attendance-snapshots/{id}/divergence` | ঐ |

**Update/delete endpoint নেই, ইচ্ছাকৃত।** Immutability model event-এ বসানো — `updating`/`deleting` throw করে।

### নিয়ম

- run **draft** না হলে `409`
- frozen নয় এমন employee থাকলে `409`, `errors.employees`-এ তাদের নাম/employee_no/কারণ
- approval row-ই নেই এমন হলে `404`, একইভাবে চিহ্নিত
- **idempotent** — আগে snapshot হয়ে যাওয়া employee skip হয়, কখনো overwrite নয়; DB unique constraint শেষ রক্ষাকবচ
- off-cycle/reprocessed run আলাদা `payroll_run_id` পায়, তাই নতুন সেট — আগেরটা অক্ষত

### ⚠ Payslip integration করা যায়নি

**কোডবেসে payslip generation নেই** — Payslip model/table/service/controller কিছুই না (`8.2a-BE` এখনো pending)। তাই "payslip generator-কে snapshot পড়াও" আর "payslip দুবার generate করে মিলিয়ে দেখো" আক্ষরিক করা অসম্ভব ছিল। বদলে যা দেওয়া হয়েছে, যাতে `8.2a-BE` সরাসরি বসাতে পারে:

- `AttendanceSnapshotServiceInterface::getPayrollAttendanceForRun()` — payroll calculation-এর **একমাত্র** attendance উৎস
- `AttendanceSnapshot::PAYROLL_FIELDS` / `SOURCE_COLUMNS` — payslip pipeline আর divergence একই সেট দেখে
- **guard test** — `DB::listen` দিয়ে প্রমাণ যে ওই path-এ `attendance_records`/`monthly_attendance_approvals`-এর কোনো query যায় না
- **reproducibility test** — snapshot নেওয়ার পর live monthly attendance বদলে দিয়ে দেখানো যে payroll attendance অবিকল একই থাকে

### দুটো বাস্তবতা যা নকশাকে আকার দিয়েছে

**১. run-এ কোন employee আছে তা কোথাও সংরক্ষিত নেই।** `createRun` কেবল `total_employees` গণনা রাখে, id ফেলে দেয়। তাই সেট নির্ধারণে বিদ্যমান `PayrollRunService::checkReadiness()` পুনঃব্যবহার করা হয়েছে — সমান্তরাল সংজ্ঞা বানানো হয়নি। **পরিণতি:** run তৈরির পর কেউ active/inactive হলে সেট বদলাতে পারে। membership টেবিল ছাড়া এড়ানো যায় না — `8.2a-BE`-তে ভাবার মতো।

**২. `phpunit.xml`-এ `memory_limit=512M` যোগ করতে হয়েছে।** suite আগে থেকেই PHP-র ১২৮M default-এর গা ঘেঁষে চলছিল, তাই *যেকোনো* নতুন test class যোগ করলেই run fatal error-এ ভাঙত (`BangladeshGeoSeeder`-এ), test failure হিসেবে নয়। নিজের test class থেকে seeding সম্পূর্ণ বাদ দিয়েও (developer-bypass দিয়ে auth, ১.৩s-এ চলে) ঠেকানো যায়নি — ফাইল সরিয়ে যাচাই করা: ছাড়া suite শেষ হয়, সহ ভাঙে। **এটা এই card-এর ঋণ নয়, আগে থেকেই থাকা ভঙ্গুরতা** — কিন্তু এখন থেকে যে-ই test যোগ করুক, এটা না থাকলে আটকাত।

### Tests — ১৭/১৭ সবুজ

creation (মান হুবহু, সব FK/period/`snapshot_taken_at`) · freeze `409` + employee চিহ্নিত · missing `404` + চিহ্নিত · non-draft `409` · idempotency (duplicate নেই, মান অপরিবর্তিত) · DB unique constraint · off-cycle আলাদা সেট · immutability (model throw + PUT/PATCH/DELETE → 405) · reproducibility · no-live-query guard · divergence (নেই/আছে, read-only) · list+filter · tenant isolation · permission 403।

**Suite: 36 failed / 626 passed** — baseline ছিল 36 failed / 609 passed, ফেল-তালিকা হুবহু এক। কোনো regression নেই।

### একটা সীমা

Immutability model event-এ, আর কোনো write route নেই। তবে কেউ ইচ্ছে করে query builder দিয়ে (`DB::table('attendance_snapshots')->update(...)`) লিখলে আটকাবে না — Eloquent guard-এর সাধারণ সীমা। DB trigger লাগালে বন্ধ হতো, কিন্তু প্রকল্পে কোথাও trigger ব্যবহার হয় না।

### API collection

`api collection/Payroll/Attendance Snapshots/` — folder + build/list/show/divergence, প্রতিটায় request/response উদাহরণ। পাঁচটা YAML-ই parse করে যাচাই করা।

---

## Production speed / Docker ops (3 Sep 2026) — card নয়

Attendance/Payroll spine অপরিবর্তিত। Infra:

- Root module `.md`/`.pdf` → [`docs/`](../README.md) (এই ফাইল এখন `docs/attendance-payroll/`)
- `docker-compose.prod.yml` + root `Makefile` (`make up` / `make prod-up`)
- Redis cache + session (compose override); PHP opcache + `phpredis`; FPM workers env-driven
- Horizon: `default` + `long`/`id-cards` supervisors; long job timeout 3700s; heavy jobs `onQueue('long')`
- Local compose আর Vite/phpMyAdmin-কে production-এ তোলে না

Server: `make prod-up` → `make prod-migrate` → `make prod-cache`. `git pull`-এর পর `make prod-restart`.

### Local + prod smoke test (3 Sep 2026)

- **Local `make up`:** PASS — `/up` 200, frontend 3011 200, Horizon running, redis/opcache/FPM(8) OK.
- **Prod backend (frontend skipped):** PASS — `APP_ENV=production`, FPM 24, Horizon 10+6, config cached, redis OK, `:8010` 200. `backend-edge :80` not reachable on Mac Docker Desktop (Linux server only).
- **Full `make prod-up`:** FAIL — frontend `tsc -b` has pre-existing TypeScript errors (monthly approval, payroll runs, employee tabs, etc.). Style-boundary false positive on `text-text-muted` fixed in `frontend/scripts/check-style-boundary.sh`.

### Frontend TS fix (3 Sep 2026)

- `tsc -b` ৩৫ error → ০. Shared `PaginatedResponse`/`PaginationParams`; UI props matched to Drawer/PageHeader/StatusBadge/Button; import/Experience/UserForm types cleaned.
- `make prod-up` full stack PASS (frontend build + API 200 + FE 3010 200 + Horizon).

### Local CORS (3 Sep 2026)

Local Vite is `:3011`. `CORS_ALLOWED_ORIGINS` now includes 3010/3011 + localhost and 127.0.0.1. Local compose waits for php-fpm health before nginx so rebuild 502s are not shown as CORS.

### Real client IP / punch whitelist (3 Sep 2026)

- Prod path: `backend-edge :80` → XFF → trusted proxies → `$request->ip()` on punch; `make prod-up` forces `BACKEND_BIND=127.0.0.1`; prod `VITE_API_URL` default is edge `/api/v1` (not `:8010`).
- IP whitelist accepts CIDR (`IpOrCidr`); UI **Use my current IP** via `GET /configuration/ip-whitelists/client-ip`.
- Punch ignores client-supplied `ip_address` (server-only).


## Cross-module note (14 Sep 2026) — PMS Phase 1 schema lock

Not an attendance/payroll build step. PMS added shared task **PMS-SCHEMA-1** locking all Phase 1 table schemas before lane migrations. See [phase1-database-schema.md](../project-management/wbs/phase1-database-schema.md).

## Cross-module note (16 Sep 2026) — Commissioning C3 (approve/post/ledger)

Not an attendance/payroll build step. Commissioning epic phase **C3** shipped: salary/fund allocation, BD-pool redistribution, run approve→post with immutable snapshots + ledger entries (stub Project context). See [COMMISSIONING_IMPLEMENTATION_PLAN.md](../project-management/commission/COMMISSIONING_IMPLEMENTATION_PLAN.md). Payroll still consumes posted salary lines later via events (C4+).

## Cross-module note (16 Sep 2026) — Commissioning C4 (fund unlock + exit settlement)

Not an attendance/payroll build step. Commissioning **C4** shipped employee fund balances, time-based **fund unlock** schedules (unlocked / still locked), and exit settlement. See [COMMISSIONING_IMPLEMENTATION_PLAN.md](../project-management/commission/COMMISSIONING_IMPLEMENTATION_PLAN.md).

## Cross-module note (16 Sep 2026) — Commissioning C5 (real Project adapter + reports)

Not an attendance/payroll build step. Commissioning **C5** adds `RealProjectContext` (reads Project tables), `PaymentCleared` inbox/listener, reports APIs, and outbound `CommissionPostedToSalary` / `CommissionFundUpdated` for Payroll consumers. Default driver remains stub until Project emits payments.

## Cross-module note (16 Sep 2026) — Commissioning post-C5 (tenant fix + Payroll listener skeleton)

Commissioning run calculate/post now uses **tenant `company_id`** for Project context reads (not hardcoded fixture company). Real-mode smoke test added (`RealModeRunSmokeTest`). Payroll **`HandleCommissionPostedToSalary`** listener registered (skeleton: logs posted run; full variable earning lines TBD).

## Cross-module note (16 Sep 2026) — Commission immediate payout mode

Rules can set immediate cash as **pay with salary** or **pay separately**. Posted `with_salary` lines emit `CommissionPostedToSalary`; `separate` lines stay in Commissioning payout queue (`/payouts/separate`) until marked paid. Fund unlock path unchanged.

## Cross-module note (16 Sep 2026) — Commissioning rule form UX

Not an attendance/payroll build step. Commission rule form copy made simpler for non-technical users (examples + field hints). User-facing labels avoid “pool” wording (e.g. BD/PD/Team **commission share**). See commissioning rules page + `CalculationBase` / related enum labels.

## Cross-module note (16 Sep 2026) — Commissioning rule drawer UX

Not an attendance/payroll build step. Rule drawer widened (`2xl`); form sections cleaned; scope target uses **named searchable dropdowns** via `GET /commissioning/rules/scope-options` (no raw IDs).

## Cross-module note (16 Sep 2026) — Commissioning advanced rule policy UI

Not an attendance/payroll build step. Rule form now configures **designation**, **eligibility**, **redistribution**, **fund unlock**, and **termination** (backend engines already existed; FE gap closed).

## Cross-module note (16 Sep 2026) — Commissioning rule form bilingual toggle

Not an attendance/payroll build step. Commission rule drawer includes **English / বাংলা** toggle; selected locale drives all form labels, hints, section copy, and dropdown option labels (`commissionRuleFormCopy.ts`; preference stored in `localStorage`).
