shifts
This commit is contained in:
@@ -15,6 +15,7 @@ type PartnerRepository interface {
|
||||
GetActiveRiders(partnerid, aid, uid, tid int) ([]models.RiderInfo, error)
|
||||
GetPartners(aid, pid, uid int) ([]models.Partnerinfo, error)
|
||||
GetRiderShifts(aid int) ([]models.Ridershifts, error)
|
||||
CreateRiderShift(shift models.Ridershifts) (models.Ridershifts, error)
|
||||
GetLocationConfig(uid, cid int) ([]models.Locationconfigs, error)
|
||||
GetRiderLogs(pid, aid int, fdate, tdate string) ([]models.RiderlogDetails, error)
|
||||
GetRiderInfo(userid int) (models.RiderInfo, error)
|
||||
@@ -1027,3 +1028,178 @@ func (r *partnerRepository) regionByName(db *gorm.DB, name string) int {
|
||||
WHERE LOWER(TRIM(locationname)) = LOWER(TRIM(?)) LIMIT 1`, name).Scan(&id)
|
||||
return id
|
||||
}
|
||||
|
||||
// ── Rider shifts ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A shift is the window a rider works, and `CreateRider` refuses a rider
|
||||
// without one — `getriders` joins `ridershifts` through `ridersettings.shiftid`,
|
||||
// so a rider on a shift that does not exist is a rider nobody can see.
|
||||
//
|
||||
// Until now the table could only be READ. There was no endpoint, no service
|
||||
// method and not even a field for `applocationid` on the model, so a region
|
||||
// that shipped without shift rows could never have a rider added to it at all:
|
||||
// the console showed "No shifts set up for this region" and there was nothing
|
||||
// anybody could do from the product to change that. Till staff had
|
||||
// `createstaffshift` from the beginning; riders were simply missed.
|
||||
|
||||
// riderShiftClock is a start or end time as the column stores it.
|
||||
//
|
||||
// Accepts `9:00`, `09:00` and `09:00:00` and normalises to `HH:MM`. The rows
|
||||
// inserted by hand over the years use all three spellings, and `GetRiderShifts`
|
||||
// builds its label by concatenating the two columns raw — so `9:00-17:00` and
|
||||
// `09:00-17:00` are two different labels for one window in the same dropdown.
|
||||
func riderShiftClock(raw string) (string, error) {
|
||||
text := strings.TrimSpace(raw)
|
||||
if text == "" {
|
||||
return "", errors.New("a shift needs a start and an end time")
|
||||
}
|
||||
|
||||
parts := strings.Split(text, ":")
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
return "", fmt.Errorf("%q is not a time — write it as HH:MM", raw)
|
||||
}
|
||||
|
||||
hour, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
if err != nil || hour < 0 || hour > 23 {
|
||||
return "", fmt.Errorf("%q is not a time — the hour must be 0 to 23", raw)
|
||||
}
|
||||
minute, err := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
if err != nil || minute < 0 || minute > 59 {
|
||||
return "", fmt.Errorf("%q is not a time — the minutes must be 0 to 59", raw)
|
||||
}
|
||||
return fmt.Sprintf("%02d:%02d", hour, minute), nil
|
||||
}
|
||||
|
||||
// riderShiftHours is how long the window runs, in hours.
|
||||
//
|
||||
// Computed rather than asked for, because it is the one field a person gets
|
||||
// wrong and nothing downstream checks: `shifthours` feeds rider pay, and a
|
||||
// window of 09:00–17:00 recorded as 4 hours underpays every rider on it.
|
||||
//
|
||||
// A window that ends before it starts crosses midnight and is measured that
|
||||
// way — a 22:00–06:00 night shift is eight hours, not minus sixteen.
|
||||
func riderShiftHours(start, end string) float32 {
|
||||
toMinutes := func(clock string) int {
|
||||
parts := strings.Split(clock, ":")
|
||||
hour, _ := strconv.Atoi(parts[0])
|
||||
minute, _ := strconv.Atoi(parts[1])
|
||||
return hour*60 + minute
|
||||
}
|
||||
|
||||
span := toMinutes(end) - toMinutes(start)
|
||||
if span <= 0 {
|
||||
span += 24 * 60
|
||||
}
|
||||
return float32(span) / 60
|
||||
}
|
||||
|
||||
// validateRiderShift checks everything that does not need the database.
|
||||
//
|
||||
// Split out so the rules are testable without one, and returns the shift with
|
||||
// its times normalised and its hours worked out rather than reporting on a copy
|
||||
// the caller then has to rebuild.
|
||||
func validateRiderShift(shift models.Ridershifts) (models.Ridershifts, error) {
|
||||
if shift.Applocationid == 0 {
|
||||
return shift, errors.New("a shift needs a delivery region")
|
||||
}
|
||||
|
||||
start, err := riderShiftClock(shift.Starttime)
|
||||
if err != nil {
|
||||
return shift, err
|
||||
}
|
||||
end, err := riderShiftClock(shift.Endtime)
|
||||
if err != nil {
|
||||
return shift, err
|
||||
}
|
||||
if start == end {
|
||||
return shift, errors.New("a shift cannot start and end at the same time")
|
||||
}
|
||||
|
||||
shift.Starttime = start
|
||||
shift.Endtime = end
|
||||
// Always recomputed, never taken from the request. A caller that sends its
|
||||
// own number is a caller that can disagree with the window it just sent.
|
||||
shift.Shifthours = riderShiftHours(start, end)
|
||||
|
||||
if shift.Basefare < 0 || shift.Additionalcharges < 0 || shift.Fuelcharge < 0 {
|
||||
return shift, errors.New("pay cannot be negative")
|
||||
}
|
||||
return shift, nil
|
||||
}
|
||||
|
||||
// CreateRiderShift opens a shift window in one region.
|
||||
func (r *partnerRepository) CreateRiderShift(shift models.Ridershifts) (models.Ridershifts, error) {
|
||||
shift, err := validateRiderShift(shift)
|
||||
if err != nil {
|
||||
return models.Ridershifts{}, err
|
||||
}
|
||||
|
||||
// Same guard `CreateRider` applies to a rider's region, for the same reason:
|
||||
// `getriders` joins app_locationconfig, so a shift in a region with no
|
||||
// config row would be offered in the dropdown and then hide every rider put
|
||||
// on it.
|
||||
var configs int64
|
||||
if err := r.db.Table("app_locationconfig").
|
||||
Where("applocationid = ?", shift.Applocationid).Count(&configs).Error; err != nil {
|
||||
return models.Ridershifts{}, err
|
||||
}
|
||||
if configs == 0 {
|
||||
return models.Ridershifts{}, fmt.Errorf(
|
||||
"delivery region %d is not configured, so a shift there would hide every rider on it", shift.Applocationid)
|
||||
}
|
||||
|
||||
// The dropdown labels a shift by its times alone, so a duplicate window is
|
||||
// two identical-looking choices and no way to tell which one a rider is on.
|
||||
var clash int64
|
||||
if err := r.db.Table("ridershifts").
|
||||
Where("applocationid = ? AND starttime = ? AND endtime = ?",
|
||||
shift.Applocationid, shift.Starttime, shift.Endtime).
|
||||
Count(&clash).Error; err != nil {
|
||||
return models.Ridershifts{}, err
|
||||
}
|
||||
if clash > 0 {
|
||||
return models.Ridershifts{}, fmt.Errorf(
|
||||
"a %s-%s shift already exists in this region", shift.Starttime, shift.Endtime)
|
||||
}
|
||||
|
||||
// A column map with RETURNING, the same way CreatePartner writes its row,
|
||||
// rather than inserting the struct.
|
||||
//
|
||||
// Inserting the struct would carry `shiftid` at zero into the statement and
|
||||
// leave whether the sequence is used to how GORM reads a `Primary_Key` tag
|
||||
// written in the v1 spelling — which is exactly the kind of thing that works
|
||||
// on one driver and writes a row with id 0 on another. Naming the columns
|
||||
// removes the question: the id is the database's to assign.
|
||||
row := map[string]any{
|
||||
"applocationid": shift.Applocationid,
|
||||
"shiftdate": shift.Shiftdate,
|
||||
"starttime": shift.Starttime,
|
||||
"endtime": shift.Endtime,
|
||||
"shifthours": shift.Shifthours,
|
||||
"basefare": shift.Basefare,
|
||||
"additionalkm": shift.Additionalkm,
|
||||
"additionalcharges": shift.Additionalcharges,
|
||||
"orders": shift.Orders,
|
||||
"fuelcharge": shift.Fuelcharge,
|
||||
}
|
||||
|
||||
if err := r.db.Table("ridershifts").
|
||||
Clauses(clause.Returning{Columns: []clause.Column{{Name: "shiftid"}}}).
|
||||
Create(&row).Error; err != nil {
|
||||
return models.Ridershifts{}, err
|
||||
}
|
||||
|
||||
id, ok := row["shiftid"]
|
||||
if !ok || toInt(id) == 0 {
|
||||
// The rider form selects the new shift by id the moment this returns. A
|
||||
// shift written without one would leave the drawer selecting nothing and
|
||||
// reading as a failed save.
|
||||
return models.Ridershifts{}, errors.New("the shift was written without an id")
|
||||
}
|
||||
shift.Shiftid = toInt(id)
|
||||
|
||||
// The label the dropdown shows, built the same way GetRiderShifts builds it
|
||||
// so a shift reads identically the moment it is created and after a reload.
|
||||
shift.Shiftname = shift.Starttime + "-" + shift.Endtime
|
||||
return shift, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user