# Calls History Malaysia Time Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Emit unambiguous UTC Call History timestamps and render list/detail times in Malaysia standard time, so the observed 10:16 row displays as 18:16.

**Architecture:** Keep `call_recordings.called_at` as a UTC database wall-clock. Centralize UTC ISO serialization in the existing `CallRecordingPresenter` used by the list/detail payloads, then format the resulting instant with the Inertia `userTimezone` and `Asia/Kuala_Lumpur` fallback.

**Tech Stack:** PHP 8.3, Laravel 13, Carbon, Inertia, Vue 3, Vitest 4.1.8, happy-dom.

## Global Constraints

- Database and API instants remain UTC; do not add eight hours manually.
- A raw `2026-08-28 10:16:00` UTC value emits `2026-08-28T10:16:00Z`.
- Calls History list and detail display `28 Aug 2026, 18:16` / the equivalent long detail format in `Asia/Kuala_Lumpur`.
- Do not migrate or rewrite historical rows.
- Do not alter sorting, filtering, duration, direction, or unrelated date surfaces.
- Make surgical changes and atomic commits; do not push or open a PR.
- After all task tests pass, cherry-pick only these task commits into local `dev-chen` following the repository integration policy.

---

## File structure

- `src/Call/Support/CallRecordingPresenter.php`: authoritative raw-UTC-to-ISO conversion.
- `app/Http/Controllers/Manage/Calls/HistoryController.php`: list rows consume the presenter conversion.
- `tests/Feature/Manage/Calls/HistoryTimezoneTest.php`: list/detail UTC contract.
- `resources/js/Pages/Manage/Calls/History/callHistoryTime.js`: focused list date formatter.
- `resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js`: host-timezone-independent formatter regression.
- `resources/js/Pages/Manage/Calls/History/Index.vue`: configured timezone wiring.
- `resources/js/Components/RecordingDetail/Tabs/BasicInfoTab.test.js`: detail display regression.

### Task 1: Emit an explicit UTC instant from list and detail payloads

**Files:**
- Create: `tests/Feature/Manage/Calls/HistoryTimezoneTest.php`
- Modify: `src/Call/Support/CallRecordingPresenter.php`
- Modify: `app/Http/Controllers/Manage/Calls/HistoryController.php`

**Interfaces:**
- Produces: `CallRecordingPresenter::calledAtIsoUtc(CallRecording $recording): ?string`.
- Consumes: `CallRecording::getRawOriginal('called_at')`.
- Produces UTC `Z` timestamps for presenter summary/detail and History list rows.

- [ ] **Step 1: Write the failing feature test**

Create a real record and assert both list and detail props:

```php
<?php

namespace Tests\Feature\Manage\Calls;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Src\Call\CallRecording;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;

class HistoryTimezoneTest extends TestCase
{
    use InteractsWithAdmin;
    use RefreshDatabase;

    public function test_list_and_detail_emit_called_at_as_an_explicit_utc_instant(): void
    {
        $this->actingAsAdmin();

        $call = CallRecording::create([
            'dedupe_key' => str_repeat('t', 64),
            'customer_phone_digits' => '60123456789',
            'direction' => CallRecording::DIRECTION_OUTBOUND,
            'source' => CallRecording::SOURCE_BADGE_BLE,
            'status' => CallRecording::STATUS_NEW,
            'duration_seconds' => 14,
            'called_at' => '2026-08-28 10:16:00',
        ]);

        $this->get('/manage/calls/history?'.http_build_query([
            'detail' => $call->uuid,
        ]))->assertInertia(fn (Assert $page) => $page
            ->component('Manage/Calls/History/Index')
            ->where('calls.data.0.called_at', '2026-08-28T10:16:00Z')
            ->where('detail.called_at', '2026-08-28T10:16:00Z'));
    }

    public function test_null_called_at_remains_null(): void
    {
        $this->actingAsAdmin();

        $call = CallRecording::create([
            'dedupe_key' => str_repeat('n', 64),
            'customer_phone_digits' => '60123456789',
            'direction' => CallRecording::DIRECTION_OUTBOUND,
            'source' => CallRecording::SOURCE_BADGE_BLE,
            'status' => CallRecording::STATUS_NEW,
            'called_at' => null,
        ]);

        $this->get('/manage/calls/history?'.http_build_query([
            'detail' => $call->uuid,
        ]))->assertInertia(fn (Assert $page) => $page
            ->where('calls.data.0.called_at', null)
            ->where('detail.called_at', null));
    }
}
```

