Files
backend_fiesta/repositories/posShiftRepository.go
2026-09-25 16:18:53 +05:30

198 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}