package repositories import ( "fmt" "regexp" "strings" "nearle/models" ) // Shift windows for till staff. // // Scoped by tenant *and* outlet in every statement rather than checked first, // the same shape the till-user queries use: a console naming somebody else's // shift id updates no rows and is told so, instead of quietly editing another // shop's hours. var posTimeOfDay = regexp.MustCompile(`^([01]\d|2[0-3]):[0-5]\d$`) // normaliseShiftTime accepts what a time input actually sends. // // `` gives "07:00", some browsers and most hand-typed values // give "07:00:00", and `ridershifts` already stores the seconds form. Both are // reduced to `HH:MM` so a shift written by one client reads the same to another. func normaliseShiftTime(raw string) (string, error) { t := strings.TrimSpace(raw) if t == "" { return "", fmt.Errorf("a start and end time are required") } if len(t) == 8 && strings.Count(t, ":") == 2 { t = t[:5] } if !posTimeOfDay.MatchString(t) { return "", fmt.Errorf("time must be 24-hour HH:MM; got %q", raw) } return t, nil } // ListStaffShifts returns an outlet's shifts, newest last so a picker reads in // the order they were created rather than alphabetically by name. func (r *posRepository) ListStaffShifts(tenantID, locationID int, includeInactive bool) ([]models.StaffShifts, error) { if tenantID <= 0 || locationID <= 0 { return nil, fmt.Errorf("tenantid and locationid are required") } shifts := make([]models.StaffShifts, 0) query := `SELECT * FROM staffshifts WHERE tenantid = ? AND locationid = ?` if !includeInactive { query += ` AND LOWER(COALESCE(status,'active')) <> 'inactive'` } query += ` ORDER BY staffshiftid` if err := r.db.Raw(query, tenantID, locationID).Scan(&shifts).Error; err != nil { return nil, err } return shifts, nil } // CreateStaffShift adds a window at one outlet. func (r *posRepository) CreateStaffShift(tenantID, locationID int, req models.StaffShifts) (*models.StaffShifts, error) { if tenantID <= 0 || locationID <= 0 { return nil, fmt.Errorf("tenantid and locationid are required") } name := strings.TrimSpace(req.Name) if name == "" { return nil, fmt.Errorf("a shift name is required") } start, err := normaliseShiftTime(req.Starttime) if err != nil { return nil, err } end, err := normaliseShiftTime(req.Endtime) if err != nil { return nil, err } // An end before a start is allowed on purpose — a night shift runs 22:00 to // 06:00 and wrapping midnight is ordinary in retail. Only the equal case is // refused, because a zero-length window cannot be what anyone meant. if start == end { return nil, fmt.Errorf("a shift cannot start and end at the same time") } weekdays := strings.TrimSpace(req.Weekdays) if weekdays != "" && !regexp.MustCompile(`^[01]{7}$`).MatchString(weekdays) { return nil, fmt.Errorf("weekdays must be seven 0/1 characters starting Monday, e.g. 1111100") } shift := models.StaffShifts{ Tenantid: tenantID, Locationid: locationID, Name: name, Starttime: start, Endtime: end, Weekdays: weekdays, Status: "Active", } if err := r.db.Table("staffshifts").Create(&shift).Error; err != nil { return nil, err } return &shift, nil } // UpdateStaffShift edits a window. Every field is optional; only the ones sent // are written, matching how the till-user update behaves. func (r *posRepository) UpdateStaffShift(tenantID, locationID int, req models.StaffShifts) (*models.StaffShifts, error) { if req.Staffshiftid <= 0 { return nil, fmt.Errorf("staff_shift_id is required") } sets := []string{} args := []interface{}{} if name := strings.TrimSpace(req.Name); name != "" { sets = append(sets, "name = ?") args = append(args, name) } if strings.TrimSpace(req.Starttime) != "" { t, err := normaliseShiftTime(req.Starttime) if err != nil { return nil, err } sets = append(sets, "starttime = ?") args = append(args, t) } if strings.TrimSpace(req.Endtime) != "" { t, err := normaliseShiftTime(req.Endtime) if err != nil { return nil, err } sets = append(sets, "endtime = ?") args = append(args, t) } if w := strings.TrimSpace(req.Weekdays); w != "" { if !regexp.MustCompile(`^[01]{7}$`).MatchString(w) { return nil, fmt.Errorf("weekdays must be seven 0/1 characters starting Monday") } sets = append(sets, "weekdays = ?") args = append(args, w) } if s := strings.TrimSpace(req.Status); s != "" { sets = append(sets, "status = ?") args = append(args, s) } if len(sets) == 0 { return nil, fmt.Errorf("nothing to change") } sets = append(sets, "updated = NOW()") args = append(args, req.Staffshiftid, tenantID, locationID) res := r.db.Exec( fmt.Sprintf(`UPDATE staffshifts SET %s WHERE staffshiftid = ? AND tenantid = ? AND locationid = ?`, strings.Join(sets, ", ")), args...) if res.Error != nil { return nil, res.Error } if res.RowsAffected == 0 { return nil, fmt.Errorf("no shift %d at this outlet", req.Staffshiftid) } var out models.StaffShifts if err := r.db.Raw(`SELECT * FROM staffshifts WHERE staffshiftid = ?`, req.Staffshiftid).Scan(&out).Error; err != nil { return nil, err } return &out, nil }