Compare commits

..

11 commits

Author SHA1 Message Date
Christophe Robillard
0a2f26684b fill form mode 2026-03-21 16:17:59 +01:00
Christophe Robillard
a9f72b60af can edit selected day 2026-03-21 16:16:54 +01:00
Christophe Robillard
46e6014fea update day_log 2026-03-20 19:28:54 +01:00
Christophe Robillard
d6fba7e6e8 valid mood unique per day per user 2026-03-20 19:08:26 +01:00
Christophe Robillard
560a9442a7 fix modes factory 2026-03-20 19:08:26 +01:00
Christophe Robillard
93b578c3cf create day_log 2026-03-20 19:08:10 +01:00
Christophe Robillard
5c67b91302 add username to user factory 2026-03-19 21:22:05 +01:00
Christophe Robillard
962915829f add day_log model 2026-03-19 21:22:05 +01:00
Christophe Robillard
497744d165 add image for mode 2026-03-19 19:04:16 +01:00
Christophe Robillard
2ea386e557 show legend based on custom modes 2026-03-15 19:27:36 +01:00
Christophe Robillard
820d31b3e5 replace mood mode string with mode ref 2026-03-15 19:27:25 +01:00
13 changed files with 300 additions and 323 deletions

View file

@ -54,37 +54,13 @@ main {
height: 15px; height: 15px;
} }
.day.has-day-log {
position: relative;
}
.day.has-day-log::after {
content: '';
position: absolute;
bottom: 1px;
right: 1px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: white;
}
.selected-day { .selected-day {
border-radius: 50%; border: 2px double white;
box-shadow: 0 0 0 3px white, 0 0 0 5px black;
margin: 4px; margin: 4px;
width: 15px; width: 15px;
height: 15px; height: 15px;
} }
.empty-day {
border: 1px dashed #aaa;
margin: 4px;
width: 15px;
height: 15px;
background: transparent;
}
.info { .info {
margin: 30px; margin: 30px;
} }

View file

@ -11,7 +11,7 @@ class DayLogsController < ApplicationController
if @day_log.save if @day_log.save
handle_mood(@day_log.day, params[:day_log][:mode_id]) handle_mood(@day_log.day, params[:day_log][:mode_id])
redirect_to dashboard_path(day: @day_log.day) redirect_to root_path
else else
render :new, status: :unprocessable_content render :new, status: :unprocessable_content
end end
@ -20,7 +20,7 @@ class DayLogsController < ApplicationController
def update def update
if @day_log.update(day_log_params) if @day_log.update(day_log_params)
handle_mood(@day_log.day, params[:day_log][:mode_id]) handle_mood(@day_log.day, params[:day_log][:mode_id])
redirect_to dashboard_path(day: @day_log.day) redirect_to dashboard_path
else else
render :edit, status: :unprocessable_content render :edit, status: :unprocessable_content
end end

View file

@ -1,6 +1,5 @@
class MoodsController < ApplicationController class MoodsController < ApplicationController
def index def index
@user = Current.user @user = Current.user
redirect_to dashboard_path(day: Date.today) unless params[:day]
end end
end end

View file

@ -1,19 +1,4 @@
module MoodsHelper module MoodsHelper
def day_status(mood, user)
return :filled if mood[:mode].present?
return :guessed if mood[:guess].present? && user.guess?
return :unknown if mood[:guess].present? && !user.guess?
:empty
end
def mode_for(mood, user)
case day_status(mood, user)
when :filled then mood[:mode]
when :guessed then mood[:guess]
when :unknown, :empty then Mode.new(label: "unknown", color: "white")
end
end
=begin
def mode_for(mood, user) def mode_for(mood, user)
if user.guess? if user.guess?
mood[:mode] || mood[:guess] || { label: "unknown", color: "white", image_url: "unknown.jpg" } mood[:mode] || mood[:guess] || { label: "unknown", color: "white", image_url: "unknown.jpg" }
@ -21,19 +6,10 @@ module MoodsHelper
mood[:mode] || { label: "unknown", color: "white", image_url: "unknown.jpg" } mood[:mode] || { label: "unknown", color: "white", image_url: "unknown.jpg" }
end end
end end
=end
def style_for_mode(mode, status) def style_for_mode(mode)
case status style = "background-color: #{mode[:color]};"
when :empty then "" style += " border: 2px double grey;" if mode[:label] == "unknown"
when :unknown then "background-color: #{mode.color}; border: 2px double grey;" style
else "background-color: #{mode.color};"
end
end
def css_class_for_day(status, day_log)
css_class = status == :empty ? "day empty-day" : "day"
css_class += " has-day-log" if day_log
css_class
end end
end end

