.機能追加、DBバグ修正

This commit is contained in:
2026-01-05 11:27:37 +09:00
parent 7d3da1bf2e
commit b2fbb472df
19 changed files with 1619 additions and 56 deletions

View File

@@ -285,7 +285,7 @@ func (h *APIHandler) CreateAssignment(c *gin.Context) {
}
}
assignment, err := h.assignmentService.Create(userID, input.Title, input.Description, input.Subject, input.Priority, dueDate)
assignment, err := h.assignmentService.Create(userID, input.Title, input.Description, input.Subject, input.Priority, dueDate, false, nil, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create assignment"})
return
@@ -351,7 +351,8 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
}
}
assignment, err := h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate)
// Preserve existing reminder settings for API updates
assignment, err := h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, existing.ReminderEnabled, existing.ReminderAt, existing.UrgentReminderEnabled)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update assignment"})
return
@@ -396,3 +397,44 @@ func (h *APIHandler) ToggleAssignment(c *gin.Context) {
c.JSON(http.StatusOK, assignment)
}
// GetStatistics returns statistics for the authenticated user
// GET /api/v1/statistics?subject=数学&from=2025-01-01&to=2025-12-31&include_archived=true
func (h *APIHandler) GetStatistics(c *gin.Context) {
userID := h.getUserID(c)
// Parse filter parameters
filter := service.StatisticsFilter{
Subject: c.Query("subject"),
IncludeArchived: c.Query("include_archived") == "true",
}
// Parse from date
if fromStr := c.Query("from"); fromStr != "" {
fromDate, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DD"})
return
}
filter.From = &fromDate
}
// Parse to date
if toStr := c.Query("to"); toStr != "" {
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DD"})
return
}
filter.To = &toDate
}
stats, err := h.assignmentService.GetStatistics(userID, filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch statistics"})
return
}
c.JSON(http.StatusOK, stats)
}

View File

