package checkin import ( "database/sql" "errors" "fmt" "time" ) 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(volunteerID int64, in CheckInInput) (*CheckIn, error) { // Ensure no active check-in exists var count int s.db.QueryRow( `SELECT COUNT(*) FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL`, volunteerID, ).Scan(&count) if count > 0 { return nil, fmt.Errorf("already checked in") } res, err := s.db.Exec( `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(id) } func (s *Store) CheckOut(volunteerID int64, in CheckOutInput) (*CheckIn, error) { var id int64 err := s.db.QueryRow( `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, fmt.Errorf("not checked in") } if err != nil { return nil, fmt.Errorf("find active checkin: %w", err) } _, err = s.db.Exec( `UPDATE checkins SET checked_out_at=datetime('now'), notes=COALESCE(NULLIF(?, ''), notes) WHERE id=?`, in.Notes, id, ) if err != nil { return nil, fmt.Errorf("checkout: %w", err) } return s.GetByID(id) } func (s *Store) GetByID(id int64) (*CheckIn, error) { ci := &CheckIn{} var checkedInAt string var checkedOutAt sql.NullString var scheduleID sql.NullInt64 var notes sql.NullString err := s.db.QueryRow( `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, nil } 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(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.Query(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() }