# PRD: Calendar Module — Failed Test Cases Resolution

## Context

The Calendar module in HubWallet CRM (FullCalendar v6.1.10 + Laravel 11 backend) has 28 failing test cases across six functional areas: time/date validation, filter functionality, event creation integrity, reminder system, UI/display issues, and reschedule behavior. This PRD captures every failing case mapped to its root cause and the exact code change required.

---

## Epic 1 — Time & Date Validation

### Failing Cases
CAL-047, CAL-048, CAL-049, CAL-050, CAL_EVT_027, CAL_EVT_028, CAL_EVT_045, CAL_EVT_050

### Root Cause
`lead-event-create.js` has end > start validation, but the main calendar event form (`event_form.blade.php`) and the server-side `EventController@store` have **no** time comparison validation. There is also no check for past start times, same start/end, or consistent 12/24-hour display between form and view.

### Stories

**Story 1.1 — End time must be after start time (client + server)**
- **Files:** `public/assets/js/lead-event-listings.js`, `event_form.blade.php` (embedded script), `app/Http/Controllers/Calendar/EventController.php`
- **Change:** Add `end_time > start_time` check in the `$(document).on('change', 'input[name="start_time"]')` block inside `event_form.blade.php` and mirror it in the `storeAjax()` / `store()` server validation (`$request->validate([])` block). Return a 422 with `"End time must be after start time"` message.
- **Covers:** CAL-047, CAL-049

**Story 1.2 — Same start and end time must be blocked**
- **Files:** Same as Story 1.1
- **Change:** Extend the comparison to `end_time !== start_time` (strict). Show validation message: `"End time must be different from start time"`.
- **Covers:** CAL-048, CAL-050, CAL_EVT_027

**Story 1.3 — Past start time warning/block**
- **Files:** `event_form.blade.php` (embedded JS), `EventController@store`
- **Change:** On form submit, compare `start_time` to `now()`. If in the past, show a SweetAlert confirmation warning: `"Start time is in the past. Do you want to continue?"` (not a hard block, per business rule). Server does not need to block.
- **Covers:** CAL_EVT_028

**Story 1.4 — Invalid date format validation**
- **Files:** `event_form.blade.php`, `EventController@store`
- **Change:** Add Moment.js `isValid()` check on both `start_time` and `end_time` before submit. Server: add `date_format:Y-m-d H:i` rule in Laravel validation.
- **Covers:** CAL_EVT_045

**Story 1.5 — Consistent 12/24-hour format**
- **Files:** `resources/views/admin/components/date-time-picker.blade.php`, `event_view.blade.php`
- **Change:** Audit the `format` option passed to the Bootstrap Material DateTime Picker. Standardize to 24-hour (`HH:mm`) across all calendar date-time inputs and view templates. Ensure `event_view.blade.php` formats times with the same Moment.js pattern as the picker.
- **Covers:** CAL_EVT_050

---

## Epic 2 — Filter Functionality

### Failing Cases
CAL-022, CAL-026, CAL-028, CAL-067, CAL_EVT_098, SMK-CAL-06

### Root Cause
1. `search_status_state` filter joins `leads` table but may be silently skipped when `lead_id` is null — events without a linked lead are excluded from results.
2. When both event-type checkboxes are unchecked, `search_event_type` is sent as `null` and the backend returns all events instead of zero.
3. Right-panel user checkbox state is not reliably syncing to `search_user` array before `calendar.refetchEvents()` fires.
4. After creating an event, the calendar refetches but does not carry current filter state into the refetch parameters.

### Stories

**Story 2.1 — Status State filter includes events with no linked lead**
- **Files:** `app/Models/Calendar/Event.php` — `getListing()` method
- **Change:** In the `search_status_state` conditional, change the join to a `leftJoin` and add `whereNull OR whereMasterCategoryId` so events without a `lead_id` are not silently dropped.
- **Covers:** CAL-022, SMK-CAL-06

**Story 2.2 — Both event-type checkboxes unchecked → show zero events**
- **Files:** `app/Models/Calendar/Event.php` — `getListing()`, `public/assets/js/pages/calendar.init.js`
- **Change (JS):** When `eventTypes` array is empty (both unchecked), pass a sentinel value (e.g., `search_event_type: []`) rather than `null`. **Change (Backend):** In `getListing()`, detect empty array and add `whereRaw('1=0')` to return no rows.
- **Covers:** CAL-026

