package checkin import ( "context" "database/sql" "errors" "fmt" "time" ) var ( ErrNotFound = fmt.Errorf("check-in not found") ErrAlreadyCheckedIn = fmt.Errorf("already checked in") ErrNotCheckedIn = fmt.Errorf("not checked in") ) type CheckIn struct { ID int64 `json:"id"` VolunteerID int64 `json:"volunteer_id"` ScheduleID *int64 `json:"schedule_id,omitempty"` CheckedInAt time.Time `json:"checked_in_at"` CheckedOutAt *time.Time `json:"checked_out_at,omitempty"` Notes string `json:"notes,omitempty"` } type CheckInInput struct { ScheduleID *int64 `json:"schedule_id"` Notes string `json:"notes"` } type CheckOutInput struct { Notes string `json:"notes"` } type Store struct { db *sql.DB } func NewStore(db *sql.DB) *Store { return &Store{db: db} } func (s *Store) CheckIn(ctx context.Context, volunteerID int64, in CheckInInput) (*CheckIn, error) { // Ensure no active check-in exists var count int s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL`, volunteerID, ).Scan(&count) if count > 0 { return nil, ErrAlreadyCheckedIn } res, err := s.db.ExecContext(ctx, `INSERT INTO checkins (volunteer_id, schedule_id, notes) VALUES (?, ?, ?)`, volunteerID, in.ScheduleID, in.Notes, ) if err != nil { return nil, fmt.Errorf("insert checkin: %w", err) } id, _ := res.LastInsertId() return s.GetByID(ctx, id) } func (s *Store) CheckOut(ctx context.Context, volunteerID int64, in CheckOutInput) (*CheckIn, error) { var id int64 err := s.db.QueryRowContext(ctx, `SELECT id FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL ORDER BY checked_in_at DESC LIMIT 1`, volunteerID, ).Scan(&id) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotCheckedIn } if err != nil { return nil, fmt.Errorf("find active checkin: %w", err) } _, err = s.db.ExecContext(ctx, `UPDATE checkins SET checked_out_at=NOW(), notes=COALESCE(NULLIF(?, ''), notes) WHERE id=?`, in.Notes, id, ) if err != nil { return nil, fmt.Errorf("checkout: %w", err) } return s.GetByID(ctx, id) } func (s *Store) GetByID(ctx context.Context, id int64) (*CheckIn, error) { ci := &CheckIn{} var checkedInAt string var checkedOutAt sql.NullString var scheduleID sql.NullInt64 var notes sql.NullString err := s.db.QueryRowContext(ctx, `SELECT id, volunteer_id, schedule_id, checked_in_at, checked_out_at, notes FROM checkins WHERE id = ?`, id, ).Scan(&ci.ID, &ci.VolunteerID, &scheduleID, &checkedInAt, &checkedOutAt, ¬es) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("get checkin: %w", err) } ci.CheckedInAt, _ = time.Parse("2006-01-02 15:04:05", checkedInAt) if checkedOutAt.Valid { t, _ := time.Parse("2006-01-02 15:04:05", checkedOutAt.String) ci.CheckedOutAt = &t } if scheduleID.Valid { ci.ScheduleID = &scheduleID.Int64 } if notes.Valid { ci.Notes = notes.String } return ci, nil } func (s *Store) History(ctx context.Context, volunteerID int64) ([]CheckIn, error) { query := `SELECT id, volunteer_id, schedule_id, checked_in_at, checked_out_at, notes FROM checkins` args := []any{} if volunteerID > 0 { query += ` WHERE volunteer_id = ?` args = append(args, volunteerID) } query += ` ORDER BY checked_in_at DESC` rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("list checkins: %w", err) } defer rows.Close() var checkins []CheckIn for rows.Next() { var ci CheckIn var checkedInAt string var checkedOutAt sql.NullString var scheduleID sql.NullInt64 var notes sql.NullString if err := rows.Scan(&ci.ID, &ci.VolunteerID, &scheduleID, &checkedInAt, &checkedOutAt, ¬es); err != nil { return nil, err } ci.CheckedInAt, _ = time.Parse("2006-01-02 15:04:05", checkedInAt) if checkedOutAt.Valid { t, _ := time.Parse("2006-01-02 15:04:05", checkedOutAt.String) ci.CheckedOutAt = &t } if scheduleID.Valid { ci.ScheduleID = &scheduleID.Int64 } if notes.Valid { ci.Notes = notes.String } checkins = append(checkins, ci) } return checkins, rows.Err() }