Files
personotes/internal/api/daily_notes.go
2025-11-12 17:16:13 +01:00

449 lines
12 KiB
Go

package api
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// DailyNoteInfo contient les métadonnées d'une daily note
type DailyNoteInfo struct {
Date time.Time
Path string
Exists bool
Title string
DayOfWeek string
DayOfMonth int
}
// CalendarDay représente un jour dans le calendrier
type CalendarDay struct {
Day int
Date time.Time
HasNote bool
IsToday bool
NotePath string
InMonth bool // Indique si le jour appartient au mois affiché
}
// CalendarData contient les données pour le template du calendrier
type CalendarData struct {
Year int
Month time.Month
MonthName string
Weeks [][7]CalendarDay
PrevMonth string // Format: YYYY-MM
NextMonth string // Format: YYYY-MM
CurrentMonth string // Format: YYYY-MM
}
// getDailyNotePath retourne le chemin d'une daily note pour une date donnée
// Format: notes/daily/2025/01/11.md
func (h *Handler) getDailyNotePath(date time.Time) string {
year := date.Format("2006")
month := date.Format("01")
day := date.Format("02")
relativePath := filepath.Join("daily", year, month, fmt.Sprintf("%s.md", day))
return relativePath
}
// getDailyNoteAbsolutePath retourne le chemin absolu d'une daily note
func (h *Handler) getDailyNoteAbsolutePath(date time.Time) string {
relativePath := h.getDailyNotePath(date)
return filepath.Join(h.notesDir, relativePath)
}
// dailyNoteExists vérifie si une daily note existe pour une date donnée
func (h *Handler) dailyNoteExists(date time.Time) bool {
absPath := h.getDailyNoteAbsolutePath(date)
_, err := os.Stat(absPath)
return err == nil
}
// createDailyNote crée une daily note avec un template par défaut
func (h *Handler) createDailyNote(date time.Time) error {
absPath := h.getDailyNoteAbsolutePath(date)
// Créer les dossiers parents si nécessaire
dir := filepath.Dir(absPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("impossible de créer les dossiers: %w", err)
}
// Vérifier si le fichier existe déjà
if _, err := os.Stat(absPath); err == nil {
return nil // Fichier existe déjà, ne pas écraser
}
// Formatter les dates
dateStr := date.Format("02-01-2006")
dateTimeStr := date.Format("02-01-2006:15:04")
// Noms des jours en français
dayNames := map[time.Weekday]string{
time.Monday: "Lundi",
time.Tuesday: "Mardi",
time.Wednesday: "Mercredi",
time.Thursday: "Jeudi",
time.Friday: "Vendredi",
time.Saturday: "Samedi",
time.Sunday: "Dimanche",
}
// Noms des mois en français
monthNames := map[time.Month]string{
time.January: "janvier",
time.February: "février",
time.March: "mars",
time.April: "avril",
time.May: "mai",
time.June: "juin",
time.July: "juillet",
time.August: "août",
time.September: "septembre",
time.October: "octobre",
time.November: "novembre",
time.December: "décembre",
}
dayName := dayNames[date.Weekday()]
monthName := monthNames[date.Month()]
// Template de la daily note
template := fmt.Sprintf(`---
title: "Daily Note - %s"
date: "%s"
last_modified: "%s"
tags: [daily]
---
# 📅 %s %d %s %d
## 🎯 Objectifs du jour
-
## 📝 Notes
-
## ✅ Accompli
-
## 💭 Réflexions
-
## 🔗 Liens
-
`, date.Format("2006-01-02"), dateStr, dateTimeStr, dayName, date.Day(), monthName, date.Year())
// Écrire le fichier
if err := os.WriteFile(absPath, []byte(template), 0644); err != nil {
return fmt.Errorf("impossible d'écrire le fichier: %w", err)
}
return nil
}
// handleDailyToday gère GET /api/daily/today - Ouvre ou crée la note du jour
func (h *Handler) handleDailyToday(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
today := time.Now()
// Créer la note si elle n'existe pas
if !h.dailyNoteExists(today) {
if err := h.createDailyNote(today); err != nil {
h.logger.Printf("Erreur création daily note: %v", err)
http.Error(w, "Erreur lors de la création de la note", http.StatusInternalServerError)
return
}
// Déclencher la ré-indexation
go h.idx.Load(h.notesDir)
}
// Rediriger vers l'endpoint normal de note
notePath := h.getDailyNotePath(today)
http.Redirect(w, r, "/api/notes/"+notePath, http.StatusSeeOther)
}
// handleDailyDate gère GET /api/daily/{YYYY-MM-DD} - Ouvre ou crée la note d'une date spécifique
func (h *Handler) handleDailyDate(w http.ResponseWriter, r *http.Request, dateStr string) {
if r.Method != http.MethodGet {
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
// Parser la date (format YYYY-MM-DD)
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
http.Error(w, "Format de date invalide (attendu: YYYY-MM-DD)", http.StatusBadRequest)
return
}
// Créer la note si elle n'existe pas
if !h.dailyNoteExists(date) {
if err := h.createDailyNote(date); err != nil {
h.logger.Printf("Erreur création daily note: %v", err)
http.Error(w, "Erreur lors de la création de la note", http.StatusInternalServerError)
return
}
// Déclencher la ré-indexation
go h.idx.Load(h.notesDir)
}
// Rediriger vers l'endpoint normal de note
notePath := h.getDailyNotePath(date)
http.Redirect(w, r, "/api/notes/"+notePath, http.StatusSeeOther)
}
// handleDailyCalendar gère GET /api/daily/calendar/{YYYY}/{MM} - Retourne le HTML du calendrier
func (h *Handler) handleDailyCalendar(w http.ResponseWriter, r *http.Request, yearStr, monthStr string) {
if r.Method != http.MethodGet {
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
// Si ce n'est pas une requête HTMX, rediriger vers la page principale
if r.Header.Get("HX-Request") == "" {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Parser année et mois
year, err := strconv.Atoi(yearStr)
if err != nil || year < 1900 || year > 2100 {
http.Error(w, "Année invalide", http.StatusBadRequest)
return
}
month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 {
http.Error(w, "Mois invalide", http.StatusBadRequest)
return
}
// Créer les données du calendrier
calendarData := h.buildCalendarData(year, time.Month(month))
// Rendre le template
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "daily-calendar.html", calendarData); err != nil {
h.logger.Printf("Erreur template calendrier: %v", err)
http.Error(w, "Erreur de rendu", http.StatusInternalServerError)
}
}
// buildCalendarData construit les données du calendrier pour un mois donné
func (h *Handler) buildCalendarData(year int, month time.Month) *CalendarData {
// Premier jour du mois
firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
// Dernier jour du mois
lastDay := firstDay.AddDate(0, 1, -1)
// Date d'aujourd'hui
today := time.Now()
// Noms des mois en français
monthNames := map[time.Month]string{
time.January: "Janvier",
time.February: "Février",
time.March: "Mars",
time.April: "Avril",
time.May: "Mai",
time.June: "Juin",
time.July: "Juillet",
time.August: "Août",
time.September: "Septembre",
time.October: "Octobre",
time.November: "Novembre",
time.December: "Décembre",
}
data := &CalendarData{
Year: year,
Month: month,
MonthName: monthNames[month],
Weeks: make([][7]CalendarDay, 0),
}
// Calculer mois précédent et suivant
prevMonth := firstDay.AddDate(0, -1, 0)
nextMonth := firstDay.AddDate(0, 1, 0)
data.PrevMonth = fmt.Sprintf("%d-%02d", prevMonth.Year(), prevMonth.Month())
data.NextMonth = fmt.Sprintf("%d-%02d", nextMonth.Year(), nextMonth.Month())
data.CurrentMonth = fmt.Sprintf("%d-%02d", year, month)
// Construire les semaines
// Lundi = 0, Dimanche = 6
var week [7]CalendarDay
weekDay := 0
// Jour de la semaine du premier jour (convertir : Dimanche=0 → Lundi=0)
firstWeekday := int(firstDay.Weekday())
if firstWeekday == 0 {
firstWeekday = 7 // Dimanche devient 7
}
firstWeekday-- // Maintenant Lundi=0
// Remplir les jours avant le premier du mois (mois précédent)
prevMonthLastDay := firstDay.AddDate(0, 0, -1)
for i := 0; i < firstWeekday; i++ {
daysBack := firstWeekday - i
date := prevMonthLastDay.AddDate(0, 0, -daysBack+1)
week[i] = CalendarDay{
Day: date.Day(),
Date: date,
HasNote: h.dailyNoteExists(date),
IsToday: isSameDay(date, today),
InMonth: false,
}
}
weekDay = firstWeekday
// Remplir les jours du mois
for day := 1; day <= lastDay.Day(); day++ {
date := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
week[weekDay] = CalendarDay{
Day: day,
Date: date,
HasNote: h.dailyNoteExists(date),
IsToday: isSameDay(date, today),
NotePath: h.getDailyNotePath(date),
InMonth: true,
}
weekDay++
if weekDay == 7 {
data.Weeks = append(data.Weeks, week)
week = [7]CalendarDay{}
weekDay = 0
}
}
// Remplir les jours après le dernier du mois (mois suivant)
if weekDay > 0 {
nextMonthDay := 1
for weekDay < 7 {
date := time.Date(year, month+1, nextMonthDay, 0, 0, 0, 0, time.Local)
week[weekDay] = CalendarDay{
Day: nextMonthDay,
Date: date,
HasNote: h.dailyNoteExists(date),
IsToday: isSameDay(date, today),
InMonth: false,
}
weekDay++
nextMonthDay++
}
data.Weeks = append(data.Weeks, week)
}
return data
}
// handleDailyRecent gère GET /api/daily/recent - Retourne les 7 dernières daily notes
func (h *Handler) handleDailyRecent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
// Si ce n'est pas une requête HTMX, rediriger vers la page principale
if r.Header.Get("HX-Request") == "" {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Chercher les daily notes des 14 derniers jours (au cas où certaines manquent)
recentNotes := make([]*DailyNoteInfo, 0, 7)
today := time.Now()
for i := 0; i < 14 && len(recentNotes) < 7; i++ {
date := today.AddDate(0, 0, -i)
if h.dailyNoteExists(date) {
dayNames := map[time.Weekday]string{
time.Monday: "Lun",
time.Tuesday: "Mar",
time.Wednesday: "Mer",
time.Thursday: "Jeu",
time.Friday: "Ven",
time.Saturday: "Sam",
time.Sunday: "Dim",
}
info := &DailyNoteInfo{
Date: date,
Path: h.getDailyNotePath(date),
Exists: true,
Title: date.Format("02/01/2006"),
DayOfWeek: dayNames[date.Weekday()],
DayOfMonth: date.Day(),
}
recentNotes = append(recentNotes, info)
}
}
// Rendre le template
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "daily-recent.html", map[string]interface{}{
"Notes": recentNotes,
}); err != nil {
h.logger.Printf("Erreur template notes récentes: %v", err)
http.Error(w, "Erreur de rendu", http.StatusInternalServerError)
}
}
// isSameDay vérifie si deux dates sont le même jour
func isSameDay(d1, d2 time.Time) bool {
y1, m1, day1 := d1.Date()
y2, m2, day2 := d2.Date()
return y1 == y2 && m1 == m2 && day1 == day2
}
// handleDaily route les requêtes /api/daily/*
func (h *Handler) handleDaily(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/daily")
path = strings.TrimPrefix(path, "/")
// /api/daily/today
if path == "today" || path == "" {
h.handleDailyToday(w, r)
return
}
// /api/daily/recent
if path == "recent" {
h.handleDailyRecent(w, r)
return
}
// /api/daily/calendar/{YYYY}/{MM}
if strings.HasPrefix(path, "calendar/") {
parts := strings.Split(strings.TrimPrefix(path, "calendar/"), "/")
if len(parts) == 2 {
h.handleDailyCalendar(w, r, parts[0], parts[1])
return
}
}
// /api/daily/{YYYY-MM-DD}
if len(path) == 10 && path[4] == '-' && path[7] == '-' {
h.handleDailyDate(w, r, path)
return
}
http.NotFound(w, r)
}