feat(helpdesk): add debitor meetings tab with upcoming/past meeting display
Adds a meetings section to the debitor detail page, fetching meeting data from BC OData API with cache control support. Includes timeline UI for upcoming and past meetings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ class DebitorCacheControlTest extends TestCase
|
||||
public function testInvalidateDebitorCachesRemovesDebitorAndTenantSharedKeys(): void
|
||||
{
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->expects($this->exactly(13))
|
||||
$sessionStore->expects($this->exactly(14))
|
||||
->method('remove')
|
||||
->with($this->callback(static function (string $key): bool {
|
||||
return str_contains($key, '.13.10259')
|
||||
|
||||
@@ -1245,4 +1245,134 @@ class DebitorDetailServiceTest extends TestCase
|
||||
$this->assertSame(2, $result['members'][0]['open_tickets']);
|
||||
$this->assertSame('Max Müller', $result['members'][0]['display_name']);
|
||||
}
|
||||
|
||||
// --- buildMeetingsTimeline() ---
|
||||
|
||||
public function testBuildMeetingsTimelineReturnsEmptyForNoMeetings(): void
|
||||
{
|
||||
$result = DebitorDetailService::buildMeetingsTimeline([]);
|
||||
|
||||
$this->assertSame([], $result['timeline']['upcoming']);
|
||||
$this->assertSame([], $result['timeline']['past']);
|
||||
$this->assertSame(0, $result['kpis']['total']);
|
||||
$this->assertSame(0, $result['kpis']['upcoming_count']);
|
||||
$this->assertSame(0, $result['kpis']['days_since_last']);
|
||||
$this->assertNull($result['kpis']['last_meeting_date']);
|
||||
}
|
||||
|
||||
public function testBuildMeetingsTimelineSkipsZeroDateEntries(): void
|
||||
{
|
||||
$meetings = [
|
||||
['Entry_No' => 1, 'Appointment_Date' => '0001-01-01', 'Customer_No' => '10254', 'Comment' => 'Skip me'],
|
||||
['Entry_No' => 2, 'Appointment_Date' => '', 'Customer_No' => '10254', 'Comment' => 'Also skip'],
|
||||
];
|
||||
|
||||
$result = DebitorDetailService::buildMeetingsTimeline($meetings);
|
||||
|
||||
$this->assertSame(0, $result['kpis']['total']);
|
||||
}
|
||||
|
||||
public function testBuildMeetingsTimelineSplitsUpcomingAndPast(): void
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$futureDate = date('Y-m-d', strtotime('+30 days'));
|
||||
$pastDate = date('Y-m-d', strtotime('-10 days'));
|
||||
|
||||
$meetings = [
|
||||
['Entry_No' => 1, 'Appointment_Date' => $futureDate, 'Customer_No' => '10254', 'Customer_Name' => 'Test', 'Salesperson_Code' => 'MER', 'Comment' => 'Future meeting', 'Released_by' => '', 'Release_Date' => '0001-01-01'],
|
||||
['Entry_No' => 2, 'Appointment_Date' => $pastDate, 'Customer_No' => '10254', 'Customer_Name' => 'Test', 'Salesperson_Code' => 'MER', 'Comment' => 'Past meeting', 'Released_by' => 'USER', 'Release_Date' => $pastDate],
|
||||
];
|
||||
|
||||
$result = DebitorDetailService::buildMeetingsTimeline($meetings);
|
||||
|
||||
$this->assertSame(2, $result['kpis']['total']);
|
||||
$this->assertSame(1, $result['kpis']['upcoming_count']);
|
||||
$this->assertSame(10, $result['kpis']['days_since_last']);
|
||||
$this->assertSame($pastDate, $result['kpis']['last_meeting_date']);
|
||||
|
||||
$futureMonth = substr($futureDate, 0, 7);
|
||||
$pastMonth = substr($pastDate, 0, 7);
|
||||
|
||||
$this->assertArrayHasKey($futureMonth, $result['timeline']['upcoming']);
|
||||
$this->assertArrayHasKey($pastMonth, $result['timeline']['past']);
|
||||
|
||||
// Verify upcoming entry
|
||||
$upcomingEntry = $result['timeline']['upcoming'][$futureMonth][0];
|
||||
$this->assertSame('Future meeting', $upcomingEntry['comment']);
|
||||
$this->assertFalse($upcomingEntry['is_released']);
|
||||
|
||||
// Verify past entry
|
||||
$pastEntry = $result['timeline']['past'][$pastMonth][0];
|
||||
$this->assertSame('Past meeting', $pastEntry['comment']);
|
||||
$this->assertTrue($pastEntry['is_released']);
|
||||
$this->assertSame($pastDate, $pastEntry['release_date']);
|
||||
}
|
||||
|
||||
public function testBuildMeetingsTimelineGroupsByMonth(): void
|
||||
{
|
||||
$meetings = [
|
||||
['Entry_No' => 1, 'Appointment_Date' => '2025-03-15', 'Customer_No' => '10254', 'Customer_Name' => 'A', 'Salesperson_Code' => 'MER', 'Comment' => 'A', 'Released_by' => '', 'Release_Date' => '0001-01-01'],
|
||||
['Entry_No' => 2, 'Appointment_Date' => '2025-03-20', 'Customer_No' => '10254', 'Customer_Name' => 'B', 'Salesperson_Code' => 'MER', 'Comment' => 'B', 'Released_by' => '', 'Release_Date' => '0001-01-01'],
|
||||
['Entry_No' => 3, 'Appointment_Date' => '2025-02-10', 'Customer_No' => '10254', 'Customer_Name' => 'C', 'Salesperson_Code' => 'MER', 'Comment' => 'C', 'Released_by' => 'U', 'Release_Date' => '2025-02-11'],
|
||||
];
|
||||
|
||||
$result = DebitorDetailService::buildMeetingsTimeline($meetings);
|
||||
|
||||
$this->assertSame(3, $result['kpis']['total']);
|
||||
// All 3 are in the past (2025-02, 2025-03)
|
||||
$this->assertArrayHasKey('2025-03', $result['timeline']['past']);
|
||||
$this->assertArrayHasKey('2025-02', $result['timeline']['past']);
|
||||
$this->assertCount(2, $result['timeline']['past']['2025-03']);
|
||||
$this->assertCount(1, $result['timeline']['past']['2025-02']);
|
||||
}
|
||||
|
||||
// --- loadMeetings() ---
|
||||
|
||||
public function testLoadMeetingsReturnsErrorForEmptyCustomerNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadMeetings('');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('Missing customer number', $result['error']);
|
||||
}
|
||||
|
||||
public function testLoadMeetingsReturnsErrorWhenNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->loadMeetings('10254');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('BC connection not configured', $result['error']);
|
||||
}
|
||||
|
||||
public function testLoadMeetingsReturnsTimelineOnSuccess(): void
|
||||
{
|
||||
$futureDate = date('Y-m-d', strtotime('+7 days'));
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getMeetingsForCustomer')->with('10254')->willReturn([
|
||||
['Entry_No' => 1, 'Appointment_Date' => $futureDate, 'Customer_No' => '10254', 'Customer_Name' => 'Test GmbH', 'Salesperson_Code' => 'MER', 'Comment' => 'Workshop', 'Released_by' => '', 'Release_Date' => '0001-01-01'],
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadMeetings('10254');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(1, $result['kpis']['total']);
|
||||
$this->assertSame(1, $result['kpis']['upcoming_count']);
|
||||
$this->assertArrayHasKey('upcoming', $result['timeline']);
|
||||
$this->assertArrayHasKey('past', $result['timeline']);
|
||||
}
|
||||
|
||||
public function testLoadMeetingsReturnsErrorOnGatewayException(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getMeetingsForCustomer')->willThrowException(new \RuntimeException('Connection timeout'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadMeetings('10254');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('Connection timeout', $result['error']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user