From 29d7eff20372aaa22a75c42c13262f5b76969cb1 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 08:20:08 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20composable=20de=20matrice=20mensuell?= =?UTF-8?q?e=20avec=20n=C2=B0=20de=20semaine=20ISO=20(#MUI-33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../date/composables/useMonthMatrix.test.ts | 69 +++++++++++++++++++ .../malio/date/composables/useMonthMatrix.ts | 60 ++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 app/components/malio/date/composables/useMonthMatrix.test.ts create mode 100644 app/components/malio/date/composables/useMonthMatrix.ts diff --git a/app/components/malio/date/composables/useMonthMatrix.test.ts b/app/components/malio/date/composables/useMonthMatrix.test.ts new file mode 100644 index 0000000..edb55a9 --- /dev/null +++ b/app/components/malio/date/composables/useMonthMatrix.test.ts @@ -0,0 +1,69 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest' +import {ref} from 'vue' +import {useMonthMatrix} from './useMonthMatrix' + +describe('useMonthMatrix', () => { + it('always produces 6 weeks of 7 days', () => { + const {weeks} = useMonthMatrix(ref(4), ref(2026)) // mai 2026 + expect(weeks.value).toHaveLength(6) + weeks.value.forEach(week => expect(week.days).toHaveLength(7)) + }) + + it('starts every week on a Monday', () => { + const {weeks} = useMonthMatrix(ref(4), ref(2026)) + weeks.value.forEach(week => { + const first = new Date(`${week.days[0].isoDate}T00:00:00`) + expect(first.getDay()).toBe(1) // 1 = lundi + }) + }) + + it('flags exactly the days of the current month', () => { + const {weeks} = useMonthMatrix(ref(4), ref(2026)) // mai = 31 jours + const currentMonthDays = weeks.value + .flatMap(w => w.days) + .filter(d => d.isCurrentMonth) + expect(currentMonthDays).toHaveLength(31) + expect(currentMonthDays.every(d => d.isoDate.startsWith('2026-05'))).toBe(true) + }) + + it('handles leap year February (29 days)', () => { + const {weeks} = useMonthMatrix(ref(1), ref(2024)) // février 2024 + const days = weeks.value.flatMap(w => w.days).filter(d => d.isCurrentMonth) + expect(days).toHaveLength(29) + }) + + it('assigns ISO week 1 to the week containing Jan 4th', () => { + const {weeks} = useMonthMatrix(ref(0), ref(2026)) // janvier 2026 + const weekWithJan4 = weeks.value.find(w => + w.days.some(d => d.isoDate === '2026-01-04'), + ) + expect(weekWithJan4?.weekNumber).toBe(1) + }) + + it('reacts to month/year changes', () => { + const month = ref(4) + const year = ref(2026) + const {weeks} = useMonthMatrix(month, year) + const mayCount = weeks.value.flatMap(w => w.days).filter(d => d.isCurrentMonth).length + month.value = 1 // février + year.value = 2024 + const febCount = weeks.value.flatMap(w => w.days).filter(d => d.isCurrentMonth).length + expect(mayCount).toBe(31) + expect(febCount).toBe(29) + }) + + describe('isToday', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026 + }) + afterEach(() => vi.useRealTimers()) + + it('flags only today', () => { + const {weeks} = useMonthMatrix(ref(4), ref(2026)) + const todays = weeks.value.flatMap(w => w.days).filter(d => d.isToday) + expect(todays).toHaveLength(1) + expect(todays[0].isoDate).toBe('2026-05-19') + }) + }) +}) diff --git a/app/components/malio/date/composables/useMonthMatrix.ts b/app/components/malio/date/composables/useMonthMatrix.ts new file mode 100644 index 0000000..893c23b --- /dev/null +++ b/app/components/malio/date/composables/useMonthMatrix.ts @@ -0,0 +1,60 @@ +import {computed, type ComputedRef, type Ref} from 'vue' + +export type DayCell = { + isoDate: string + day: number + isCurrentMonth: boolean + isToday: boolean +} +export type WeekRow = { + weekNumber: number + days: DayCell[] +} + +const toIso = (d: Date): string => { + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +const isoWeek = (d: Date): number => { + const target = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())) + const dayNum = target.getUTCDay() || 7 // dimanche = 7 + target.setUTCDate(target.getUTCDate() + 4 - dayNum) // jeudi de la semaine + const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1)) + return Math.ceil((((target.getTime() - yearStart.getTime()) / 86400000) + 1) / 7) +} + +export function useMonthMatrix( + month: Ref, + year: Ref, +): {weeks: ComputedRef} { + const weeks = computed(() => { + const todayIso = toIso(new Date()) + const first = new Date(year.value, month.value, 1) + // recule jusqu'au lundi (getDay : 0 = dimanche) + const offset = (first.getDay() + 6) % 7 + const start = new Date(year.value, month.value, 1 - offset) + + const rows: WeekRow[] = [] + const cursor = new Date(start) + for (let w = 0; w < 6; w++) { + const days: DayCell[] = [] + for (let i = 0; i < 7; i++) { + const iso = toIso(cursor) + days.push({ + isoDate: iso, + day: cursor.getDate(), + isCurrentMonth: cursor.getMonth() === month.value, + isToday: iso === todayIso, + }) + cursor.setDate(cursor.getDate() + 1) + } + rows.push({weekNumber: isoWeek(new Date(`${days[0].isoDate}T00:00:00`)), days}) + } + return rows + }) + + return {weeks} +}