View file

@ -1,39 +1,18 @@
import { Controller } from '@hotwired/stimulus' import { Controller } from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
static targets = [ "image", "modeDay", "dayLogLink", "dayLogInfo" ]; static targets = [ "image", "modeDay", "dayLogLink" ];
connect() {
this.lastTarget = null;
const params = new URLSearchParams(window.location.search);
const selectedDay = params.get("day");
if (selectedDay) {
const dayEl = document.querySelector(`[data-day="${selectedDay}"]`);
if (dayEl) {
dayEl.click();
dayEl.scrollIntoView({ block: "center" });
}
} else {
const logs = document.querySelector(".logs");
if (logs) logs.scrollTop = logs.scrollHeight;
}
}
updateDayInfo(event) { updateDayInfo(event) {
const image = this.imageTarget; const image = this.imageTarget;
const modeDay = this.modeDayTarget; const modeDay = this.modeDayTarget;
const day = event.target.dataset.day; const day = event.target.dataset.day;
const formattedDay = new Date(day).toLocaleDateString('fr-FR'); const modeDayContent = day + ' : ' + event.target.dataset.mode;
const modeDayContent = formattedDay + ' : ' + event.target.dataset.mode;
image.src = event.target.dataset.image; image.src = event.target.dataset.image;
modeDay.textContent = modeDayContent; modeDay.textContent = modeDayContent;
this.dayLogLinkTarget.href = `/day_logs/edit?day=${day}`;
this.dayLogInfoTarget.textContent = event.target.dataset.info || '';
event.target.className = "selected-day " + event.target.dataset.mode; event.target.className = "selected-day " + event.target.dataset.mode;
history.pushState({}, '', `?day=${day}`); this.dayLogLinkTarget.href = `/day_logs/edit?day=${day}`
if (this.lastTarget) { if (this.lastTarget) {
this.lastTarget.className = "day " + this.lastTarget.dataset.mode; this.lastTarget.className = "day " + this.lastTarget.dataset.mode;
@ -41,4 +20,9 @@ export default class extends Controller {
this.lastTarget = event.target; this.lastTarget = event.target;
} }
connect() {
this.lastTarget = null;
window.location = "#end";
}
} }

View file

@ -1,8 +1,4 @@
class Mode < ApplicationRecord class Mode < ApplicationRecord
belongs_to :user belongs_to :user
has_one_attached :image has_one_attached :image
def image_url
image.attached? ? Rails.application.routes.url_helpers.rails_blob_path(image, only_path: true) : ActionController::Base.helpers.asset_path("unknown.jpg")
end
end end

View file

@ -33,14 +33,10 @@ class User < ApplicationRecord
end end
def history def history
MoodCalendarService.generate_calendar(self) MoodCalendarService.generate_calendar(moods)
end end
def current_mode def current_mode
self.moods.last&.mode || "croisiere" self.moods.last&.mode || "croisiere"
end end
def current_day_log
self.day_logs.find_by(day: Date.today)
end
end end

View file

@ -1,10 +1,26 @@
# app/services/mood_calendar_service.rb
class MoodCalendarService class MoodCalendarService
def self.generate_calendar(user, start_date: nil, end_date: nil) def self.generate_calendar(moods, start_date: nil, end_date: Date.current)
data = user.moods.includes(:mode) # Preload modes with their image attachments
.order(:recorded_at) mode_ids = moods.joins(:mode).pluck("modes.id").uniq
.map { |mood| { mode: mood.mode, recorded_at: mood.recorded_at } } modes_by_id = Mode.where(id: mode_ids)
.with_attached_image
.index_by(&:id)
day_logs_by_date = user.day_logs.index_by(&:day) data = moods.joins(:mode)
.order(:recorded_at)
.pluck(:recorded_at, "modes.label", "modes.color", "modes.id")
.map do |recorded_at, label, color, mode_id|
mode = modes_by_id[mode_id]
{
mode: {
label: label,
color: color,
image_url: mode&.image&.attached? ? Rails.application.routes.url_helpers.rails_blob_url(mode.image, only_path: true) : nil
},
recorded_at: recorded_at
}
end # Convertir la relation ActiveRecord en tableau de hash
if data.empty? if data.empty?
start_date = Date.current start_date = Date.current
@ -12,35 +28,36 @@ class MoodCalendarService
start_date ||= data.first[:recorded_at].to_date start_date ||= data.first[:recorded_at].to_date
end end
start_date = start_date.to_date if end_date < (start_date + 5.months)
end_date = end_date ? end_date.to_date : [ Date.current, start_date + 5.months ].max
=begin
if end_date.nil?
end_date = start_date + 5.months end_date = start_date + 5.months
else
end_date = end_date.to_date
end end
=end
# Convertir en Date si ce sont des DateTime ou Time
start_date = start_date.to_date
end_date = end_date&.to_date
# Grouper par jour et garder le plus récent pour chaque jour
data_by_date = data.group_by { |d| d[:recorded_at].to_date } data_by_date = data.group_by { |d| d[:recorded_at].to_date }
.transform_values { |entries| entries.max_by { |e| e[:recorded_at] } } .transform_values { |entries| entries.max_by { |e| e[:recorded_at] } }
# Trouver le dernier mood avant start_date pour initialiser le guess
last_mode = data.select { |d| d[:recorded_at].to_date < start_date } last_mode = data.select { |d| d[:recorded_at].to_date < start_date }
.max_by { |d| d[:recorded_at] } .max_by { |d| d[:recorded_at] }
&.[](:mode) &.[](:mode)
# Générer le tableau complet avec tous les jours
complete_data = (start_date..end_date).map do |date| complete_data = (start_date..end_date).map do |date|
day_log = day_logs_by_date[date]
if data_by_date[date] if data_by_date[date]
last_mode = data_by_date[date][:mode] last_mode = data_by_date[date][:mode]
data_by_date[date].merge(day_log: day_log) data_by_date[date]
else else
{ mode: nil, recorded_at: date.to_datetime, guess: last_mode, day_log: day_log } { mode: nil, recorded_at: date.to_datetime, guess: last_mode }
end end
end end
# Regrouper par mois avec semaines commençant au premier lundi
complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month } complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month }
.map do |month_start, _| .map do |month_start, month_data|
first_monday = month_start first_monday = month_start
first_monday = first_monday.next_occurring(:monday) unless first_monday.monday? first_monday = first_monday.next_occurring(:monday) unless first_monday.monday?
@ -53,7 +70,7 @@ class MoodCalendarService
data_hash = complete_data.index_by { |d| d[:recorded_at].to_date } data_hash = complete_data.index_by { |d| d[:recorded_at].to_date }
all_days = (first_monday..month_end).map do |date| all_days = (first_monday..month_end).map do |date|
data_hash[date] || { mode: nil, recorded_at: date.to_datetime, guess: nil, day_log: nil } data_hash[date] || { mode: nil, recorded_at: date.to_datetime, guess: nil }
end end
weeks = all_days.group_by { |d| d[:recorded_at].to_date.beginning_of_week(:monday) } weeks = all_days.group_by { |d| d[:recorded_at].to_date.beginning_of_week(:monday) }
@ -66,4 +83,4 @@ class MoodCalendarService
} }
end end
end end
end end#

