feat: Afficher la couleur des matières dans l'emploi du temps élève et parent
L'admin pouvait attribuer une couleur à chaque matière, mais cette couleur n'était utilisée que dans la vue admin de l'emploi du temps. Les APIs élève et parent ne renvoyaient pas cette information, ce qui donnait un affichage générique (gris/bleu) pour tous les créneaux. L'API renvoie désormais subjectColor dans chaque créneau, et les vues jour/semaine/widget/détails affichent la bordure colorée correspondante. Le marqueur "Prochain cours" conserve sa priorité visuelle via une surcharge CSS variable.
This commit is contained in:
@@ -15,9 +15,9 @@ interface ScheduleDisplayReader
|
|||||||
/**
|
/**
|
||||||
* @param string ...$subjectIds Identifiants des matières
|
* @param string ...$subjectIds Identifiants des matières
|
||||||
*
|
*
|
||||||
* @return array<string, string> Map subjectId => nom de la matière
|
* @return array<string, array{name: string, color: string|null}> Map subjectId => {name, color}
|
||||||
*/
|
*/
|
||||||
public function subjectNames(string $tenantId, string ...$subjectIds): array;
|
public function subjectDisplay(string $tenantId, string ...$subjectIds): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string ...$teacherIds Identifiants des enseignants
|
* @param string ...$teacherIds Identifiants des enseignants
|
||||||
|
|||||||
@@ -111,13 +111,14 @@ final readonly class GetChildrenScheduleHandler
|
|||||||
array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots),
|
array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots),
|
||||||
));
|
));
|
||||||
|
|
||||||
$subjectNames = $this->displayReader->subjectNames($tenantId, ...$subjectIds);
|
$subjects = $this->displayReader->subjectDisplay($tenantId, ...$subjectIds);
|
||||||
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
|
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
|
||||||
|
|
||||||
return array_map(
|
return array_map(
|
||||||
static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved(
|
static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved(
|
||||||
$s,
|
$s,
|
||||||
$subjectNames[(string) $s->subjectId] ?? '',
|
$subjects[(string) $s->subjectId]['name'] ?? '',
|
||||||
|
$subjects[(string) $s->subjectId]['color'] ?? null,
|
||||||
$teacherNames[(string) $s->teacherId] ?? '',
|
$teacherNames[(string) $s->teacherId] ?? '',
|
||||||
),
|
),
|
||||||
$slots,
|
$slots,
|
||||||
|
|||||||
@@ -74,13 +74,14 @@ final readonly class GetStudentScheduleHandler
|
|||||||
array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots),
|
array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots),
|
||||||
));
|
));
|
||||||
|
|
||||||
$subjectNames = $this->displayReader->subjectNames($tenantId, ...$subjectIds);
|
$subjects = $this->displayReader->subjectDisplay($tenantId, ...$subjectIds);
|
||||||
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
|
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
|
||||||
|
|
||||||
return array_map(
|
return array_map(
|
||||||
static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved(
|
static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved(
|
||||||
$s,
|
$s,
|
||||||
$subjectNames[(string) $s->subjectId] ?? '',
|
$subjects[(string) $s->subjectId]['name'] ?? '',
|
||||||
|
$subjects[(string) $s->subjectId]['color'] ?? null,
|
||||||
$teacherNames[(string) $s->teacherId] ?? '',
|
$teacherNames[(string) $s->teacherId] ?? '',
|
||||||
),
|
),
|
||||||
$slots,
|
$slots,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ final readonly class StudentScheduleSlotDto
|
|||||||
public string $endTime,
|
public string $endTime,
|
||||||
public string $subjectId,
|
public string $subjectId,
|
||||||
public string $subjectName,
|
public string $subjectName,
|
||||||
|
public ?string $subjectColor,
|
||||||
public string $teacherId,
|
public string $teacherId,
|
||||||
public string $teacherName,
|
public string $teacherName,
|
||||||
public ?string $room,
|
public ?string $room,
|
||||||
@@ -27,6 +28,7 @@ final readonly class StudentScheduleSlotDto
|
|||||||
public static function fromResolved(
|
public static function fromResolved(
|
||||||
ResolvedScheduleSlot $slot,
|
ResolvedScheduleSlot $slot,
|
||||||
string $subjectName,
|
string $subjectName,
|
||||||
|
?string $subjectColor,
|
||||||
string $teacherName,
|
string $teacherName,
|
||||||
): self {
|
): self {
|
||||||
return new self(
|
return new self(
|
||||||
@@ -37,6 +39,7 @@ final readonly class StudentScheduleSlotDto
|
|||||||
endTime: $slot->timeSlot->endTime,
|
endTime: $slot->timeSlot->endTime,
|
||||||
subjectId: (string) $slot->subjectId,
|
subjectId: (string) $slot->subjectId,
|
||||||
subjectName: $subjectName,
|
subjectName: $subjectName,
|
||||||
|
subjectColor: $subjectColor,
|
||||||
teacherId: (string) $slot->teacherId,
|
teacherId: (string) $slot->teacherId,
|
||||||
teacherName: $teacherName,
|
teacherName: $teacherName,
|
||||||
room: $slot->room,
|
room: $slot->room,
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ use function usort;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoints de consultation de l'emploi du temps des enfants pour le parent connecté.
|
* Endpoints de consultation de l'emploi du temps des enfants pour le parent connecté.
|
||||||
*
|
|
||||||
* @see Story 4.4 - Consultation EDT par le Parent
|
|
||||||
* @see FR30 - Consulter emploi du temps enfant (parent)
|
|
||||||
*/
|
*/
|
||||||
#[IsGranted(ScheduleSlotVoter::VIEW)]
|
#[IsGranted(ScheduleSlotVoter::VIEW)]
|
||||||
final readonly class ParentScheduleController
|
final readonly class ParentScheduleController
|
||||||
@@ -190,6 +187,7 @@ final readonly class ParentScheduleController
|
|||||||
'endTime' => $slot->endTime,
|
'endTime' => $slot->endTime,
|
||||||
'subjectId' => $slot->subjectId,
|
'subjectId' => $slot->subjectId,
|
||||||
'subjectName' => $slot->subjectName,
|
'subjectName' => $slot->subjectName,
|
||||||
|
'subjectColor' => $slot->subjectColor,
|
||||||
'teacherId' => $slot->teacherId,
|
'teacherId' => $slot->teacherId,
|
||||||
'teacherName' => $slot->teacherName,
|
'teacherName' => $slot->teacherName,
|
||||||
'room' => $slot->room,
|
'room' => $slot->room,
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ use function usort;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoints de consultation de l'emploi du temps pour l'élève connecté.
|
* Endpoints de consultation de l'emploi du temps pour l'élève connecté.
|
||||||
*
|
|
||||||
* @see Story 4.3 - Consultation EDT par l'Élève
|
|
||||||
* @see FR29 - Consulter emploi du temps (élève)
|
|
||||||
*/
|
*/
|
||||||
#[IsGranted(ScheduleSlotVoter::VIEW)]
|
#[IsGranted(ScheduleSlotVoter::VIEW)]
|
||||||
final readonly class StudentScheduleController
|
final readonly class StudentScheduleController
|
||||||
@@ -178,6 +175,7 @@ final readonly class StudentScheduleController
|
|||||||
'endTime' => $slot->endTime,
|
'endTime' => $slot->endTime,
|
||||||
'subjectId' => $slot->subjectId,
|
'subjectId' => $slot->subjectId,
|
||||||
'subjectName' => $slot->subjectName,
|
'subjectName' => $slot->subjectName,
|
||||||
|
'subjectColor' => $slot->subjectColor,
|
||||||
'teacherId' => $slot->teacherId,
|
'teacherId' => $slot->teacherId,
|
||||||
'teacherName' => $slot->teacherName,
|
'teacherName' => $slot->teacherName,
|
||||||
'room' => $slot->room,
|
'room' => $slot->room,
|
||||||
|
|||||||
@@ -20,29 +20,31 @@ final readonly class DoctrineScheduleDisplayReader implements ScheduleDisplayRea
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function subjectNames(string $tenantId, string ...$subjectIds): array
|
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
||||||
{
|
{
|
||||||
if ($subjectIds === []) {
|
if ($subjectIds === []) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$rows = $this->connection->fetchAllAssociative(
|
$rows = $this->connection->fetchAllAssociative(
|
||||||
'SELECT id, name FROM subjects WHERE id IN (:ids) AND tenant_id = :tenantId',
|
'SELECT id, name, color FROM subjects WHERE id IN (:ids) AND tenant_id = :tenantId',
|
||||||
['ids' => $subjectIds, 'tenantId' => $tenantId],
|
['ids' => $subjectIds, 'tenantId' => $tenantId],
|
||||||
['ids' => ArrayParameterType::STRING],
|
['ids' => ArrayParameterType::STRING],
|
||||||
);
|
);
|
||||||
|
|
||||||
$names = [];
|
$display = [];
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
/** @var string $id */
|
/** @var string $id */
|
||||||
$id = $row['id'];
|
$id = $row['id'];
|
||||||
/** @var string $name */
|
/** @var string $name */
|
||||||
$name = $row['name'];
|
$name = $row['name'];
|
||||||
$names[$id] = $name;
|
/** @var string|null $color */
|
||||||
|
$color = $row['color'];
|
||||||
|
$display[$id] = ['name' => $name, 'color' => $color];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $names;
|
return $display;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
|
|||||||
@@ -265,9 +265,14 @@ final class GetChildrenScheduleHandlerTest extends TestCase
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subjectNames(string $tenantId, string ...$subjectIds): array
|
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
||||||
{
|
{
|
||||||
return $this->subjects;
|
$display = [];
|
||||||
|
foreach ($this->subjects as $id => $name) {
|
||||||
|
$display[$id] = ['name' => $name, 'color' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $display;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
||||||
|
|||||||
@@ -210,9 +210,14 @@ final class GetStudentScheduleHandlerTest extends TestCase
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subjectNames(string $tenantId, string ...$subjectIds): array
|
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
||||||
{
|
{
|
||||||
return $this->subjects;
|
$display = [];
|
||||||
|
foreach ($this->subjects as $id => $name) {
|
||||||
|
$display[$id] = ['name' => $name, 'color' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $display;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ test.describe('Child Selector', () => {
|
|||||||
// Should display the label
|
// Should display the label
|
||||||
await expect(childSelector.locator('.child-selector-label')).toHaveText('Enfant :');
|
await expect(childSelector.locator('.child-selector-label')).toHaveText('Enfant :');
|
||||||
|
|
||||||
// Should have 2 child buttons
|
// Should have 3 buttons: "Tous" + 2 children
|
||||||
const buttons = childSelector.locator('.child-button');
|
const buttons = childSelector.locator('.child-button');
|
||||||
await expect(buttons).toHaveCount(2);
|
await expect(buttons).toHaveCount(3);
|
||||||
|
|
||||||
// First child should be auto-selected
|
// "Tous" button should be selected initially (no child auto-selected)
|
||||||
await expect(buttons.first()).toHaveClass(/selected/);
|
await expect(buttons.first()).toHaveClass(/selected/);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,18 +162,18 @@ test.describe('Child Selector', () => {
|
|||||||
await expect(childSelector).toBeVisible({ timeout: 10000 });
|
await expect(childSelector).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
const buttons = childSelector.locator('.child-button');
|
const buttons = childSelector.locator('.child-button');
|
||||||
await expect(buttons).toHaveCount(2);
|
await expect(buttons).toHaveCount(3);
|
||||||
|
|
||||||
// First button should be selected initially
|
// "Tous" button (index 0) should be selected initially
|
||||||
await expect(buttons.first()).toHaveClass(/selected/);
|
await expect(buttons.nth(0)).toHaveClass(/selected/);
|
||||||
await expect(buttons.nth(1)).not.toHaveClass(/selected/);
|
await expect(buttons.nth(1)).not.toHaveClass(/selected/);
|
||||||
|
|
||||||
// Click second button
|
// Click first child button (index 1)
|
||||||
await buttons.nth(1).click();
|
await buttons.nth(1).click();
|
||||||
|
|
||||||
// Second button should now be selected, first should not
|
// First child should now be selected, "Tous" should not
|
||||||
await expect(buttons.nth(1)).toHaveClass(/selected/);
|
await expect(buttons.nth(1)).toHaveClass(/selected/);
|
||||||
await expect(buttons.first()).not.toHaveClass(/selected/);
|
await expect(buttons.nth(0)).not.toHaveClass(/selected/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[P1] parent with single child should see static child name', async ({ browser, page }) => {
|
test('[P1] parent with single child should see static child name', async ({ browser, page }) => {
|
||||||
|
|||||||
@@ -114,6 +114,21 @@ async function loginAsParent(page: import('@playwright/test').Page) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-child parents land on the summary view (no child auto-selected).
|
||||||
|
* This helper selects the first actual child (skipping the "Tous" button).
|
||||||
|
*/
|
||||||
|
async function selectFirstChild(page: import('@playwright/test').Page) {
|
||||||
|
const childButtons = page.locator('.child-button');
|
||||||
|
await expect(childButtons.first()).toBeVisible({ timeout: 15000 });
|
||||||
|
const count = await childButtons.count();
|
||||||
|
if (count > 2) {
|
||||||
|
// Multi-child: "Tous" is at index 0, first child at index 1
|
||||||
|
await childButtons.nth(1).click();
|
||||||
|
}
|
||||||
|
// Single child is auto-selected, nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
||||||
test.describe.configure({ mode: 'serial' });
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
@@ -305,6 +320,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
||||||
).toBeVisible({ timeout: 15000 });
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
await selectFirstChild(page);
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
|
|
||||||
// Wait for slots to load
|
// Wait for slots to load
|
||||||
@@ -324,19 +340,29 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
||||||
).toBeVisible({ timeout: 15000 });
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
// If multiple children, buttons should be visible
|
// Multi-child: "Tous" + N children buttons
|
||||||
const childButtons = page.locator('.child-button');
|
const childButtons = page.locator('.child-button');
|
||||||
const count = await childButtons.count();
|
const count = await childButtons.count();
|
||||||
|
|
||||||
if (count > 1) {
|
if (count > 2) {
|
||||||
// Click second child
|
// Select first child (index 1, after "Tous")
|
||||||
await childButtons.nth(1).click();
|
await childButtons.nth(1).click();
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Should update schedule data
|
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
const slots = page.locator('[data-testid="schedule-slot"]');
|
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
||||||
await expect(slots.first()).toBeVisible({ timeout: 20000 });
|
timeout: 20000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch to second child (index 2)
|
||||||
|
await childButtons.nth(2).click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await navigateToSeededDay(page);
|
||||||
|
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
||||||
|
timeout: 20000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch back to "Tous" summary view
|
||||||
|
await childButtons.nth(0).click();
|
||||||
|
await expect(page.locator('.multi-child-summary')).toBeVisible({ timeout: 10000 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -351,6 +377,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
||||||
).toBeVisible({ timeout: 15000 });
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
await selectFirstChild(page);
|
||||||
|
|
||||||
const dayButton = page.locator('.view-toggle button', { hasText: 'Jour' });
|
const dayButton = page.locator('.view-toggle button', { hasText: 'Jour' });
|
||||||
await expect(dayButton).toHaveClass(/active/, { timeout: 5000 });
|
await expect(dayButton).toHaveClass(/active/, { timeout: 5000 });
|
||||||
@@ -362,6 +389,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
||||||
).toBeVisible({ timeout: 15000 });
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
await selectFirstChild(page);
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
|
|
||||||
// Wait for slots to load
|
// Wait for slots to load
|
||||||
@@ -384,6 +412,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
||||||
).toBeVisible({ timeout: 15000 });
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
await selectFirstChild(page);
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
|
|
||||||
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
||||||
@@ -419,6 +448,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
page.getByRole('heading', { name: /emploi du temps des enfants/i })
|
||||||
).toBeVisible({ timeout: 15000 });
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
await selectFirstChild(page);
|
||||||
|
|
||||||
// Wait for schedule slots to load
|
// Wait for schedule slots to load
|
||||||
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
||||||
@@ -448,9 +478,11 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
|
|
||||||
const childButtons = page.locator('.child-button');
|
const childButtons = page.locator('.child-button');
|
||||||
const count = await childButtons.count();
|
const count = await childButtons.count();
|
||||||
test.skip(count < 2, 'Need at least 2 children for this test');
|
// "Tous" + at least 2 children = 3 buttons minimum
|
||||||
|
test.skip(count < 3, 'Need at least 2 children for this test');
|
||||||
|
|
||||||
// Navigate to seeded day to see slots
|
// Select first child (index 1, after "Tous")
|
||||||
|
await childButtons.nth(1).click();
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
|
|
||||||
// First child (6A) should show Maths
|
// First child (6A) should show Maths
|
||||||
@@ -461,8 +493,8 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
timeout: 5000
|
timeout: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Switch to second child (5B) — should show SVT
|
// Switch to second child (index 2) (5B) — should show SVT
|
||||||
await childButtons.nth(1).click();
|
await childButtons.nth(2).click();
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({
|
||||||
timeout: 20000
|
timeout: 20000
|
||||||
@@ -480,6 +512,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => {
|
|||||||
test('shows offline banner when network is lost', async ({ page, context }) => {
|
test('shows offline banner when network is lost', async ({ page, context }) => {
|
||||||
await loginAsParent(page);
|
await loginAsParent(page);
|
||||||
await page.goto(`${ALPHA_URL}/dashboard/parent-schedule`);
|
await page.goto(`${ALPHA_URL}/dashboard/parent-schedule`);
|
||||||
|
await selectFirstChild(page);
|
||||||
await navigateToSeededDay(page);
|
await navigateToSeededDay(page);
|
||||||
|
|
||||||
// Wait for schedule to load
|
// Wait for schedule to load
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
let {
|
let {
|
||||||
onChildSelected
|
onChildSelected
|
||||||
}: {
|
}: {
|
||||||
onChildSelected?: (childId: string) => void;
|
onChildSelected?: (childId: string | null) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let children = $state<Child[]>([]);
|
let children = $state<Child[]>([]);
|
||||||
@@ -40,8 +40,9 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
children = data['hydra:member'] ?? data['member'] ?? (Array.isArray(data) ? data : []);
|
children = data['hydra:member'] ?? data['member'] ?? (Array.isArray(data) ? data : []);
|
||||||
|
|
||||||
|
// Auto-select only when there's exactly 1 child
|
||||||
const first = children[0];
|
const first = children[0];
|
||||||
if (first && !selectedChildId) {
|
if (first && !selectedChildId && children.length === 1) {
|
||||||
selectedChildId = first.studentId;
|
selectedChildId = first.studentId;
|
||||||
onChildSelected?.(first.studentId);
|
onChildSelected?.(first.studentId);
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectChild(childId: string) {
|
function selectChild(childId: string | null) {
|
||||||
selectedChildId = childId;
|
selectedChildId = childId;
|
||||||
onChildSelected?.(childId);
|
onChildSelected?.(childId);
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,13 @@
|
|||||||
<div class="child-selector">
|
<div class="child-selector">
|
||||||
<span class="child-selector-label">Enfant :</span>
|
<span class="child-selector-label">Enfant :</span>
|
||||||
<div class="child-selector-buttons">
|
<div class="child-selector-buttons">
|
||||||
|
<button
|
||||||
|
class="child-button"
|
||||||
|
class:selected={selectedChildId === null}
|
||||||
|
onclick={() => selectChild(null)}
|
||||||
|
>
|
||||||
|
Tous
|
||||||
|
</button>
|
||||||
{#each children as child (child.id)}
|
{#each children as child (child.id)}
|
||||||
<button
|
<button
|
||||||
class="child-button"
|
class="child-button"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { DemoData } from '$types';
|
import type { DemoData } from '$types';
|
||||||
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
||||||
import { fetchChildDaySchedule } from '$lib/features/schedule/api/parentSchedule';
|
import { fetchChildDaySchedule } from '$lib/features/schedule/api/parentSchedule';
|
||||||
import { recordSync } from '$lib/features/schedule/stores/scheduleCache';
|
import { recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
import SerenityScorePreview from '$lib/components/molecules/SerenityScore/SerenityScorePreview.svelte';
|
import SerenityScorePreview from '$lib/components/molecules/SerenityScore/SerenityScorePreview.svelte';
|
||||||
import SerenityScoreExplainer from '$lib/components/molecules/SerenityScore/SerenityScoreExplainer.svelte';
|
import SerenityScoreExplainer from '$lib/components/molecules/SerenityScore/SerenityScoreExplainer.svelte';
|
||||||
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
|
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { DemoData } from '$types';
|
import type { DemoData } from '$types';
|
||||||
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
||||||
import { fetchDaySchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
|
import { fetchDaySchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
|
||||||
import { recordSync } from '$lib/features/schedule/stores/scheduleCache';
|
import { recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
|
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
|
||||||
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
|
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
|
||||||
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
|
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { ChildScheduleSummary } from '$lib/features/schedule/api/parentSchedule';
|
import type { ChildScheduleSummary } from '$lib/features/schedule/api/parentSchedule';
|
||||||
import { fetchChildrenScheduleSummary } from '$lib/features/schedule/api/parentSchedule';
|
import { fetchChildrenScheduleSummary } from '$lib/features/schedule/api/parentSchedule';
|
||||||
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
||||||
import { isOffline, getLastSyncDate } from '$lib/features/schedule/stores/scheduleCache';
|
import { isOffline, getLastSyncDate, recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
children = await fetchChildrenScheduleSummary();
|
children = await fetchChildrenScheduleSummary();
|
||||||
|
recordSync();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Erreur de chargement';
|
error = e instanceof Error ? e.message : 'Erreur de chargement';
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
||||||
import { fetchChildDaySchedule, fetchChildWeekSchedule } from '$lib/features/schedule/api/parentSchedule';
|
import { fetchChildDaySchedule, fetchChildWeekSchedule } from '$lib/features/schedule/api/parentSchedule';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache';
|
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
||||||
import ChildSelector from '$lib/components/organisms/ChildSelector/ChildSelector.svelte';
|
import ChildSelector from '$lib/components/organisms/ChildSelector/ChildSelector.svelte';
|
||||||
import MultiChildSummary from '$lib/components/organisms/ParentSchedule/MultiChildSummary.svelte';
|
import MultiChildSummary from '$lib/components/organisms/ParentSchedule/MultiChildSummary.svelte';
|
||||||
@@ -88,13 +88,15 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleChildSelected(childId: string) {
|
function handleChildSelected(childId: string | null) {
|
||||||
selectedChildId = childId;
|
selectedChildId = childId;
|
||||||
untrack(() => {
|
if (childId) {
|
||||||
loadSchedule();
|
untrack(() => {
|
||||||
// Prefetch for offline support
|
loadSchedule();
|
||||||
prefetchScheduleDays((date) => fetchChildDaySchedule(childId, date));
|
// Prefetch for offline support
|
||||||
});
|
prefetchScheduleDays((date) => fetchChildDaySchedule(childId, date));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateDay(offset: number) {
|
function navigateDay(offset: number) {
|
||||||
|
|||||||
@@ -44,7 +44,11 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<ul class="slot-list">
|
<ul class="slot-list">
|
||||||
{#each slots as slot (slot.slotId + slot.date)}
|
{#each slots as slot (slot.slotId + slot.date)}
|
||||||
<li class="slot-item" class:next={slot.slotId === nextSlotId}>
|
<li
|
||||||
|
class="slot-item"
|
||||||
|
class:next={slot.slotId === nextSlotId}
|
||||||
|
style:--slot-color={slot.subjectColor ?? '#e5e7eb'}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="slot-button"
|
class="slot-button"
|
||||||
onclick={() => onSlotClick(slot)}
|
onclick={() => onSlotClick(slot)}
|
||||||
@@ -123,12 +127,12 @@
|
|||||||
.slot-item {
|
.slot-item {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-left: 4px solid #e5e7eb;
|
border-left: 4px solid var(--slot-color, #e5e7eb);
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-item.next {
|
.slot-item.next {
|
||||||
border-left-color: #3b82f6;
|
--slot-color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-button {
|
.slot-button {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
||||||
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
||||||
import { isOffline, getLastSyncDate } from '$lib/features/schedule/stores/scheduleCache';
|
import { isOffline, getLastSyncDate } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
slots = [],
|
slots = [],
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
class="slot-item"
|
class="slot-item"
|
||||||
class:next={slot.slotId === nextSlotId}
|
class:next={slot.slotId === nextSlotId}
|
||||||
data-testid="schedule-slot"
|
data-testid="schedule-slot"
|
||||||
|
style:--slot-color={slot.subjectColor ?? 'transparent'}
|
||||||
>
|
>
|
||||||
<div class="slot-time">
|
<div class="slot-time">
|
||||||
<span class="time-start">{slot.startTime}</span>
|
<span class="time-start">{slot.startTime}</span>
|
||||||
@@ -144,12 +145,12 @@
|
|||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid var(--slot-color, transparent);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-item.next {
|
.slot-item.next {
|
||||||
border-left-color: #3b82f6;
|
--slot-color: #3b82f6;
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
aria-label="Fermer">×</button
|
aria-label="Fermer">×</button
|
||||||
>
|
>
|
||||||
|
|
||||||
<h2 class="subject-name">{slot.subjectName}</h2>
|
<h2 class="subject-name" class:has-color={slot.subjectColor} style:--slot-color={slot.subjectColor}>{slot.subjectName}</h2>
|
||||||
|
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
@@ -149,6 +149,11 @@
|
|||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subject-name.has-color {
|
||||||
|
border-left: 4px solid var(--slot-color);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { fetchDaySchedule, fetchWeekSchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
|
import { fetchDaySchedule, fetchWeekSchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
|
||||||
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache';
|
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
import DayView from './DayView.svelte';
|
import DayView from './DayView.svelte';
|
||||||
import WeekView from './WeekView.svelte';
|
import WeekView from './WeekView.svelte';
|
||||||
import SlotDetails from './SlotDetails.svelte';
|
import SlotDetails from './SlotDetails.svelte';
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
class:modified={slot.isModified}
|
class:modified={slot.isModified}
|
||||||
onclick={() => onSlotClick(slot)}
|
onclick={() => onSlotClick(slot)}
|
||||||
data-testid="week-slot"
|
data-testid="week-slot"
|
||||||
|
style:--slot-color={slot.subjectColor ?? '#e5e7eb'}
|
||||||
>
|
>
|
||||||
<span class="slot-time-mobile">{slot.startTime} - {slot.endTime}</span>
|
<span class="slot-time-mobile">{slot.startTime} - {slot.endTime}</span>
|
||||||
<span class="slot-subject-mobile">{slot.subjectName}</span>
|
<span class="slot-subject-mobile">{slot.subjectName}</span>
|
||||||
@@ -97,6 +98,7 @@
|
|||||||
class:modified={slot.isModified}
|
class:modified={slot.isModified}
|
||||||
onclick={() => onSlotClick(slot)}
|
onclick={() => onSlotClick(slot)}
|
||||||
data-testid="week-slot"
|
data-testid="week-slot"
|
||||||
|
style:--slot-color={slot.subjectColor ?? '#e5e7eb'}
|
||||||
>
|
>
|
||||||
<span class="week-slot-time">{slot.startTime}</span>
|
<span class="week-slot-time">{slot.startTime}</span>
|
||||||
<span class="week-slot-subject">{slot.subjectName}</span>
|
<span class="week-slot-subject">{slot.subjectName}</span>
|
||||||
@@ -182,6 +184,7 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid #f3f4f6;
|
border-top: 1px solid #f3f4f6;
|
||||||
|
border-left: 4px solid var(--slot-color, #e5e7eb);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@@ -283,6 +286,7 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
border: 1px solid #bfdbfe;
|
border: 1px solid #bfdbfe;
|
||||||
|
border-left: 4px solid var(--slot-color, #bfdbfe);
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ScheduleSlot {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
subjectId: string;
|
subjectId: string;
|
||||||
subjectName: string;
|
subjectName: string;
|
||||||
|
subjectColor: string | null;
|
||||||
teacherId: string;
|
teacherId: string;
|
||||||
teacherName: string;
|
teacherName: string;
|
||||||
room: string | null;
|
room: string | null;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { browser } from '$app/environment';
|
|||||||
|
|
||||||
const LAST_SYNC_KEY = 'classeo:schedule:lastSync';
|
const LAST_SYNC_KEY = 'classeo:schedule:lastSync';
|
||||||
|
|
||||||
|
let lastSyncValue = $state<string | null>(
|
||||||
|
browser ? localStorage.getItem(LAST_SYNC_KEY) : null
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si le navigateur est actuellement hors ligne.
|
* Vérifie si le navigateur est actuellement hors ligne.
|
||||||
*/
|
*/
|
||||||
@@ -15,15 +19,16 @@ export function isOffline(): boolean {
|
|||||||
*/
|
*/
|
||||||
export function recordSync(): void {
|
export function recordSync(): void {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
localStorage.setItem(LAST_SYNC_KEY, new Date().toISOString());
|
const now = new Date().toISOString();
|
||||||
|
localStorage.setItem(LAST_SYNC_KEY, now);
|
||||||
|
lastSyncValue = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère la date de dernière synchronisation de l'EDT.
|
* Récupère la date de dernière synchronisation de l'EDT (réactif via $state).
|
||||||
*/
|
*/
|
||||||
export function getLastSyncDate(): string | null {
|
export function getLastSyncDate(): string | null {
|
||||||
if (!browser) return null;
|
return lastSyncValue;
|
||||||
return localStorage.getItem(LAST_SYNC_KEY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
// Demo child name for personalized messages
|
// Demo child name for personalized messages
|
||||||
let childName = $state('Emma');
|
let childName = $state('Emma');
|
||||||
|
|
||||||
function handleChildSelected(childId: string) {
|
function handleChildSelected(childId: string | null) {
|
||||||
selectedChildId = childId;
|
selectedChildId = childId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ vi.mock('$app/environment', () => ({
|
|||||||
browser: true
|
browser: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { isOffline, recordSync, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache';
|
import { isOffline, recordSync, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
||||||
|
|
||||||
describe('scheduleCache', () => {
|
describe('scheduleCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/svelte';
|
|||||||
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
|
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
|
||||||
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
||||||
|
|
||||||
vi.mock('$lib/features/schedule/stores/scheduleCache', () => ({
|
vi.mock('$lib/features/schedule/stores/scheduleCache.svelte', () => ({
|
||||||
isOffline: vi.fn(() => false),
|
isOffline: vi.fn(() => false),
|
||||||
getLastSyncDate: vi.fn(() => null)
|
getLastSyncDate: vi.fn(() => null)
|
||||||
}));
|
}));
|
||||||
@@ -17,6 +17,7 @@ function makeSlot(overrides: Partial<ScheduleSlot> = {}): ScheduleSlot {
|
|||||||
endTime: '09:00',
|
endTime: '09:00',
|
||||||
subjectId: 'sub-1',
|
subjectId: 'sub-1',
|
||||||
subjectName: 'Mathématiques',
|
subjectName: 'Mathématiques',
|
||||||
|
subjectColor: null,
|
||||||
teacherId: 'teacher-1',
|
teacherId: 'teacher-1',
|
||||||
teacherName: 'M. Dupont',
|
teacherName: 'M. Dupont',
|
||||||
room: 'Salle 101',
|
room: 'Salle 101',
|
||||||
@@ -100,6 +101,16 @@ describe('ScheduleWidget', () => {
|
|||||||
expect(container.querySelector('.slot-room')).toBeNull();
|
expect(container.querySelector('.slot-room')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies subject color as CSS variable on slot', () => {
|
||||||
|
const slot = makeSlot({ subjectColor: '#e74c3c' });
|
||||||
|
const { container } = render(ScheduleWidget, {
|
||||||
|
props: { slots: [slot], nextSlotId: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = container.querySelector('[data-testid="schedule-slot"]') as HTMLElement;
|
||||||
|
expect(item.style.getPropertyValue('--slot-color')).toBe('#e74c3c');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders multiple slots with data-testid', () => {
|
it('renders multiple slots with data-testid', () => {
|
||||||
const slots = [
|
const slots = [
|
||||||
makeSlot({ slotId: 'slot-1' }),
|
makeSlot({ slotId: 'slot-1' }),
|
||||||
|
|||||||
@@ -79,6 +79,36 @@ export default defineConfig({
|
|||||||
statuses: [0, 200]
|
statuses: [0, 200]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /\/api\/me\/children\/schedule\/summary/,
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'parent-schedule-v1',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60
|
||||||
|
},
|
||||||
|
networkTimeoutSeconds: 5,
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /\/api\/me\/children$/,
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'parent-children-v1',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 7 * 24 * 60 * 60
|
||||||
|
},
|
||||||
|
networkTimeoutSeconds: 5,
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user