feat: add manual interval creation and inline entry editing
- Service: CreateInterval() validates same-day, end>start, day not closed
- Service: Update() now rejects edits on closed-day entries (ErrDayAlreadyClosed)
- Handler: POST /api/entries creates a completed interval with explicit times
- API client: entries.createInterval(startMs, endMs, note)
- Utils: parseTimeInput / toTimeInput helpers for HH:MM <-> unix ms
- Today page: '+ Add interval' form (time pickers + optional note)
- Today page: pencil button on each entry opens inline edit row (start/end/note)
- Tests: TestCreateInterval, TestCreateIntervalEndBeforeStart,
TestCreateIntervalCrossesMidnight, TestUpdateRejectsClosedDay
This commit is contained in:
@@ -176,6 +176,15 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Disallow editing entries that belong to a closed day.
|
||||
closed, err := s.closedDays.GetByDayKey(ctx, e.DayKey)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if closed != nil {
|
||||
return nil, ErrDayAlreadyClosed
|
||||
}
|
||||
|
||||
if input.StartTime != nil {
|
||||
e.StartTime = *input.StartTime
|
||||
e.DayKey = s.dayKeyForMs(e.StartTime)
|
||||
@@ -203,6 +212,50 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// CreateIntervalInput holds fields for a manually created completed interval.
|
||||
type CreateIntervalInput struct {
|
||||
StartTime int64
|
||||
EndTime int64
|
||||
Note string
|
||||
}
|
||||
|
||||
// CreateInterval adds a completed interval with explicit start and end times.
|
||||
// Rules: same-day, end > start, day not closed, no currently running entry on that day (check running globally).
|
||||
func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalInput) (*domain.Entry, error) {
|
||||
if input.EndTime <= input.StartTime {
|
||||
return nil, fmt.Errorf("end_time must be after start_time")
|
||||
}
|
||||
|
||||
startDayKey := s.dayKeyForMs(input.StartTime)
|
||||
endDayKey := s.dayKeyForMs(input.EndTime)
|
||||
if startDayKey != endDayKey {
|
||||
return nil, ErrCrossesMidnight
|
||||
}
|
||||
|
||||
// Check day is not closed.
|
||||
closed, err := s.closedDays.GetByDayKey(ctx, startDayKey)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if closed != nil {
|
||||
return nil, ErrDayAlreadyClosed
|
||||
}
|
||||
|
||||
nowMs := s.nowMs()
|
||||
e := &domain.Entry{
|
||||
ID: uuid.New().String(),
|
||||
StartTime: input.StartTime,
|
||||
EndTime: &input.EndTime,
|
||||
Note: input.Note,
|
||||
DayKey: startDayKey,
|
||||
UpdatedAt: nowMs,
|
||||
}
|
||||
if err := s.entries.Create(ctx, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Delete soft-deletes an entry.
|
||||
func (s *EntryService) Delete(ctx context.Context, id string) error {
|
||||
nowMs := s.nowMs()
|
||||
|
||||
Reference in New Issue
Block a user