- [ ] **Step 2: Run it and verify failure**

```bash
php artisan test tests/Feature/Manage/Calls/HistoryTimezoneTest.php
```

Expected: the non-null assertion fails because the current payload emits `10:16:00+08:00`.

- [ ] **Step 3: Implement the UTC serializer**

Import `Carbon\CarbonImmutable` in `CallRecordingPresenter` and add:

```php
public function calledAtIsoUtc(CallRecording $recording): ?string
{
    $raw = $recording->getRawOriginal('called_at');

    if ($raw === null || $raw === '') {
        return null;
    }

    return CarbonImmutable::parse((string) $raw, 'UTC')
        ->toIso8601ZuluString();
}
```

Use it in both presenter payload methods:

```php
'called_at' => $this->calledAtIsoUtc($r),
```

Use the same method in `HistoryController::transform()`:

```php
'called_at' => $this->recordings->calledAtIsoUtc($callRecording),
```

This assigns UTC semantics to the timezone-less database wall-clock; do not use `setTimezone()` on the already miscast Eloquent Carbon value.

- [ ] **Step 4: Format and verify backend behavior**

```bash
vendor/bin/pint src/Call/Support/CallRecordingPresenter.php app/Http/Controllers/Manage/Calls/HistoryController.php tests/Feature/Manage/Calls/HistoryTimezoneTest.php
php artisan test tests/Feature/Manage/Calls/HistoryTimezoneTest.php
php artisan test tests/Feature/AgentApi/AgentRecordingUploadTest.php
```

Expected: both tests pass; agent `+08:00` ingress still stores the equivalent UTC wall-clock.

- [ ] **Step 5: Commit**

```bash
git add src/Call/Support/CallRecordingPresenter.php app/Http/Controllers/Manage/Calls/HistoryController.php tests/Feature/Manage/Calls/HistoryTimezoneTest.php
git commit -m "fix(calls): serialize history timestamps as UTC"
```

### Task 2: Render list and detail in the configured Malaysia timezone

**Files:**
- Create: `resources/js/Pages/Manage/Calls/History/callHistoryTime.js`
- Create: `resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js`
- Modify: `resources/js/Pages/Manage/Calls/History/Index.vue`
- Modify: `resources/js/Components/RecordingDetail/Tabs/BasicInfoTab.test.js`

**Interfaces:**
- Produces: `formatCallHistoryDate(iso, timeZone = 'Asia/Kuala_Lumpur'): string`.
- Consumes: `usePage().props.userTimezone` with Malaysia fallback.
- Keeps detail rendering through the existing `BasicInfoTab` timezone formatter.

- [ ] **Step 1: Write the failing pure formatter test**

```javascript
import { describe, expect, it } from 'vitest';
import { formatCallHistoryDate } from './callHistoryTime.js';

describe('Call History time', () => {
    it('renders a UTC instant in Malaysia standard time', () => {
        expect(
            formatCallHistoryDate(
                '2026-08-28T10:16:00Z',
                'Asia/Kuala_Lumpur',
            ),
        ).toBe('28 Aug 2026, 18:16');
    });

    it('keeps the empty timestamp placeholder', () => {
        expect(formatCallHistoryDate(null)).toBe('—');
    });
});
```

- [ ] **Step 2: Run it and verify failure**

```bash
npm test -- resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js
```

Expected: FAIL because the formatter module does not exist.

- [ ] **Step 3: Implement and wire the formatter**

Create:

```javascript
export const formatCallHistoryDate = (
    iso,
    timeZone = 'Asia/Kuala_Lumpur',
) => iso
    ? new Date(iso).toLocaleString('en-GB', {
        timeZone,
        day: '2-digit',
        month: 'short',
        year: 'numeric',
        hour: '2-digit',
        minute: '2-digit',
    })
    : '—';
```

In `Index.vue` import it, reuse the existing `page = usePage()`, and replace the local formatter:

```javascript
import { formatCallHistoryDate } from './callHistoryTime.js';

const userTimezone = computed(
    () => page.props.userTimezone || 'Asia/Kuala_Lumpur',
);
const fmtDate = (iso) => formatCallHistoryDate(
    iso,
    userTimezone.value,
);
```