**Story 2.3 — User panel checkbox correctly filters calendar and list view**
- **Files:** `public/assets/js/pages/calendar.init.js`, `resources/views/admin/calendar/index.blade.php`
- **Change:** Ensure the `getSelectedUsers()` helper reads checkbox state at the moment `refetchEvents()` is called — not from a cached variable. Verify the `search_user` param sent to `/calendar/events` is an array of integers matching `set_for_user_id` values.
- **Covers:** CAL-028, CAL_EVT_098

**Story 2.4 — New event appears only under matching filters after creation**
- **Files:** `public/assets/js/lead-event-create.js`, `public/assets/js/pages/calendar.init.js`
- **Change:** After successful event creation AJAX response, instead of a full page reload (`window.location.reload()`), call `calendar.refetchEvents()` + `updateListView()` so the current filter state is preserved. The new event will naturally appear only if it matches.
- **Covers:** CAL-067, CAL_EVT_038

---

## Epic 3 — Event Creation Integrity

### Failing Cases
CAL-062, CAL-063, CAL-064, CAL_EVT_038 (covered above), CAL_EVT_042

### Root Cause
1. No overlap detection exists anywhere in the backend.
2. Duplicate click prevention (`disabled-pointer`) only exists in `lead-event-create.js`, not in the main calendar form submission path.
3. Required-field validation is satisfied by auto-filled defaults (user + time), so the form submits without the user consciously choosing values.

### Stories

**Story 3.1 — Prevent overlapping events for the same user**
- **Files:** `app/Http/Controllers/Calendar/EventController.php` (`store`, `storeAjax`), `app/Models/Calendar/Event.php`
- **Change:** Add a scope `scopeOverlapping($query, $userId, $startTime, $endTime, $excludeId = null)` to `Event` model. In `store()` / `storeAjax()`, call this scope before saving. If overlapping records exist, return a 422 response: `"An event already exists for this user at the selected time."`. No client-side check needed — server response surfaces via existing AJAX error handler.
- **Covers:** CAL-062, CAL-064

**Story 3.2 — Prevent duplicate submission on double-click (main calendar form)**
- **Files:** `resources/views/admin/calendar/event_form.blade.php` (embedded JS) or the form's submit button
- **Change:** Add the same button-disable pattern that `lead-event-create.js` uses to the main calendar event form submit handler. On submit: `$submitBtn.prop('disabled', true)`. Re-enable in the `complete` callback.
- **Covers:** CAL-063

**Story 3.3 — Required field defaults should not bypass validation intent**
- **Files:** `resources/views/admin/calendar/event_form.blade.php`
- **Change:** The `set_for_user_id` and `start_time` fields are auto-filled with defaults. This is correct behavior; the test case notes "default users and time are selected" as the reason validation passes. Add a visual asterisk and tooltip on these fields so users understand they are pre-filled required fields. No logic change needed — the current behavior is correct.
- **Covers:** CAL_EVT_042

---

## Epic 4 — Reminder System

### Failing Cases
CAL_EVT_029, CAL_EVT_030

### Root Cause
The `reminder_at` column exists in `calendar_events` (migration `2026_03_20_000001`) and `ProcessEventReminders` command processes it, but the **event creation/edit form has no "Remind Me At" field** — the field was never wired into the UI.

### Stories

**Story 4.1 — Add "Remind Me At" field to event form**
- **Files:** `resources/views/admin/calendar/event_form.blade.php`, `app/Http/Controllers/Calendar/EventController.php`
- **Change:** Add a datetimepicker field `reminder_at` (nullable) to the event form, below the end-time row. Label: `"Remind Me At"`. In `EventController@store` / `storeAjax`, include `reminder_at` in the mass-assignable save. On edit, pre-populate from `$event->reminder_at`.
- **Covers:** CAL_EVT_029

**Story 4.2 — Validate reminder is before event start time**
- **Files:** `event_form.blade.php` (embedded JS), `EventController@store`
- **Change (JS):** On form submit, if `reminder_at` is filled, check `reminder_at < start_time`. If not, show inline validation error: `"Reminder must be set before the event start time."` **Change (server):** Add Laravel validation rule: `'reminder_at' => 'nullable|date|before:start_time'`.
- **Covers:** CAL_EVT_030

---

## Epic 5 — UI & Display Issues

### Failing Cases
CAL-051, CAL-065, CAL-069, CAL-071, CAL-072, CAL_EVT_016

### Stories

**Story 5.1 — Description max-length enforcement and truncation in view**
- **Files:** `event_form.blade.php`, `event_view.blade.php`
- **Change (Form):** Add `maxlength="1000"` to the `description` textarea (or per business-defined limit) and show a character counter below the field. **Change (View):** In `event_view.blade.php`, wrap the description in a `<div style="max-height:150px; overflow-y:auto;">` so long text scrolls rather than overflowing the modal layout.
- **Covers:** CAL-065, CAL_EVT_016