View file

@ -1,25 +1,20 @@
= form_with model: day_log do |f| = form_with model: day_log do |f|
- @day_log.errors.full_messages.each do |msg| - @day_log.errors.full_messages.each do |msg|
.notification.is-danger= msg %p= msg
.field .field
= f.label :info, "Journal", class: "label" = f.label :day
.control = f.date_field :day
= f.text_area :info, class: "textarea", rows: 5
.field .field
= label_tag :mode_id, "Mode", class: "label" = f.label :info
.control = f.text_area :info
.select.is-fullwidth
.field
= label_tag :mode_id, "Mode"
= select_tag "day_log[mode_id]", = select_tag "day_log[mode_id]",
options_from_collection_for_select(Current.user.modes, :id, :label, @mood&.mode_id), options_from_collection_for_select(Current.user.modes, :id, :label, @mood&.mode_id),
include_blank: true include_blank: true
.field.is-hidden = f.submit
= f.date_field :day
.field.is-grouped
.control
= f.submit "Enregistrer", class: "button is-primary"
.control
= link_to "Annuler", dashboard_path(day: @day_log.day), class: "button is-light"

View file

@ -1,13 +1,3 @@
%main.columns.m-auto %h1 Modifier le journal
.current-day.column.is-hidden-mobile
%section.m-4
%figure.image.has-ratio
= image_tag @mood ? @mood.mode.image_url : "unknown.jpg"
%section.section.column.has-background-primary-light = render 'form', day_log: @day_log
%h1.title.is-4
%span.icon
%i.fa-regular.fa-calendar
%span= l @day_log.day, format: :long
= render 'form', day_log: @day_log

