Includes BMAD bmb/bmm/cis/tea workflow modules, folder (declaration) feature implementation (controllers, models, enums, views, tests), claude/cursor command configs, and email templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
3.8 KiB
Vue
125 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
|
|
|
|
type Folder = {
|
|
id: number;
|
|
due_date: string | null;
|
|
};
|
|
|
|
type Props = {
|
|
folders: Folder[];
|
|
};
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
const monthNames = [
|
|
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre',
|
|
];
|
|
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
|
|
|
|
const current = ref(new Date());
|
|
|
|
const monthLabel = computed(() =>
|
|
`${monthNames[current.value.getMonth()]} ${current.value.getFullYear()}`,
|
|
);
|
|
|
|
const datesWithFolders = computed(() => {
|
|
const set = new Set<string>();
|
|
props.folders.forEach((f) => {
|
|
if (f.due_date) set.add(f.due_date);
|
|
});
|
|
return set;
|
|
});
|
|
|
|
const foldersByDate = computed(() => {
|
|
const map = new Map<string, number>();
|
|
props.folders.forEach((f) => {
|
|
if (f.due_date) {
|
|
map.set(f.due_date, (map.get(f.due_date) ?? 0) + 1);
|
|
}
|
|
});
|
|
return map;
|
|
});
|
|
|
|
const calendarDays = computed(() => {
|
|
const year = current.value.getFullYear();
|
|
const month = current.value.getMonth();
|
|
const first = new Date(year, month, 1);
|
|
const last = new Date(year, month + 1, 0);
|
|
const startDay = (first.getDay() + 6) % 7;
|
|
const daysInMonth = last.getDate();
|
|
|
|
const days: Array<{ date: Date | null; dateStr: string | null; count: number }> = [];
|
|
|
|
for (let i = 0; i < startDay; i++) {
|
|
days.push({ date: null, dateStr: null, count: 0 });
|
|
}
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
const date = new Date(year, month, d);
|
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
days.push({
|
|
date,
|
|
dateStr,
|
|
count: foldersByDate.value.get(dateStr) ?? 0,
|
|
});
|
|
}
|
|
return days;
|
|
});
|
|
|
|
function prevMonth() {
|
|
current.value = new Date(current.value.getFullYear(), current.value.getMonth() - 1);
|
|
}
|
|
|
|
function nextMonth() {
|
|
current.value = new Date(current.value.getFullYear(), current.value.getMonth() + 1);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rounded-xl border border-sidebar-border/70 bg-card p-4">
|
|
<div class="mb-4 flex items-center justify-between">
|
|
<Button variant="ghost" size="icon" @click="prevMonth">
|
|
<ChevronLeft class="size-4" />
|
|
</Button>
|
|
<span class="text-sm font-medium">{{ monthLabel }}</span>
|
|
<Button variant="ghost" size="icon" @click="nextMonth">
|
|
<ChevronRight class="size-4" />
|
|
</Button>
|
|
</div>
|
|
<div class="grid grid-cols-7 gap-1 text-center text-xs">
|
|
<div
|
|
v-for="day in dayNames"
|
|
:key="day"
|
|
class="py-1 font-medium text-muted-foreground"
|
|
>
|
|
{{ day }}
|
|
</div>
|
|
<div
|
|
v-for="(cell, i) in calendarDays"
|
|
:key="i"
|
|
class="flex min-h-8 flex-col items-center justify-center rounded-md py-1"
|
|
:class="[
|
|
!cell.date
|
|
? 'invisible'
|
|
: cell.count > 0
|
|
? 'bg-primary/15 font-medium'
|
|
: 'text-muted-foreground hover:bg-muted/50',
|
|
]"
|
|
>
|
|
<template v-if="cell.date">
|
|
{{ cell.date.getDate() }}
|
|
<span
|
|
v-if="cell.count > 0"
|
|
class="mt-0.5 text-[10px]"
|
|
>
|
|
{{ cell.count }} dossier{{ cell.count > 1 ? 's' : '' }}
|
|
</span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|