mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-03-16 16:42:08 -05:00
Fix date inconsistencies in Favorite Days and Reading Session Timeline charts (#2096)
Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
+2
-4
@@ -34,16 +34,15 @@ public class UserStatsController {
|
||||
@Operation(summary = "Get reading session timeline for a week", description = "Returns reading sessions grouped by book for calendar timeline view")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Timeline data retrieved successfully"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid week, month, or year"),
|
||||
@ApiResponse(responseCode = "400", description = "Invalid week or year"),
|
||||
@ApiResponse(responseCode = "401", description = "Unauthorized")
|
||||
})
|
||||
@GetMapping("/timeline")
|
||||
@PreAuthorize("@securityUtil.canAccessUserStats() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<List<ReadingSessionTimelineResponse>> getTimelineForWeek(
|
||||
@RequestParam int year,
|
||||
@RequestParam int month,
|
||||
@RequestParam int week) {
|
||||
List<ReadingSessionTimelineResponse> timelineData = readingSessionService.getSessionTimelineForWeek(year, month, week);
|
||||
List<ReadingSessionTimelineResponse> timelineData = readingSessionService.getSessionTimelineForWeek(year, week);
|
||||
return ResponseEntity.ok(timelineData);
|
||||
}
|
||||
|
||||
@@ -111,4 +110,3 @@ public class UserStatsController {
|
||||
return ResponseEntity.ok(timeline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-6
@@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@@ -36,17 +37,14 @@ public interface ReadingSessionRepository extends JpaRepository<ReadingSessionEn
|
||||
FROM ReadingSessionEntity rs
|
||||
JOIN rs.book b
|
||||
WHERE rs.user.id = :userId
|
||||
AND YEAR(rs.startTime) = :year
|
||||
AND MONTH(rs.startTime) = :month
|
||||
AND WEEK(rs.startTime) = :week
|
||||
AND rs.startTime >= :startOfWeek AND rs.startTime < :endOfWeek
|
||||
GROUP BY b.id, b.metadata.title, rs.bookType
|
||||
ORDER BY MIN(rs.startTime)
|
||||
""")
|
||||
List<ReadingSessionTimelineDto> findSessionTimelineByUserAndWeek(
|
||||
@Param("userId") Long userId,
|
||||
@Param("year") int year,
|
||||
@Param("month") int month,
|
||||
@Param("week") int week);
|
||||
@Param("startOfWeek") Instant startOfWeek,
|
||||
@Param("endOfWeek") Instant endOfWeek);
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
|
||||
+13
-2
@@ -29,6 +29,12 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.time.temporal.WeekFields;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -88,11 +94,16 @@ public class ReadingSessionService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReadingSessionTimelineResponse> getSessionTimelineForWeek(int year, int month, int week) {
|
||||
public List<ReadingSessionTimelineResponse> getSessionTimelineForWeek(int year, int week) {
|
||||
BookLoreUser authenticatedUser = authenticationService.getAuthenticatedUser();
|
||||
Long userId = authenticatedUser.getId();
|
||||
|
||||
return readingSessionRepository.findSessionTimelineByUserAndWeek(userId, year, month, week)
|
||||
LocalDate date = LocalDate.of(year, 1, 1)
|
||||
.with(WeekFields.of(DayOfWeek.MONDAY, 1).weekOfYear(), week);
|
||||
LocalDateTime startOfWeek = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
|
||||
LocalDateTime endOfWeek = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).plusDays(1).atStartOfDay();
|
||||
|
||||
return readingSessionRepository.findSessionTimelineByUserAndWeek(userId, startOfWeek.atZone(ZoneId.systemDefault()).toInstant(), endOfWeek.atZone(ZoneId.systemDefault()).toInstant())
|
||||
.stream()
|
||||
.map(dto -> ReadingSessionTimelineResponse.builder()
|
||||
.bookId(dto.getBookId())
|
||||
|
||||
Generated
+11
@@ -26,6 +26,7 @@
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-chart-matrix": "^3.0.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"epubjs": "^0.3.93",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
@@ -5852,6 +5853,16 @@
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-format": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-chart-matrix": "^3.0.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"epubjs": "^0.3.93",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
@@ -66,4 +67,4 @@
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,14 +59,14 @@ export class UserStatsService {
|
||||
getHeatmapForYear(year: number): Observable<ReadingSessionHeatmapResponse[]> {
|
||||
return this.http.get<ReadingSessionHeatmapResponse[]>(
|
||||
`${this.readingSessionsUrl}/heatmap`,
|
||||
{ params: { year: year.toString() } }
|
||||
{params: {year: year.toString()}}
|
||||
);
|
||||
}
|
||||
|
||||
getTimelineForWeek(year: number, month: number, week: number): Observable<ReadingSessionTimelineResponse[]> {
|
||||
getTimelineForWeek(year: number, week: number): Observable<ReadingSessionTimelineResponse[]> {
|
||||
return this.http.get<ReadingSessionTimelineResponse[]>(
|
||||
`${this.readingSessionsUrl}/timeline`,
|
||||
{ params: { year: year.toString(), month: month.toString(), week: week.toString() } }
|
||||
{params: {year: year.toString(), week: week.toString()}}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class UserStatsService {
|
||||
getCompletionTimelineForYear(year: number): Observable<CompletionTimelineResponse[]> {
|
||||
return this.http.get<CompletionTimelineResponse[]>(
|
||||
`${this.readingSessionsUrl}/completion-timeline`,
|
||||
{ params: { year: year.toString() } }
|
||||
{params: {year: year.toString()}}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -1,4 +1,4 @@
|
||||
import {Component, inject, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {BaseChartDirective} from 'ng2-charts';
|
||||
import {ChartConfiguration, ChartData} from 'chart.js';
|
||||
@@ -32,19 +32,19 @@ export class FavoriteDaysChartComponent implements OnInit, OnDestroy {
|
||||
public selectedMonth: number | null = null;
|
||||
public yearOptions: { label: string; value: number | null }[] = [];
|
||||
public monthOptions: { label: string; value: number | null }[] = [
|
||||
{ label: 'All Months', value: null },
|
||||
{ label: 'January', value: 1 },
|
||||
{ label: 'February', value: 2 },
|
||||
{ label: 'March', value: 3 },
|
||||
{ label: 'April', value: 4 },
|
||||
{ label: 'May', value: 5 },
|
||||
{ label: 'June', value: 6 },
|
||||
{ label: 'July', value: 7 },
|
||||
{ label: 'August', value: 8 },
|
||||
{ label: 'September', value: 9 },
|
||||
{ label: 'October', value: 10 },
|
||||
{ label: 'November', value: 11 },
|
||||
{ label: 'December', value: 12 }
|
||||
{label: 'All Months', value: null},
|
||||
{label: 'January', value: 1},
|
||||
{label: 'February', value: 2},
|
||||
{label: 'March', value: 3},
|
||||
{label: 'April', value: 4},
|
||||
{label: 'May', value: 5},
|
||||
{label: 'June', value: 6},
|
||||
{label: 'July', value: 7},
|
||||
{label: 'August', value: 8},
|
||||
{label: 'September', value: 9},
|
||||
{label: 'October', value: 10},
|
||||
{label: 'November', value: 11},
|
||||
{label: 'December', value: 12}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
@@ -161,7 +161,7 @@ export class FavoriteDaysChartComponent implements OnInit, OnDestroy {
|
||||
ticks: {
|
||||
color: '#ffffff',
|
||||
font: {family: "'Inter', sans-serif", size: 11},
|
||||
callback: function(value) {
|
||||
callback: function (value) {
|
||||
return (typeof value === 'number' ? value.toFixed(1) : '0.0') + 'h';
|
||||
}
|
||||
},
|
||||
@@ -186,9 +186,9 @@ export class FavoriteDaysChartComponent implements OnInit, OnDestroy {
|
||||
|
||||
private initializeYearOptions(): void {
|
||||
const currentYear = new Date().getFullYear();
|
||||
this.yearOptions = [{ label: 'All Years', value: null }];
|
||||
this.yearOptions = [{label: 'All Years', value: null}];
|
||||
for (let year = currentYear; year >= currentYear - 10; year--) {
|
||||
this.yearOptions.push({ label: year.toString(), value: year });
|
||||
this.yearOptions.push({label: year.toString(), value: year});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ export class FavoriteDaysChartComponent implements OnInit, OnDestroy {
|
||||
private updateChartData(favoriteDays: FavoriteDaysResponse[]): void {
|
||||
const dayMap = new Map<number, FavoriteDaysResponse>();
|
||||
favoriteDays.forEach(item => {
|
||||
dayMap.set(item.dayOfWeek, item);
|
||||
dayMap.set(item.dayOfWeek - 1, item);
|
||||
});
|
||||
|
||||
const labels = this.allDays;
|
||||
|
||||
+38
-55
@@ -7,6 +7,16 @@ import {catchError} from 'rxjs/operators';
|
||||
import {of} from 'rxjs';
|
||||
import {Select} from 'primeng/select';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {
|
||||
addWeeks,
|
||||
endOfISOWeek,
|
||||
getISOWeek,
|
||||
getISOWeeksInYear,
|
||||
getISOWeekYear,
|
||||
setISOWeek,
|
||||
setISOWeekYear,
|
||||
startOfISOWeek
|
||||
} from 'date-fns';
|
||||
|
||||
interface ReadingSession {
|
||||
startTime: Date;
|
||||
@@ -47,7 +57,7 @@ interface DayTimeline {
|
||||
})
|
||||
export class ReadingSessionTimelineComponent implements OnInit {
|
||||
@Input() initialYear: number = new Date().getFullYear();
|
||||
@Input() weekNumber: number = this.getCurrentWeekNumber();
|
||||
@Input() weekNumber: number = getISOWeek(new Date());
|
||||
|
||||
private userStatsService = inject(UserStatsService);
|
||||
private urlHelperService = inject(UrlHelperService);
|
||||
@@ -56,8 +66,8 @@ export class ReadingSessionTimelineComponent implements OnInit {
|
||||
public hourLabels: string[] = [];
|
||||
public timelineData: DayTimeline[] = [];
|
||||
public currentYear: number = new Date().getFullYear();
|
||||
public currentWeek: number = this.getCurrentWeekNumber();
|
||||
public currentMonth: number = new Date().getMonth() + 1;
|
||||
public currentWeek: number = getISOWeek(new Date());
|
||||
private currentDate: Date = new Date();
|
||||
|
||||
public yearOptions: { label: string; value: number }[] = [];
|
||||
public weekOptions: { label: string; value: number }[] = [];
|
||||
@@ -65,8 +75,9 @@ export class ReadingSessionTimelineComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.currentYear = this.initialYear;
|
||||
this.currentWeek = this.weekNumber;
|
||||
this.updateCurrentMonth();
|
||||
this.updateDateFromYearAndWeek();
|
||||
this.initializeYearOptions();
|
||||
this.ensureYearInOptions();
|
||||
this.updateWeekOptions();
|
||||
this.initializeHourLabels();
|
||||
this.loadReadingSessions();
|
||||
@@ -76,30 +87,31 @@ export class ReadingSessionTimelineComponent implements OnInit {
|
||||
const currentYear = new Date().getFullYear();
|
||||
this.yearOptions = [];
|
||||
for (let year = currentYear; year >= currentYear - 10; year--) {
|
||||
this.yearOptions.push({ label: year.toString(), value: year });
|
||||
this.yearOptions.push({label: year.toString(), value: year});
|
||||
}
|
||||
}
|
||||
|
||||
private updateWeekOptions(): void {
|
||||
const weeksInYear = this.getWeeksInYear(this.currentYear);
|
||||
const weeksInYear = getISOWeeksInYear(this.currentDate);
|
||||
this.weekOptions = [];
|
||||
for (let week = 1; week <= weeksInYear; week++) {
|
||||
this.weekOptions.push({ label: `Week ${week}`, value: week });
|
||||
this.weekOptions.push({label: `Week ${week}`, value: week});
|
||||
}
|
||||
}
|
||||
|
||||
public onYearChange(): void {
|
||||
this.updateWeekOptions();
|
||||
const maxWeeks = this.getWeeksInYear(this.currentYear);
|
||||
this.updateDateFromYearAndWeek();
|
||||
const maxWeeks = getISOWeeksInYear(this.currentDate);
|
||||
if (this.currentWeek > maxWeeks) {
|
||||
this.currentWeek = maxWeeks;
|
||||
this.updateDateFromYearAndWeek();
|
||||
}
|
||||
this.updateCurrentMonth();
|
||||
this.updateWeekOptions();
|
||||
this.loadReadingSessions();
|
||||
}
|
||||
|
||||
public onWeekChange(): void {
|
||||
this.updateCurrentMonth();
|
||||
this.updateDateFromYearAndWeek();
|
||||
this.loadReadingSessions();
|
||||
}
|
||||
|
||||
@@ -112,7 +124,7 @@ export class ReadingSessionTimelineComponent implements OnInit {
|
||||
}
|
||||
|
||||
private loadReadingSessions(): void {
|
||||
this.userStatsService.getTimelineForWeek(this.currentYear, this.currentMonth, this.currentWeek)
|
||||
this.userStatsService.getTimelineForWeek(this.currentYear, this.currentWeek)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
console.error('Error loading reading sessions:', error);
|
||||
@@ -148,59 +160,30 @@ export class ReadingSessionTimelineComponent implements OnInit {
|
||||
return sessions.sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
|
||||
}
|
||||
|
||||
private getCurrentWeekNumber(): number {
|
||||
const now = new Date();
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1);
|
||||
const days = Math.floor((now.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000));
|
||||
return Math.ceil((days + startOfYear.getDay() + 1) / 7);
|
||||
}
|
||||
|
||||
public changeWeek(delta: number): void {
|
||||
this.currentWeek += delta;
|
||||
this.currentDate = addWeeks(this.currentDate, delta);
|
||||
this.currentYear = getISOWeekYear(this.currentDate);
|
||||
this.currentWeek = getISOWeek(this.currentDate);
|
||||
|
||||
const weeksInYear = this.getWeeksInYear(this.currentYear);
|
||||
if (this.currentWeek > weeksInYear) {
|
||||
this.currentWeek = 1;
|
||||
this.currentYear++;
|
||||
this.updateWeekOptions();
|
||||
} else if (this.currentWeek < 1) {
|
||||
this.currentYear--;
|
||||
this.currentWeek = this.getWeeksInYear(this.currentYear);
|
||||
this.updateWeekOptions();
|
||||
}
|
||||
|
||||
this.updateCurrentMonth();
|
||||
this.ensureYearInOptions();
|
||||
this.updateWeekOptions();
|
||||
this.loadReadingSessions();
|
||||
}
|
||||
|
||||
private updateCurrentMonth(): void {
|
||||
const startOfYear = new Date(this.currentYear, 0, 1);
|
||||
const daysToAdd = (this.currentWeek - 1) * 7;
|
||||
const weekStart = new Date(startOfYear.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
|
||||
|
||||
const dayOfWeek = weekStart.getDay();
|
||||
weekStart.setDate(weekStart.getDate() - dayOfWeek);
|
||||
|
||||
this.currentMonth = weekStart.getMonth() + 1;
|
||||
private ensureYearInOptions(): void {
|
||||
if (!this.yearOptions.some(option => option.value === this.currentYear)) {
|
||||
this.yearOptions.unshift({label: this.currentYear.toString(), value: this.currentYear});
|
||||
this.yearOptions.sort((a, b) => b.value - a.value);
|
||||
}
|
||||
}
|
||||
|
||||
private getWeeksInYear(year: number): number {
|
||||
const lastDay = new Date(year, 11, 31);
|
||||
const startOfYear = new Date(year, 0, 1);
|
||||
const days = Math.floor((lastDay.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000));
|
||||
return Math.ceil((days + startOfYear.getDay() + 1) / 7);
|
||||
private updateDateFromYearAndWeek(): void {
|
||||
this.currentDate = setISOWeek(setISOWeekYear(new Date(), this.currentYear), this.currentWeek);
|
||||
}
|
||||
|
||||
public getWeekDateRange(): string {
|
||||
const startOfYear = new Date(this.currentYear, 0, 1);
|
||||
const daysToAdd = (this.currentWeek - 1) * 7;
|
||||
const weekStart = new Date(startOfYear.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
|
||||
|
||||
const dayOfWeek = weekStart.getDay();
|
||||
weekStart.setDate(weekStart.getDate() - dayOfWeek);
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
const weekStart = startOfISOWeek(this.currentDate);
|
||||
const weekEnd = endOfISOWeek(this.currentDate);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const month = date.toLocaleDateString('en-US', {month: 'short'});
|
||||
|
||||
Reference in New Issue
Block a user