View file

@ -2,7 +2,7 @@
<div class="current-day column is-hidden-mobile"> <div class="current-day column is-hidden-mobile">
<section class="m-4"> <section class="m-4">
<figure class="image has-ratio"> <figure class="image has-ratio">
<%= image_tag(@user.current_mode.image_url, "data-mood-target": "image") %> <%= image_tag(@user.current_mode.image, "data-mood-target": "image") %>
</figure> </figure>
</section> </section>
</div> </div>
@ -14,7 +14,7 @@
</div> </div>
<% @user.modes.each do |mode| %> <% @user.modes.each do |mode| %>
<div class="is-flex is-align-items-center mr-4"> <div class="is-flex is-align-items-center mr-4">
<div class="bar mr-1" style="<%= style_for_mode(mode, :filled) %>"></div> <div class="bar mr-1" style="<%= style_for_mode(mode) %>"></div>
<div class=""><%= mode.label %></div> <div class=""><%= mode.label %></div>
</div> </div>
<% end %> <% end %>
@ -23,11 +23,8 @@
<div class="title is-4"> <div class="title is-4">
<span class="icon"><i class="fa-regular fa-calendar"></i></span> <span class="icon"><i class="fa-regular fa-calendar"></i></span>
<span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mode.label %></span> <span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mode.label %></span>
<%= link_to edit_day_log_for_day_path(day: Date.today), "data-mood-target": "dayLogLink" do %> <%= link_to "Journal", edit_day_log_for_day_path(day: Date.today), "data-mood-target": "dayLogLink" %>
<span class="icon ml-2"><i class="has-text-grey fa-solid fa-pencil fa-xs"></i></span>
<% end %>
</div> </div>
<p data-mood-target="dayLogInfo"><%= @user.current_day_log&.info %></p>
</div> </div>
<div class="logs"> <div class="logs">
@ -39,23 +36,15 @@
<% month[:weeks].each do |week| %> <% month[:weeks].each do |week| %>
<div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3"> <div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3">
<% week.each do |mood| %> <% week.each do |mood| %>
<% status = day_status(mood, @user) %>
<% mode = mode_for(mood, @user) %> <% mode = mode_for(mood, @user) %>
<div data-image="<%= mode.image_url %>" <div data-image="<%= mode[:image_url] %>" data-mode="<%= mode[:label] %>" data-day="<%= l mood[:recorded_at].to_date %>" data-action="click->mood#updateDayInfo" title="<%= l(mood[:recorded_at].to_date) %> : <%= mode[:label] %>" class="day" style="<%= style_for_mode(mode) %>"></div>
data-mode="<%= mode.label %>"
data-day="<%= mood[:recorded_at].to_date.iso8601 %>"
data-info="<%= mood[:day_log]&.info %>"
data-action="click->mood#updateDayInfo"
title="<%= l(mood[:recorded_at].to_date) %> : <%= mode.label %>"
class="<%= css_class_for_day(status, mood[:day_log]) %>"
style="<%= style_for_mode(mode, status) %>">
</div>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %> <% end %>
<div id="end"></>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,15 +1,13 @@
class ReplaceMoodModeStringWithModeReference < ActiveRecord::Migration[8.0] class ReplaceMoodModeStringWithModeReference < ActiveRecord::Migration[8.0]
def up def up
rename_column :moods, :mode, :mode_string
# 1. Ajouter la colonne de référence # 1. Ajouter la colonne de référence
add_reference :moods, :mode, foreign_key: true add_reference :moods, :mode, foreign_key: true
# 2. Pour chaque mood, trouver ou créer le Mode correspondant # 2. Pour chaque mood, trouver ou créer le Mode correspondant
Mood.find_each do |mood| Mood.find_each do |mood|
next if mood.mode_string.blank? next if mood.mode.blank?
mode_record = Mode.find_or_create_by!(label: mood.mode_string, user_id: mood.user_id) do |m| mode_record = Mode.find_or_create_by!(label: mood.mode, user_id: mood.user_id) do |m|
m.color = "#000000" # couleur par défaut m.color = "#000000" # couleur par défaut
end end
@ -17,18 +15,16 @@ class ReplaceMoodModeStringWithModeReference < ActiveRecord::Migration[8.0]
end end
# 3. Supprimer l'ancienne colonne string # 3. Supprimer l'ancienne colonne string
remove_column :moods, :mode_string, :string remove_column :moods, :mode, :string
end end
def down def down
add_column :moods, :mode_string, :string add_column :moods, :mode, :string
Mood.find_each do |mood| Mood.find_each do |mood|
mood.update_column(:mode_string, mood.mode&.label) mood.update_column(:mode, mood.mode&.label)
end end
remove_reference :moods, :mode, foreign_key: true remove_reference :moods, :mode, foreign_key: true
rename_column :moods, :mode_string, :mode
end end
end end

