198 lines
6.3 KiB
Go
198 lines
6.3 KiB
Go
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.
|
||
//
|
||
// `<input type="time">` 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 the shifts a tenant's staff can be put on, newest
|
||
// last so a picker reads in the order they were created rather than
|
||
// alphabetically by name.
|
||
//
|
||
// ── Why the outlet is optional ──────────────────────────────────────────────
|
||
//
|
||
// A shift is a fact about how a BUSINESS runs, not about one shop: a tenant
|
||
// that works 07:00–15:00 and 15:00–23:00 works those hours at every branch it
|
||
// owns, and making somebody re-enter them per outlet guarantees the third
|
||
// branch gets 07:00–15:30 and nobody notices. `locationid = 0` is a shift that
|
||
// belongs to the whole tenant.
|
||
//
|
||
// Branch-specific rows are still honoured, because they already exist and a
|
||
// tenant may genuinely run one outlet differently. Asking for an outlet returns
|
||
// the tenant's shifts AND that outlet's own; asking for none returns everything
|
||
// the tenant has.
|
||
func (r *posRepository) ListStaffShifts(tenantID, locationID int, includeInactive bool) ([]models.StaffShifts, error) {
|
||
if tenantID <= 0 {
|
||
return nil, fmt.Errorf("tenantid is required")
|
||
}
|
||
|
||
shifts := make([]models.StaffShifts, 0)
|
||
args := []any{tenantID}
|
||
|
||
query := `SELECT * FROM staffshifts WHERE tenantid = ?`
|
||
if locationID > 0 {
|
||
// The tenant-wide ones and this outlet's, never another outlet's.
|
||
query += ` AND (COALESCE(locationid, 0) = 0 OR locationid = ?)`
|
||
args = append(args, locationID)
|
||
}
|
||
if !includeInactive {
|
||
query += ` AND LOWER(COALESCE(status,'active')) <> 'inactive'`
|
||
}
|
||
query += ` ORDER BY staffshiftid`
|
||
|
||
if err := r.db.Raw(query, args...).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 {
|
||
return nil, fmt.Errorf("tenantid is required")
|
||
}
|
||
// `locationid = 0` is deliberate and is now the ordinary case: the shift
|
||
// belongs to the tenant and every branch it owns can use it. An outlet is
|
||
// only named when one shop really does run different hours.
|
||
if locationID < 0 {
|
||
locationID = 0
|
||
}
|
||
|
||
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
|
||
}
|