feat(schedule): add course table screens and navigation
Add complete schedule functionality including: - Schedule screen with weekly course table view - Course detail screen with transparent modal presentation - New ScheduleStack navigator integrated into main tab bar - Schedule service for API interactions - Type definitions for course entities Also includes bug fixes for group invite/request handlers to include required groupId parameter.
This commit is contained in:
207
internal/service/schedule_service.go
Normal file
207
internal/service/schedule_service.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/dto"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSchedulePayload = &ServiceError{Code: 400, Message: "invalid schedule payload"}
|
||||
ErrScheduleCourseNotFound = &ServiceError{Code: 404, Message: "schedule course not found"}
|
||||
ErrScheduleForbidden = &ServiceError{Code: 403, Message: "forbidden schedule operation"}
|
||||
ErrScheduleColorDuplicated = &ServiceError{Code: 400, Message: "course color already used"}
|
||||
)
|
||||
|
||||
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)
|
||||
|
||||
type CreateScheduleCourseInput struct {
|
||||
Name string
|
||||
Teacher string
|
||||
Location string
|
||||
DayOfWeek int
|
||||
StartSection int
|
||||
EndSection int
|
||||
Weeks []int
|
||||
Color string
|
||||
}
|
||||
|
||||
type ScheduleService interface {
|
||||
ListCourses(userID string, week int) ([]*dto.ScheduleCourseResponse, error)
|
||||
CreateCourse(userID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error)
|
||||
UpdateCourse(userID, courseID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error)
|
||||
DeleteCourse(userID, courseID string) error
|
||||
}
|
||||
|
||||
type scheduleService struct {
|
||||
repo repository.ScheduleRepository
|
||||
}
|
||||
|
||||
func NewScheduleService(repo repository.ScheduleRepository) ScheduleService {
|
||||
return &scheduleService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *scheduleService) ListCourses(userID string, week int) ([]*dto.ScheduleCourseResponse, error) {
|
||||
courses, err := s.repo.ListByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*dto.ScheduleCourseResponse, 0, len(courses))
|
||||
for _, item := range courses {
|
||||
weeks := dto.ParseWeeksJSON(item.Weeks)
|
||||
if week > 0 && !containsWeek(weeks, week) {
|
||||
continue
|
||||
}
|
||||
result = append(result, dto.ConvertScheduleCourseToResponse(item, weeks))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *scheduleService) CreateCourse(userID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error) {
|
||||
entity, weeks, err := buildScheduleEntity(userID, input, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.ensureUniqueColor(userID, entity.Color, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.Create(entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.ConvertScheduleCourseToResponse(entity, weeks), nil
|
||||
}
|
||||
|
||||
func (s *scheduleService) UpdateCourse(userID, courseID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error) {
|
||||
existing, err := s.repo.GetByID(courseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrScheduleCourseNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if existing.UserID != userID {
|
||||
return nil, ErrScheduleForbidden
|
||||
}
|
||||
|
||||
entity, weeks, err := buildScheduleEntity(userID, input, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.ensureUniqueColor(userID, entity.Color, entity.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.Update(entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.ConvertScheduleCourseToResponse(entity, weeks), nil
|
||||
}
|
||||
|
||||
func (s *scheduleService) DeleteCourse(userID, courseID string) error {
|
||||
existing, err := s.repo.GetByID(courseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrScheduleCourseNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if existing.UserID != userID {
|
||||
return ErrScheduleForbidden
|
||||
}
|
||||
return s.repo.DeleteByID(courseID)
|
||||
}
|
||||
|
||||
func buildScheduleEntity(userID string, input CreateScheduleCourseInput, target *model.ScheduleCourse) (*model.ScheduleCourse, []int, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" || input.DayOfWeek < 0 || input.DayOfWeek > 6 || input.StartSection < 1 || input.EndSection < input.StartSection {
|
||||
return nil, nil, ErrInvalidSchedulePayload
|
||||
}
|
||||
|
||||
weeks := normalizeWeeks(input.Weeks)
|
||||
if len(weeks) == 0 {
|
||||
return nil, nil, ErrInvalidSchedulePayload
|
||||
}
|
||||
weeksJSON, err := json.Marshal(weeks)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
entity := target
|
||||
if entity == nil {
|
||||
entity = &model.ScheduleCourse{
|
||||
UserID: userID,
|
||||
}
|
||||
}
|
||||
|
||||
normalizedColor := normalizeHexColor(input.Color)
|
||||
if normalizedColor == "" || !hexColorRegex.MatchString(normalizedColor) {
|
||||
return nil, nil, ErrInvalidSchedulePayload
|
||||
}
|
||||
|
||||
entity.Name = name
|
||||
entity.Teacher = strings.TrimSpace(input.Teacher)
|
||||
entity.Location = strings.TrimSpace(input.Location)
|
||||
entity.DayOfWeek = input.DayOfWeek
|
||||
entity.StartSection = input.StartSection
|
||||
entity.EndSection = input.EndSection
|
||||
entity.Weeks = string(weeksJSON)
|
||||
entity.Color = normalizedColor
|
||||
|
||||
return entity, weeks, nil
|
||||
}
|
||||
|
||||
func (s *scheduleService) ensureUniqueColor(userID, color, excludeID string) error {
|
||||
exists, err := s.repo.ExistsColorByUser(userID, color, excludeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrScheduleColorDuplicated
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeWeeks(source []int) []int {
|
||||
unique := make(map[int]struct{}, len(source))
|
||||
result := make([]int, 0, len(source))
|
||||
for _, w := range source {
|
||||
if w < 1 || w > 30 {
|
||||
continue
|
||||
}
|
||||
if _, exists := unique[w]; exists {
|
||||
continue
|
||||
}
|
||||
unique[w] = struct{}{}
|
||||
result = append(result, w)
|
||||
}
|
||||
sort.Ints(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func containsWeek(weeks []int, target int) bool {
|
||||
for _, week := range weeks {
|
||||
if week == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeHexColor(color string) string {
|
||||
trimmed := strings.TrimSpace(color)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
return strings.ToUpper(trimmed)
|
||||
}
|
||||
return "#" + strings.ToUpper(trimmed)
|
||||
}
|
||||
Reference in New Issue
Block a user