- [ ] **Step 4: Add the detail display assertion**

Update `mountTab` to accept recording overrides:

```javascript
const mountTab = (pipelineStage, overrides = {}) => {
    root = document.createElement('div');
    document.body.appendChild(root);
    app = createApp({
        render: () => h(BasicInfoTab, {
            recording: {
                audio_url: '/recordings/audio',
                transcript: null,
                pipeline_stage: pipelineStage,
                ...overrides,
            },
            retryUrl: '/recordings/retry',
        }),
    });
    app.mount(root);
};
```

Then include:

```javascript
it('renders a UTC call instant in Malaysia time', () => {
    mountTab(null, {
        recorded_at: '2026-08-28T10:16:00Z',
        duration_seconds: 14,
    });

    expect(root.textContent).toContain('28 August 2026');
    expect(root.textContent).toContain('18:16');
});
```

The mocked `usePage` already supplies `Asia/Kuala_Lumpur`.

- [ ] **Step 5: Run targeted frontend tests**

```bash
npm test -- resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js resources/js/Components/RecordingDetail/Tabs/BasicInfoTab.test.js resources/js/Pages/Manage/Calls/History/Partials/CallDetailDrawer.test.js
```

Expected: all targeted Vitest files pass.

- [ ] **Step 6: Commit**

```bash
git add resources/js/Pages/Manage/Calls/History/callHistoryTime.js resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js resources/js/Pages/Manage/Calls/History/Index.vue resources/js/Components/RecordingDetail/Tabs/BasicInfoTab.test.js
git commit -m "fix(calls): render history in Malaysia time"
```

### Task 3: Relevant regression and local dev-chen integration

**Files:**
- No new source files.
- Integrate only task commits into the existing local `dev-chen` worktree.

**Interfaces:**
- Consumes: Tasks 1–2 plus design/plan commits.
- Produces: tested task commits cherry-picked into local `dev-chen`.
- Does not push.

- [ ] **Step 1: Run relevant complete checks**

```bash
php artisan test tests/Feature/Manage/Calls/HistoryTimezoneTest.php tests/Feature/AgentApi/AgentRecordingUploadTest.php tests/Feature/Manage/Calls/CallOpportunityColumnsTest.php
npm test -- resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js resources/js/Components/RecordingDetail/Tabs/BasicInfoTab.test.js resources/js/Pages/Manage/Calls/History/Partials/CallDetailDrawer.test.js
vendor/bin/pint --test src/Call/Support/CallRecordingPresenter.php app/Http/Controllers/Manage/Calls/HistoryController.php tests/Feature/Manage/Calls/HistoryTimezoneTest.php
git diff fb4792bfb^..HEAD --check
```

Expected: all tests and formatting checks pass.

- [ ] **Step 2: Verify the real regression value locally**

Use the test response or a local Inertia request to confirm:

```text
Database:  2026-08-28 10:16:00
Payload:   2026-08-28T10:16:00Z
List:      28 Aug 2026, 18:16
Detail:    28 August 2026 ... 18:16
```

- [ ] **Step 3: Confirm source and target worktrees are safe**

```bash
git status --short --branch
git worktree list --porcelain
```

Locate the worktree whose branch is `refs/heads/dev-chen`. Stop if that target has tracked or untracked changes whose ownership is ambiguous.

- [ ] **Step 4: Create a recoverable target backup**

In the clean `dev-chen` worktree:

```bash
git branch backup/dev-chen-before-calls-time-20260828
```

Expected: backup points to the pre-integration target HEAD.

- [ ] **Step 5: Cherry-pick only task commits**

Cherry-pick, in order, the Calls History design commit `fb4792bfb`, this implementation-plan commit, and the two implementation commits created by Tasks 1–2. Use explicit hashes resolved from `git log --oneline`; do not merge the feature branch.

- [ ] **Step 6: Re-run targeted checks in dev-chen**

```bash
php artisan test tests/Feature/Manage/Calls/HistoryTimezoneTest.php tests/Feature/AgentApi/AgentRecordingUploadTest.php
npm test -- resources/js/Pages/Manage/Calls/History/callHistoryTime.test.js resources/js/Components/RecordingDetail/Tabs/BasicInfoTab.test.js
git status --short --branch
```

Expected: checks pass and `dev-chen` is clean. Do not push.