**Story 5.2 — Tooltip content correctness on Add Event popup**
- **Files:** `event_form.blade.php`
- **Change:** Audit every `data-bs-toggle="tooltip" title="..."` attribute in the form. Ensure each tooltip accurately describes its associated field. If the Start Time helper/settings icon (CAL-051) is absent, add an info icon with tooltip: `"Set the time this event begins"`.
- **Covers:** CAL-051, CAL-069

**Story 5.3 — Top search bar functionality**
- **Files:** `resources/views/admin/calendar/index.blade.php`, `public/assets/js/pages/calendar.init.js`
- **Change:** Identify the top search input (`input[name^="search_"]` for free-text). Verify it is wired to the debounced handler in `calendar.init.js` that calls `calendar.refetchEvents()`. If disconnected, add the event binding. Ensure the backend `getListing()` applies a `where('title', 'like', '%'.$search.'%')` when a text search term is present.
- **Covers:** CAL-071

**Story 5.4 — Email and Message toolbar buttons**
- **Files:** `resources/views/admin/calendar/index.blade.php` or `index_list.blade.php`, relevant JS
- **Change:** Locate the Email and Message icon buttons in the toolbar. Wire them to their respective action endpoints (existing email/messaging controllers). If endpoints are not yet implemented, add stubs that return a `"Coming soon"` toast so the buttons are not silently broken.
- **Covers:** CAL-072

---

## Epic 6 — Reschedule Behavior

### Failing Case
CAL_EVT_106

### Root Cause
`EventController@rescheduleStore` creates a new event with `event_parent_id` pointing to the original and sets `is_rescheduled = 1` on the original. However, the calendar's `getEvents()` endpoint does not exclude events where `is_rescheduled = 1`, so both the old and new events appear on the grid simultaneously.

### Stories

**Story 6.1 — Hide original event from calendar after reschedule**
- **Files:** `app/Http/Controllers/Calendar/EventController.php` (`getEvents`), `app/Models/Calendar/Event.php` (`getListing`)
- **Change:** In `getListing()` (and the `getEvents` query), add `->where('is_rescheduled', 0)` as a default scope condition so rescheduled originals do not appear. Verify the new rescheduled event (child) does appear at the new time slot.
- **Covers:** CAL_EVT_106

---

## Critical Files

| File | Epics Affected |
|------|---------------|
| `app/Models/Calendar/Event.php` | 1, 2, 3, 6 |
| `app/Http/Controllers/Calendar/EventController.php` | 1, 2, 3, 4, 6 |
| `resources/views/admin/calendar/event_form.blade.php` | 1, 3, 4, 5 |
| `resources/views/admin/calendar/event_view.blade.php` | 1, 5 |
| `resources/views/admin/calendar/index.blade.php` | 2, 5 |
| `public/assets/js/pages/calendar.init.js` | 2, 3 |
| `public/assets/js/lead-event-create.js` | 2, 3 |
| `resources/views/admin/components/date-time-picker.blade.php` | 1 |

---

## Implementation Order

1. **Epic 1** (time validation) — foundation for correct data entry, no dependencies
2. **Epic 4** (reminders) — self-contained DB field already exists
3. **Epic 3** (creation integrity) — depends on form being corrected first
4. **Epic 2** (filters) — depends on events being stored correctly
5. **Epic 6** (reschedule) — single model query change
6. **Epic 5** (UI/display) — polish, last to avoid rework

---

## Verification

For each Epic:
1. **Epic 1:** Open Add Event modal → set end_time before start_time → submit → expect inline error. Try past date → expect SweetAlert warning. Try invalid date string → expect validation toast.
2. **Epic 2:** Uncheck both Task/Appointment → calendar shows 0 events. Uncheck a user in the right panel → their events disappear. Apply Status State filter → list updates correctly.
3. **Epic 3:** Rapidly double-click Add → only one event created. Create two events for same user at same time → second is rejected with overlap error.
4. **Epic 4:** Open Add Event → verify "Remind Me At" field appears → set reminder after start time → expect validation error → set before start time → save → reopen event → reminder_at is populated.
5. **Epic 5:** Enter 1500-char description → character count appears, maxlength enforced. Hover all tooltips → correct labels shown. Use top search bar → calendar filters in real-time.
6. **Epic 6:** Reschedule an event → original disappears from calendar → rescheduled event appears at new time slot.