View file

@ -1,231 +1,330 @@
# spec/services/mood_calendar_service_spec.rb
require 'rails_helper' require 'rails_helper'
RSpec.describe MoodCalendarService do RSpec.describe MoodCalendarService do
let(:user) { create(:user) }
let(:mode) { create(:mode, user: user) }
describe '.generate_calendar' do describe '.generate_calendar' do
context 'avec des jours manquants' do context 'avec un recorded_at pour chaque jour' do
it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do it 'génère un calendrier complet sans jours nil' do
mode2 = create(:mode, user: user) # Données du 1er au 7 février 2025 (une semaine complète)
mode3 = create(:mode, user: user) (1..7).each do |day|
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-02-12 14:20:00")) create(:mood, mode: "mood_#{day}", recorded_at: Time.zone.parse("2025-02-0#{day} 10:00:00"))
create(:mood, user: user, mode: mode3, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) end
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-18 16:45:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-02-28") end_date: Date.parse("2025-02-28")
) )
expect(result.length).to be >= 1 expect(result.length).to eq(1) # Un seul mois
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be true expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten # Première semaine commence le lundi 3 février
first_week = result[0][:weeks][0]
expect(first_week.length).to eq(7) # Du lundi 3 au dimanche 9
# Vérifier que tous les jours du 3 au 7 ont un mode
(3..7).each do |day|
mood = first_week.find { |m| m[:recorded_at].day == day }
expect(mood[:mode]).to eq("mood_#{day}")
expect(mood).not_to have_key(:guess)
end
# Le 8 et 9 février devraient avoir mode: nil avec guess: "mood_7"
mood_8 = first_week.find { |m| m[:recorded_at].day == 8 }
expect(mood_8[:mode]).to be_nil
expect(mood_8[:guess]).to eq("mood_7")
mood_9 = first_week.find { |m| m[:recorded_at].day == 9 }
expect(mood_9[:mode]).to be_nil
expect(mood_9[:guess]).to eq("mood_7")
end
end
context 'avec des jours manquants' do
it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-02-18 16:45:00"))
moods = Mood.all
result = MoodCalendarService.generate_calendar(
moods,
end_date: Date.parse("2025-02-28")
)
expect(result.length).to eq(1)
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
# Trouver tous les jours
all_days = result[0][:weeks].flatten
# Le 12 février devrait avoir le dernier mood (content à 14:20)
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") } mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
expect(mood_12[:mode]).to eq(mode2) expect(mood_12[:mode]).to eq("content")
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i) expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
# Le 13 février devrait être nil avec guess: "content"
mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") } mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") }
expect(mood_13[:mode]).to be_nil expect(mood_13[:mode]).to be_nil
expect(mood_13[:guess]).to eq(mode2) expect(mood_13[:guess]).to eq("content")
# Le 14 février devrait être nil avec guess: "content"
mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") } mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") }
expect(mood_14[:mode]).to be_nil expect(mood_14[:mode]).to be_nil
expect(mood_14[:guess]).to eq(mode2) expect(mood_14[:guess]).to eq("content")
# Le 15 février devrait avoir le mood "heureux"
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") } mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(mood_15[:mode]).to eq(mode3) expect(mood_15[:mode]).to eq("heureux")
# Le 16 février devrait être nil avec guess: "heureux"
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") } mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
expect(mood_16[:mode]).to be_nil expect(mood_16[:mode]).to be_nil
expect(mood_16[:guess]).to eq(mode3) expect(mood_16[:guess]).to eq("heureux")
# Les jours avant le 12 (à partir du premier lundi 3 février) devraient avoir guess: nil
mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") } mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") }
expect(mood_3[:mode]).to be_nil expect(mood_3[:mode]).to be_nil
expect(mood_3[:guess]).to be_nil expect(mood_3[:guess]).to be_nil
end end
it 'gère correctement plusieurs mois avec des jours manquants' do it 'gère correctement plusieurs mois avec des jours manquants' do
mode2 = create(:mode, user: user) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "joyeux", recorded_at: Time.zone.parse("2025-04-20 11:30:00"))
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-04-20 11:30:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-04-30") end_date: Date.parse("2025-04-30")
) )
expect(result.length).to be >= 3 # Devrait avoir exactement février, mars et avril
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be true expect(result.length).to eq(3)
expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be true
expect(result.any? { |m| m[:month] == Date.parse("2025-04-01") }).to be true
# Vérifier février
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
expect(february).not_to be_nil
# Vérifier mars (tous les jours devraient avoir mode: nil et guess: "heureux")
march = result.find { |m| m[:month] == Date.parse("2025-03-01") } march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
expect(march).not_to be_nil
all_march_days = march[:weeks].flatten all_march_days = march[:weeks].flatten
# Tous les jours de mars (y compris ceux qui débordent début avril) devraient avoir mode: nil
expect(all_march_days.all? { |d| d[:mode].nil? }).to be true expect(all_march_days.all? { |d| d[:mode].nil? }).to be true
# Tous les jours de mars devraient avoir guess: "heureux"
# (le mois de mars va du 3 mars au 6 avril, avant le mood du 20 avril)
all_march_days.each do |day| all_march_days.each do |day|
expect(day[:guess]).to be_present, expect(day[:guess]).to eq("heureux"),
"Le jour #{day[:recorded_at].to_date} devrait avoir un guess" "Le jour #{day[:recorded_at].to_date} devrait avoir guess: 'heureux' mais a: #{day[:guess].inspect}"
end end
# Vérifier avril
april = result.find { |m| m[:month] == Date.parse("2025-04-01") } april = result.find { |m| m[:month] == Date.parse("2025-04-01") }
expect(april).not_to be_nil
all_april_days = april[:weeks].flatten all_april_days = april[:weeks].flatten
mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") } mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") }
expect(mood_20_april[:mode]).to eq(mode2) expect(mood_20_april[:mode]).to eq("joyeux")
# Les jours du 7 au 19 avril devraient avoir guess: "heureux"
mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") } mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") }
expect(mood_10_april[:guess]).to eq(mode) expect(mood_10_april[:guess]).to eq("heureux")
# Les jours après le 20 avril devraient avoir guess: "joyeux"
mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") } mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") }
expect(mood_21_april[:guess]).to eq(mode2) expect(mood_21_april[:guess]).to eq("joyeux")
end end
end end
context 'sans aucun recorded_at' do context 'sans aucun recorded_at' do
it 'retourne un calendrier à partir de la date courante' do it 'retourne un tableau vide' do
result = MoodCalendarService.generate_calendar(user) result = MoodCalendarService.generate_calendar(Mood.none)
expect(result).not_to be_empty expect(result).to eq([])
expect(result.first[:month]).to eq(Date.current.beginning_of_month) end
end
context 'avec plusieurs moods le même jour' do
it 'garde uniquement le mood le plus récent' do
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
create(:mood, mode: "neutre", recorded_at: Time.zone.parse("2025-02-12 12:00:00"))
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
create(:mood, mode: "fatigué", recorded_at: Time.zone.parse("2025-02-12 09:15:00"))
moods = Mood.all
result = MoodCalendarService.generate_calendar(
moods,
end_date: Date.parse("2025-02-28")
)
all_days = result[0][:weeks].flatten
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
expect(mood_12[:mode]).to eq("content")
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
end end
end end
context 'structure du calendrier' do context 'structure du calendrier' do
it 'commence chaque mois par le premier lundi' do it 'commence chaque mois par le premier lundi' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-02-28") end_date: Date.parse("2025-02-28")
) )
february = result[0] february = result[0]
first_day = february[:weeks][0][0] first_day = february[:weeks][0][0]
# Le premier jour de février 2025 devrait être le lundi 3 février
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03")) expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
expect(first_day[:recorded_at].to_date.monday?).to be true expect(first_day[:recorded_at].to_date.monday?).to be true
end end
it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-03-31") end_date: Date.parse("2025-03-31")
) )
february = result[0] february = result[0]
all_days = february[:weeks].flatten all_days = february[:weeks].flatten
# Premier jour : lundi 3 février 2025
first_day = all_days.first first_day = all_days.first
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03")) expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
expect(first_day[:recorded_at].to_date.monday?).to be true expect(first_day[:recorded_at].to_date.monday?).to be true
# Dernier jour : dimanche 2 mars 2025 (veille du premier lundi de mars qui est le 3)
last_day = all_days.last last_day = all_days.last
expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02")) expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
expect(last_day[:recorded_at].to_date.sunday?).to be true expect(last_day[:recorded_at].to_date.sunday?).to be true
# Le 3 mars (premier lundi de mars) ne devrait PAS être dans février
expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false
end end
it 'toutes les semaines ont exactement 7 jours' do it 'toutes les semaines ont exactement 7 jours' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-02-28") end_date: Date.parse("2025-02-28")
) )
february = result[0] february = result[0]
# Toutes les semaines devraient avoir exactement 7 jours
february[:weeks].each do |week| february[:weeks].each do |week|
expect(week.length).to eq(7) expect(week.length).to eq(7)
end end
end end
it 'retourne month comme Date (le premier du mois)' do it 'retourne month comme Date (le premier du mois)' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-02-28") end_date: Date.parse("2025-02-28")
) )
february = result[0] february = result[0]
# month devrait être une Date
expect(february[:month]).to be_a(Date) expect(february[:month]).to be_a(Date)
expect(february[:month]).to eq(Date.parse("2025-02-01")) expect(february[:month]).to eq(Date.parse("2025-02-01"))
expect(february[:month].day).to eq(1) expect(february[:month].day).to eq(1) # Premier du mois
end end
end end
context 'avec paramètres par défaut' do context 'avec paramètres par défaut' do
it "utilise le premier mood comme start_date et aujourd'hui comme end_date" do it 'utilise le premier mood comme start_date et aujourd\'hui comme end_date' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(user) result = MoodCalendarService.generate_calendar(Mood.all)
expect(result).not_to be_empty expect(result).not_to be_empty
# Devrait commencer en février
expect(result.first[:month]).to eq(Date.parse("2025-02-01")) expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
end end
end end
context 'avec start_date spécifié' do context 'avec start_date spécifié' do
it 'commence à la date spécifiée' do it 'commence à la date spécifiée' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
start_date: Date.parse("2025-03-01"), start_date: Date.parse("2025-03-01"),
end_date: Date.parse("2025-03-31") end_date: Date.parse("2025-03-31")
) )
# Devrait commencer en mars même si le mood est en février
expect(result.first[:month]).to eq(Date.parse("2025-03-01")) expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
# Ne devrait pas contenir février
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
all_march_days = result.first[:weeks].flatten
puts all_march_days.inspect
expect(all_march_days.all? { |d| d[:guess] == "heureux" }).to be true
end end
end end
context 'avec end_date spécifié' do context 'avec end_date spécifié' do
it 'se termine à end_date' do it 'se termine à la date spécifiée' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-09-30") end_date: Date.parse("2025-02-28")
) )
expect(result.last[:month]).to eq(Date.parse("2025-09-01")) # Devrait s'arrêter en février
expect(result.last[:month]).to eq(Date.parse("2025-02-01"))
# Ne devrait pas contenir mars ou au-delà
expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be false
end end
end end
context 'avec start_date et end_date spécifiés' do context 'avec start_date et end_date spécifiés' do
it 'génère le calendrier pour la plage spécifiée' do it 'génère le calendrier pour la plage spécifiée' do
mode2 = create(:mode, user: user) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-04-20 10:00:00"))
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-04-20 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
start_date: Date.parse("2025-03-01"), start_date: Date.parse("2025-03-01"),
end_date: Date.parse("2025-03-31") end_date: Date.parse("2025-03-31")
) )
# Devrait contenir uniquement mars
expect(result.length).to eq(1) expect(result.length).to eq(1)
expect(result.first[:month]).to eq(Date.parse("2025-03-01")) expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
all_days = result.first[:weeks].flatten all_days = result.first[:weeks].flatten
expect(all_days.all? { |d| d[:mode].nil? }).to be true expect(all_days.all? { |d| d[:mode].nil? }).to be true
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
end end
it 'gère une plage de plusieurs mois' do it 'gère une plage de plusieurs mois' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
start_date: Date.parse("2025-02-01"), start_date: Date.parse("2025-02-01"),
end_date: Date.parse("2025-04-30") end_date: Date.parse("2025-04-30")
) )
# Devrait contenir février, mars et avril
months = result.map { |m| m[:month] } months = result.map { |m| m[:month] }
expect(months).to include( expect(months).to include(
Date.parse("2025-02-01"), Date.parse("2025-02-01"),
@ -235,10 +334,11 @@ RSpec.describe MoodCalendarService do
end end
it 'fonctionne avec DateTime en paramètres' do it 'fonctionne avec DateTime en paramètres' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
# Passer des DateTime au lieu de Date
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
start_date: Time.zone.parse("2025-02-01 00:00:00"), start_date: Time.zone.parse("2025-02-01 00:00:00"),
end_date: Time.zone.parse("2025-02-28 23:59:59") end_date: Time.zone.parse("2025-02-28 23:59:59")
) )
@ -250,109 +350,72 @@ RSpec.describe MoodCalendarService do
context 'avec une plage ne contenant aucun mood' do context 'avec une plage ne contenant aucun mood' do
it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
start_date: Date.parse("2025-04-01"), start_date: Date.parse("2025-04-01"),
end_date: Date.parse("2025-04-30") end_date: Date.parse("2025-04-30")
) )
# Devrait contenir avril
expect(result.first[:month]).to eq(Date.parse("2025-04-01")) expect(result.first[:month]).to eq(Date.parse("2025-04-01"))
# Tous les jours devraient avoir mode: nil et guess: "heureux"
all_days = result.first[:weeks].flatten all_days = result.first[:weeks].flatten
expect(all_days.all? { |d| d[:mode].nil? }).to be true expect(all_days.all? { |d| d[:mode].nil? }).to be true
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
end end
end end
context 'avec plusieurs mois' do context 'avec plusieurs mois' do
it 'chaque mois commence et finit correctement' do it 'chaque mois commence et finit correctement' do
mode2 = create(:mode, user: user) create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00")) create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-03-20 10:00:00"))
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-03-20 10:00:00"))
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, Mood.all,
end_date: Date.parse("2025-03-31") end_date: Date.parse("2025-03-31")
) )
february = result.find { |m| m[:month] == Date.parse("2025-02-01") } february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
march = result.find { |m| m[:month] == Date.parse("2025-03-01") } march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
# Février se termine le dimanche 2 mars
last_day_february = february[:weeks].flatten.last last_day_february = february[:weeks].flatten.last
expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02")) expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
# Mars commence le lundi 3 mars
first_day_march = march[:weeks].flatten.first first_day_march = march[:weeks].flatten.first
expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03")) expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03"))
# Pas de chevauchement
expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date) expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date)
end end
end end
context 'avec un scope spécifique' do context 'avec un scope spécifique' do
it 'fonctionne avec des moods filtrés par user' do it 'fonctionne avec des moods filtrés par user' do
user1 = create(:user)
user2 = create(:user) user2 = create(:user)
mode2 = create(:mode, user: user2)
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, user: user2, mode: mode2, recorded_at: Time.zone.parse("2025-02-16 10:00:00"))
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"), user: user1)
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-16 10:00:00"), user: user2)
moods = Mood.where(user: user1)
result = MoodCalendarService.generate_calendar( result = MoodCalendarService.generate_calendar(
user, moods,
end_date: Date.parse("2025-02-28") end_date: Date.parse("2025-02-28")
) )
all_days = result[0][:weeks].flatten all_days = result[0][:weeks].flatten
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") } mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(mood_15[:mode]).to eq(mode) expect(mood_15[:mode]).to eq("heureux")
# Le mood du user2 ne devrait pas apparaître
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") } mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
expect(mood_16[:mode]).to be_nil expect(mood_16[:mode]).to be_nil
expect(mood_16[:guess]).to eq(mode) expect(mood_16[:guess]).to eq("heureux")
end
end
context 'avec day_log' do
it 'inclut le day_log dans les données du jour correspondant' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
day_log = create(:day_log, user: user, day: Date.parse("2025-02-15"), info: "Une belle journée")
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
day_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(day_15[:day_log]).to eq(day_log)
end
it 'retourne nil pour day_log quand il n\'en existe pas pour ce jour' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
day_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(day_15[:day_log]).to be_nil
end
it 'inclut le day_log pour un jour sans mood' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
day_log = create(:day_log, user: user, day: Date.parse("2025-02-16"), info: "Jour sans mood")
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
day_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
expect(day_16[:mode]).to be_nil
expect(day_16[:day_log]).to eq(day_log)
end
it 'inclut le day_log pour un jour avec mood' do
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
day_log = create(:day_log, user: user, day: Date.parse("2025-02-15"), info: "Jour avec mood")
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
day_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(day_15[:mode]).to eq(mode)
expect(day_15[:day_log]).to eq(day_log)
end end
end end
end end