@@ -119,6 +119,17 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
priority := c.PostForm("priority")
dueDateStr := c.PostForm("due_date")
// Parse reminder settings
reminderEnabled := c.PostForm("reminder_enabled") == "on"
reminderAtStr := c.PostForm("reminder_at")
var reminderAt *time.Time
if reminderEnabled && reminderAtStr != "" {
if parsed, err := time.ParseInLocation("2006-01-02T15:04", reminderAtStr, time.Local); err == nil {
reminderAt = &parsed
}
}
urgentReminderEnabled := c.PostForm("urgent_reminder_enabled") == "on"
dueDate, err := time.ParseInLocation("2006-01-02T15:04", dueDateStr, time.Local)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02", dueDateStr, time.Local)
@@ -140,7 +151,7 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
}
_, err = h.assignmentService.Create(userID, title, description, subject, priority, dueDate)
_, err = h.assignmentService.Create(userID, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
if err != nil {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
@@ -191,6 +202,17 @@ func (h *AssignmentHandler) Update(c *gin.Context) {
priority := c.PostForm("priority")
dueDateStr := c.PostForm("due_date")
// Parse reminder settings
reminderEnabled := c.PostForm("reminder_enabled") == "on"
reminderAtStr := c.PostForm("reminder_at")
var reminderAt *time.Time
if reminderEnabled && reminderAtStr != "" {
if parsed, err := time.ParseInLocation("2006-01-02T15:04", reminderAtStr, time.Local); err == nil {
reminderAt = &parsed
}
}
urgentReminderEnabled := c.PostForm("urgent_reminder_enabled") == "on"
dueDate, err := time.ParseInLocation("2006-01-02T15:04", dueDateStr, time.Local)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02", dueDateStr, time.Local)
@@ -201,7 +223,7 @@ func (h *AssignmentHandler) Update(c *gin.Context) {
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
}
_, err = h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate)
_, err = h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
if err != nil {
c.Redirect(http.StatusFound, "/assignments")
return
@@ -231,3 +253,90 @@ func (h *AssignmentHandler) Delete(c *gin.Context) {
c.Redirect(http.StatusFound, "/assignments")
}
func (h *AssignmentHandler) Statistics(c *gin.Context) {
userID := h.getUserID(c)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
// Parse filter parameters
filter := service.StatisticsFilter{
Subject: c.Query("subject"),
IncludeArchived: c.Query("include_archived") == "true",
}
fromStr := c.Query("from")
toStr := c.Query("to")
// Parse from date
if fromStr != "" {
fromDate, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
if err == nil {
filter.From = &fromDate
}
}
// Parse to date
if toStr != "" {
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
if err == nil {
filter.To = &toDate
}
}
stats, err := h.assignmentService.GetStatistics(userID, filter)
if err != nil {
RenderHTML(c, http.StatusInternalServerError, "error.html", gin.H{
"title": "エラー",
"message": "統計情報の取得に失敗しました",
})
return
}
// Get available subjects for filter dropdown (exclude archived)
subjects, _ := h.assignmentService.GetSubjectsWithArchived(userID, false)
archivedSubjects, _ := h.assignmentService.GetArchivedSubjects(userID)
// Create a map for quick lookup of archived subjects
archivedMap := make(map[string]bool)
for _, s := range archivedSubjects {
archivedMap[s] = true
}
RenderHTML(c, http.StatusOK, "assignments/statistics.html", gin.H{
"title": "統計",
"stats": stats,
"subjects": subjects,
"archivedSubjects": archivedMap,
"selectedSubject": filter.Subject,
"fromDate": fromStr,
"toDate": toStr,
"includeArchived": filter.IncludeArchived,
"isAdmin": role == "admin",
"userName": name,
})
}
func (h *AssignmentHandler) ArchiveSubject(c *gin.Context) {
userID := h.getUserID(c)
subject := c.PostForm("subject")
if subject != "" {
h.assignmentService.ArchiveSubject(userID, subject)
}
c.Redirect(http.StatusFound, "/statistics")
}
func (h *AssignmentHandler) UnarchiveSubject(c *gin.Context) {
userID := h.getUserID(c)
subject := c.PostForm("subject")
if subject != "" {
h.assignmentService.UnarchiveSubject(userID, subject)
}
c.Redirect(http.StatusFound, "/statistics?include_archived=true")
}

View File

@@ -4,18 +4,21 @@ import (
"net/http"
"homework-manager/internal/middleware"
"homework-manager/internal/models"
"homework-manager/internal/service"
"github.com/gin-gonic/gin"
)
type ProfileHandler struct {
authService *service.AuthService
authService *service.AuthService
notificationService *service.NotificationService
}
func NewProfileHandler() *ProfileHandler {
func NewProfileHandler(notificationService *service.NotificationService) *ProfileHandler {
return &ProfileHandler{
authService: service.NewAuthService(),
authService: service.NewAuthService(),
notificationService: notificationService,
}
}
@@ -27,15 +30,17 @@ func (h *ProfileHandler) getUserID(c *gin.Context) uint {
func (h *ProfileHandler) Show(c *gin.Context) {
userID := h.getUserID(c)
user, _ := h.authService.GetUserByID(userID)
notifySettings, _ := h.notificationService.GetUserSettings(userID)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"isAdmin": role == "admin",
"userName": name,
"title": "プロフィール",
"user": user,
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
}
@@ -47,24 +52,27 @@ func (h *ProfileHandler) Update(c *gin.Context) {
role, _ := c.Get(middleware.UserRoleKey)
user, _ := h.authService.GetUserByID(userID)
notifySettings, _ := h.notificationService.GetUserSettings(userID)
if err != nil {
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"error": "プロフィールの更新に失敗しました",
"isAdmin": role == "admin",
"userName": name,
"title": "プロフィール",
"user": user,
"error": "プロフィールの更新に失敗しました",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
return
}
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"success": "プロフィールを更新しました",
"isAdmin": role == "admin",
"userName": user.Name,
"title": "プロフィール",
"user": user,
"success": "プロフィールを更新しました",
"isAdmin": role == "admin",
"userName": user.Name,
"notifySettings": notifySettings,
})
}
@@ -77,25 +85,28 @@ func (h *ProfileHandler) ChangePassword(c *gin.Context) {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
user, _ := h.authService.GetUserByID(userID)
notifySettings, _ := h.notificationService.GetUserSettings(userID)
if newPassword != confirmPassword {
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"passwordError": "新しいパスワードが一致しません",
"isAdmin": role == "admin",
"userName": name,
"title": "プロフィール",
"user": user,
"passwordError": "新しいパスワードが一致しません",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
return
}
if len(newPassword) < 8 {
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"passwordError": "パスワードは8文字以上で入力してください",
"isAdmin": role == "admin",
"userName": name,
"title": "プロフィール",
"user": user,
"passwordError": "パスワードは8文字以上で入力してください",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
return
}
@@ -103,11 +114,12 @@ func (h *ProfileHandler) ChangePassword(c *gin.Context) {
err := h.authService.ChangePassword(userID, oldPassword, newPassword)
if err != nil {
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"passwordError": "現在のパスワードが正しくありません",
"isAdmin": role == "admin",
"userName": name,
"title": "プロフィール",
"user": user,
"passwordError": "現在のパスワードが正しくありません",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
return
}
@@ -118,5 +130,46 @@ func (h *ProfileHandler) ChangePassword(c *gin.Context) {
"passwordSuccess": "パスワードを変更しました",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
}
func (h *ProfileHandler) UpdateNotificationSettings(c *gin.Context) {
userID := h.getUserID(c)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
user, _ := h.authService.GetUserByID(userID)
settings := &models.UserNotificationSettings{
TelegramEnabled: c.PostForm("telegram_enabled") == "on",
TelegramChatID: c.PostForm("telegram_chat_id"),
LineEnabled: c.PostForm("line_enabled") == "on",
LineNotifyToken: c.PostForm("line_token"),
}
err := h.notificationService.UpdateUserSettings(userID, settings)
notifySettings, _ := h.notificationService.GetUserSettings(userID)
if err != nil {
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"notifyError": "通知設定の更新に失敗しました",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
return
}
RenderHTML(c, http.StatusOK, "profile.html", gin.H{
"title": "プロフィール",
"user": user,
"notifySuccess": "通知設定を更新しました",
"isAdmin": role == "admin",
"userName": name,
"notifySettings": notifySettings,
})
}