优化排班功能
This commit is contained in:
1 parent
8acbe732ff
commit
8ba15d87b3
13 files changed
+2463
-565
No files matched your search
@@ -231,6 +231,60 @@ func scDateSpan(start, end string) ([]string, string) {
|
||||
return dates, ""
|
||||
}
|
||||
|
||||
// scPlanNoEndDate 无期限方案的结束日期哨兵:数据库日期列不支持空串,
|
||||
// 统一存 2099-12-31,前端按 no_end 标记展示为「无期限」。
|
||||
const scPlanNoEndDate = "2099-12-31"
|
||||
|
||||
// scDateSpanUnbounded 展开任意长度的日期区间,供无期限方案的
|
||||
// 「当年窗口」使用,不受 scDateSpan 的 92 天上限约束。
|
||||
func scDateSpanUnbounded(start, end string) ([]string, string) {
|
||||
startDate, ok := scParseDate(start)
|
||||
if !ok {
|
||||
return nil, "开始日期格式错误"
|
||||
}
|
||||
endDate, ok := scParseDate(end)
|
||||
if !ok {
|
||||
return nil, "结束日期格式错误"
|
||||
}
|
||||
if endDate.Before(startDate) {
|
||||
return nil, "结束日期不能早于开始日期"
|
||||
}
|
||||
total := int(endDate.Sub(startDate).Hours()/24) + 1
|
||||
dates := make([]string, 0, total)
|
||||
for cursor := startDate; !cursor.After(endDate); cursor = cursor.AddDate(0, 0, 1) {
|
||||
dates = append(dates, cursor.Format("2006-01-02"))
|
||||
}
|
||||
return dates, ""
|
||||
}
|
||||
|
||||
// scIsNoEnd 判断结束日期是否表示无期限(空串或哨兵值)。
|
||||
func scIsNoEnd(end string) bool {
|
||||
return end == "" || end >= scPlanNoEndDate
|
||||
}
|
||||
|
||||
// scPlanGenWindow 计算一次预览 / 发布实际展开的日期窗口。
|
||||
// 普通方案:[start, end] 原样展开;无期限方案:从 max(start, 今天) 生成到
|
||||
// 当年 12 月 31 日,跨年不自动延续,新的一年需要重新排班。
|
||||
func scPlanGenWindow(startText, endText string) ([]string, string) {
|
||||
if !scIsNoEnd(endText) {
|
||||
return scDateSpan(startText, endText)
|
||||
}
|
||||
startDate, ok := scParseDate(startText)
|
||||
if !ok {
|
||||
return nil, "开始日期格式错误"
|
||||
}
|
||||
windowStart := startDate
|
||||
today := time.Now()
|
||||
if today.After(startDate) {
|
||||
windowStart = today
|
||||
}
|
||||
windowEnd := time.Date(today.Year(), 12, 31, 0, 0, 0, 0, today.Location())
|
||||
if windowEnd.Before(windowStart) {
|
||||
windowEnd = windowStart
|
||||
}
|
||||
return scDateSpanUnbounded(windowStart.Format("2006-01-02"), windowEnd.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
// scMinutes 把 "HH:mm" 转为当日分钟数。
|
||||
func scMinutes(value string) (int, bool) {
|
||||
if value == "" {
|
||||
|
||||
@@ -400,7 +400,11 @@ func (c *BackendOaSchedulingController) SaveEntries() {
|
||||
row.Note = note
|
||||
row.CreatedBy = uid
|
||||
row.CreatedByName = username
|
||||
if _, updateErr := models.Orm.Update(row); updateErr != nil {
|
||||
// 指定字段更新:DATE 列从库里读回 string 会带上
|
||||
// "00:00:00 +0800 CST" 后缀,全字段 Update 会写回导致 MySQL 1292
|
||||
if _, updateErr := models.Orm.Update(row,
|
||||
"shift_id", "shift_name", "short_name", "color", "work_start", "work_end",
|
||||
"source", "note", "created_by", "created_by_name"); updateErr != nil {
|
||||
c.scFail(500, 500, "调整排班失败: "+updateErr.Error())
|
||||
return
|
||||
}
|
||||
@@ -445,6 +449,205 @@ func (c *BackendOaSchedulingController) SaveEntries() {
|
||||
c.scOK(stats)
|
||||
}
|
||||
|
||||
// SaveEntryCells 批量按格子编辑排班:cells 为任意「员工 × 日期」组合(排班板多选)。
|
||||
// shift_id>0 时把这些格子设为该班次(已有排班视为调班,无排班则新增临时排班);
|
||||
// shift_id=0 时清除这些格子的排班(休息 / 请假)。
|
||||
// POST /backend/oa/scheduling/entries/save-cells
|
||||
func (c *BackendOaSchedulingController) SaveEntryCells() {
|
||||
tid, uid, username, ok := c.scOperator()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
body := c.scBody()
|
||||
|
||||
type scCellTarget struct {
|
||||
employeeID uint
|
||||
date string
|
||||
}
|
||||
rawCells, _ := body["cells"].([]interface{})
|
||||
seen := map[string]bool{}
|
||||
cells := make([]scCellTarget, 0, len(rawCells))
|
||||
for _, raw := range rawCells {
|
||||
item, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
employeeID := uint(scUintValue(item, "employee_id"))
|
||||
date := normalizeOaDate(scStringValue(item, "schedule_date", "date"))
|
||||
if employeeID == 0 || date == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := time.Parse("2006-01-02", date); err != nil {
|
||||
c.scFail(400, 400, "存在无效的日期: "+date)
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf("%d|%s", employeeID, date)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
cells = append(cells, scCellTarget{employeeID: employeeID, date: date})
|
||||
}
|
||||
if len(cells) == 0 {
|
||||
c.scFail(400, 400, "请先选择要编辑的格子")
|
||||
return
|
||||
}
|
||||
if len(cells) > scPlanMaxEntries {
|
||||
c.scFail(400, 400, fmt.Sprintf("单次最多编辑 %d 个格子", scPlanMaxEntries))
|
||||
return
|
||||
}
|
||||
|
||||
shiftID := scUintValue(body, "shift_id")
|
||||
var shift *models.OaShift
|
||||
if shiftID > 0 {
|
||||
loaded, found := c.scLoadShift(tid, shiftID)
|
||||
if !found {
|
||||
c.scFail(400, 400, "班次不存在或已停用")
|
||||
return
|
||||
}
|
||||
shift = loaded
|
||||
}
|
||||
|
||||
// 排班来源:normal=正常排班(不显示临时标记),temp=临时调整(默认,显示「临」角标)
|
||||
entrySource := models.OaEntrySourceTemp
|
||||
if scStringValue(body, "source") == "normal" {
|
||||
entrySource = models.OaEntrySourcePlan
|
||||
}
|
||||
|
||||
empIDSet := map[uint]bool{}
|
||||
empIDs := make([]uint64, 0, len(cells))
|
||||
dates := make([]string, 0, len(cells))
|
||||
for _, cell := range cells {
|
||||
if !empIDSet[cell.employeeID] {
|
||||
empIDSet[cell.employeeID] = true
|
||||
empIDs = append(empIDs, uint64(cell.employeeID))
|
||||
}
|
||||
dates = append(dates, cell.date)
|
||||
}
|
||||
var employees []models.BackendEmployee
|
||||
if _, err := c.scBaseEmployeeQuery(tid).Filter("id__in", empIDs).All(&employees); err != nil && err != orm.ErrNoRows {
|
||||
c.scFail(500, 500, "查询员工失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
employeeMap := map[uint]*models.BackendEmployee{}
|
||||
for i := range employees {
|
||||
employeeMap[employees[i].ID] = &employees[i]
|
||||
}
|
||||
|
||||
var existing []models.OaScheduleEntry
|
||||
if _, err := c.scEntryQuery(tid).
|
||||
Filter("employee_id__in", empIDs).
|
||||
Filter("schedule_date__in", dates).
|
||||
All(&existing); err != nil && err != orm.ErrNoRows {
|
||||
c.scFail(500, 500, "查询已有排班失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingMap := map[string]*models.OaScheduleEntry{}
|
||||
for i := range existing {
|
||||
key := fmt.Sprintf("%d|%s", existing[i].EmployeeID, normalizeOaDate(existing[i].ScheduleDate))
|
||||
existingMap[key] = &existing[i]
|
||||
}
|
||||
|
||||
index := c.scLoadOrgIndex(tid)
|
||||
note := scStringValue(body, "note")
|
||||
stats := map[string]int{"created": 0, "updated": 0, "cleared": 0, "skipped": 0}
|
||||
now := time.Now()
|
||||
|
||||
for _, cell := range cells {
|
||||
employee, found := employeeMap[cell.employeeID]
|
||||
if !found {
|
||||
stats["skipped"]++
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%d|%s", cell.employeeID, cell.date)
|
||||
row := existingMap[key]
|
||||
orgID := scEmployeeDepartmentID(*employee)
|
||||
position := ""
|
||||
if employee.Position != nil {
|
||||
position = *employee.Position
|
||||
}
|
||||
|
||||
// 清除排班(休息 / 请假):只处理已有排班的格子
|
||||
if shift == nil {
|
||||
if row == nil {
|
||||
stats["skipped"]++
|
||||
continue
|
||||
}
|
||||
oldShiftID, oldShiftName := row.ShiftID, row.ShiftName
|
||||
row.IsDeleted = row.ID
|
||||
row.DeleteTime = &now
|
||||
if _, updateErr := models.Orm.Update(row, "is_deleted", "delete_time"); updateErr != nil {
|
||||
c.scFail(500, 500, "清除排班失败: "+updateErr.Error())
|
||||
return
|
||||
}
|
||||
c.scWriteChange(models.Orm, tid, row.ID, row.EmployeeID, row.EmployeeName, cell.date,
|
||||
models.OaChangeClear, oldShiftID, oldShiftName, 0, "", note, uid, username)
|
||||
stats["cleared"]++
|
||||
continue
|
||||
}
|
||||
|
||||
// 已有排班:调整为目标班次(调班)
|
||||
if row != nil {
|
||||
oldShiftID, oldShiftName := row.ShiftID, row.ShiftName
|
||||
row.ShiftID = shift.ID
|
||||
row.ShiftName = shift.Name
|
||||
row.ShortName = shift.ShortName
|
||||
row.Color = shift.Color
|
||||
row.WorkStart = shift.WorkStart
|
||||
row.WorkEnd = shift.WorkEnd
|
||||
row.Source = entrySource
|
||||
row.Note = note
|
||||
row.CreatedBy = uid
|
||||
row.CreatedByName = username
|
||||
// 指定字段更新:DATE 列从库里读回 string 会带上
|
||||
// "00:00:00 +0800 CST" 后缀,全字段 Update 会写回导致 MySQL 1292
|
||||
if _, updateErr := models.Orm.Update(row,
|
||||
"shift_id", "shift_name", "short_name", "color", "work_start", "work_end",
|
||||
"source", "note", "created_by", "created_by_name"); updateErr != nil {
|
||||
c.scFail(500, 500, "调整排班失败: "+updateErr.Error())
|
||||
return
|
||||
}
|
||||
c.scWriteChange(models.Orm, tid, row.ID, row.EmployeeID, row.EmployeeName, cell.date,
|
||||
models.OaChangeShift, oldShiftID, oldShiftName, shift.ID, shift.Name, note, uid, username)
|
||||
stats["updated"]++
|
||||
continue
|
||||
}
|
||||
|
||||
// 无排班:新增临时排班(上班)
|
||||
entry := &models.OaScheduleEntry{
|
||||
Tid: int(tid),
|
||||
EmployeeID: employee.ID,
|
||||
EmployeeName: employee.Name,
|
||||
EmployeeAccount: employee.Account,
|
||||
OrgID: orgID,
|
||||
OrgName: index.name(orgID),
|
||||
Position: position,
|
||||
ScheduleDate: cell.date,
|
||||
ShiftID: shift.ID,
|
||||
ShiftName: shift.Name,
|
||||
ShortName: shift.ShortName,
|
||||
Color: shift.Color,
|
||||
WorkStart: shift.WorkStart,
|
||||
WorkEnd: shift.WorkEnd,
|
||||
Source: entrySource,
|
||||
Note: note,
|
||||
CreatedBy: uid,
|
||||
CreatedByName: username,
|
||||
}
|
||||
if _, insertErr := models.Orm.Insert(entry); insertErr != nil {
|
||||
c.scFail(500, 500, "新增排班失败: "+insertErr.Error())
|
||||
return
|
||||
}
|
||||
existingMap[key] = entry
|
||||
c.scWriteChange(models.Orm, tid, entry.ID, entry.EmployeeID, entry.EmployeeName, cell.date,
|
||||
models.OaChangeManual, 0, "", shift.ID, shift.Name, note, uid, username)
|
||||
stats["created"]++
|
||||
}
|
||||
|
||||
stats["cell_count"] = len(cells)
|
||||
c.scOK(stats)
|
||||
}
|
||||
|
||||
// SwapEntry 单条调班:把某个员工某天的班次换成另一个班次。
|
||||
// POST /backend/oa/scheduling/entries/swap/:id
|
||||
func (c *BackendOaSchedulingController) SwapEntry() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/holiday"
|
||||
)
|
||||
|
||||
// scPatternDay 排班方案周期规则的每日快照。
|
||||
@@ -212,6 +213,12 @@ func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, date
|
||||
}
|
||||
}
|
||||
|
||||
// 周期类方案:构造基准周期之外的顺延上下文(节假日 / 调休修正 + 星期节奏)
|
||||
var rollExt *scRollExt
|
||||
if plan.AlignMode == models.SchedTplAlignRoll || plan.AlignMode == models.SchedTplAlignWeek {
|
||||
rollExt = scBuildRollExt(pattern, start, dates)
|
||||
}
|
||||
|
||||
result := make(map[string]scPatternDay, len(dates))
|
||||
workdays := make([]string, 0, len(dates))
|
||||
for _, date := range dates {
|
||||
@@ -219,7 +226,7 @@ func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, date
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
rule, work := scRuleForDate(plan, rules, fallback, start, day, weekdays)
|
||||
rule, work := scRuleForDate(plan, rules, fallback, start, day, weekdays, rollExt)
|
||||
if !work {
|
||||
continue
|
||||
}
|
||||
@@ -229,9 +236,109 @@ func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, date
|
||||
return result, workdays, ""
|
||||
}
|
||||
|
||||
// scRollExt 周期滚动方案(AlignMode=2,按月涂排)在基准周期之外的顺延上下文。
|
||||
//
|
||||
// 涂刷基准月里的节假日安排属于「基准月的特殊规则」,不应该跟着日序号平移到
|
||||
// 后续月份——否则 10 月的国庆假期会错位落到 11 月的普通工作日上。周期之外的
|
||||
// 推算规则:
|
||||
// 1. 目标日期是法定节假日 → 休息;
|
||||
// 2. 目标日期是调休上班日 → 上班(取涂刷里的班次);
|
||||
// 3. 该星期在涂刷周期内呈现严格 休/班 交替(如大小周的周六)→ 按周序奇偶交替顺延;
|
||||
// 4. 其余按涂刷月的星期节奏顺延(同星期几在周期内的多数状态)。
|
||||
type scRollExt struct {
|
||||
weekdayRules [8]scPatternDay // 下标 1-7(周一~周日):该星期的顺延规则
|
||||
altRules [8]bool // 该星期是否隔周交替(严格 休/班 交替)
|
||||
altEven [8]scPatternDay // 交替星期:周序为偶数时的规则
|
||||
altOdd [8]scPatternDay // 交替星期:周序为奇数时的规则
|
||||
defaultWork scPatternDay // 涂刷里的首个上班规则,供调休上班日使用
|
||||
holidays map[string]int8 // 目标日期 → holiday.Type(0 调休上班 / 1 法定放假)
|
||||
}
|
||||
|
||||
// scBuildRollExt 从涂刷周期构造顺延上下文。星期节奏取多数状态(work 数 - rest 数
|
||||
// ≥ 0 视为上班,班次取该星期首个上班规则);若某星期在周期内严格 休/班 交替,
|
||||
// 记录交替规则,顺延按周序奇偶保持交替(周序锚点 = start 所在周的周一)。
|
||||
// 节假日数据按目标区间涉及的年份拉取,拉取失败只是少了节假日修正,不阻断发布。
|
||||
func scBuildRollExt(pattern []scPatternDay, start time.Time, dates []string) *scRollExt {
|
||||
ext := &scRollExt{holidays: map[string]int8{}}
|
||||
workCount := [8]int{}
|
||||
shiftBy := [8]scPatternDay{}
|
||||
type weekSeqItem struct {
|
||||
week int
|
||||
work bool
|
||||
item scPatternDay
|
||||
}
|
||||
byWeekday := [8][]weekSeqItem{}
|
||||
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
|
||||
for _, item := range pattern {
|
||||
day := start.AddDate(0, 0, item.DayIndex)
|
||||
w := scWeekdayOffset(day) + 1
|
||||
if item.DayType == models.SchedTplDayWork {
|
||||
workCount[w]++
|
||||
if shiftBy[w].ShiftID == 0 {
|
||||
shiftBy[w] = item
|
||||
}
|
||||
if ext.defaultWork.ShiftID == 0 {
|
||||
ext.defaultWork = item
|
||||
}
|
||||
} else {
|
||||
workCount[w]--
|
||||
}
|
||||
weekSeq := int(day.AddDate(0, 0, -scWeekdayOffset(day)).Sub(startMonday).Hours()/24) / 7
|
||||
byWeekday[w] = append(byWeekday[w], weekSeqItem{week: weekSeq, work: item.DayType == models.SchedTplDayWork, item: item})
|
||||
}
|
||||
for w := 1; w <= 7; w++ {
|
||||
if workCount[w] >= 0 && shiftBy[w].ShiftID != 0 {
|
||||
ext.weekdayRules[w] = shiftBy[w]
|
||||
} else {
|
||||
ext.weekdayRules[w] = scPatternDay{DayIndex: w - 1, DayType: models.SchedTplDayRest}
|
||||
}
|
||||
// 隔周交替检测:该星期在涂刷周期内严格 休/班 交替(如大小周的周六,
|
||||
// 一周上班一周休息),顺延时按周序奇偶保持交替,而不是取多数
|
||||
list := byWeekday[w]
|
||||
if len(list) < 2 {
|
||||
continue
|
||||
}
|
||||
alternating := true
|
||||
for i := 1; i < len(list); i++ {
|
||||
if list[i].work == list[i-1].work {
|
||||
alternating = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alternating {
|
||||
continue
|
||||
}
|
||||
ext.altRules[w] = true
|
||||
if list[0].week%2 == 0 {
|
||||
ext.altEven[w] = list[0].item
|
||||
ext.altOdd[w] = list[1].item
|
||||
} else {
|
||||
ext.altOdd[w] = list[0].item
|
||||
ext.altEven[w] = list[1].item
|
||||
}
|
||||
}
|
||||
years := map[int]bool{}
|
||||
for _, date := range dates {
|
||||
if y := holidayYear(date); y > 0 {
|
||||
years[y] = true
|
||||
}
|
||||
}
|
||||
for y := range years {
|
||||
items, _, err := holiday.FetchYear(y)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, d := range items {
|
||||
ext.holidays[d.Date] = d.Type
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
// scRuleForDate 推算某一天命中的规则;第二个返回值表示当天是否上班。
|
||||
// ext 是周期滚动方案(AlignMode=2)在基准周期之外的顺延上下文,可为 nil。
|
||||
func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fallback *models.OaShift,
|
||||
start, day time.Time, weekdays map[int]bool) (scPatternDay, bool) {
|
||||
start, day time.Time, weekdays map[int]bool, ext *scRollExt) (scPatternDay, bool) {
|
||||
switch plan.AlignMode {
|
||||
case models.SchedTplAlignWeek:
|
||||
weeks := plan.CycleDays / 7
|
||||
@@ -241,6 +348,18 @@ func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fall
|
||||
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
|
||||
curMonday := day.AddDate(0, 0, -scWeekdayOffset(day))
|
||||
diffWeeks := int(curMonday.Sub(startMonday).Hours()/24) / 7
|
||||
// 首个周期内原样回放;周期之外目标月的节假日 / 调休优先修正
|
||||
//(大小周等模板按自然周循环,周序永远交替不串位)
|
||||
if ext != nil && diffWeeks >= weeks {
|
||||
if hType, ok := ext.holidays[day.Format("2006-01-02")]; ok {
|
||||
if hType == holiday.TypeHoliday {
|
||||
return scPatternDay{}, false
|
||||
}
|
||||
if hType == holiday.TypeWorkday && ext.defaultWork.ShiftID != 0 {
|
||||
return ext.defaultWork, true
|
||||
}
|
||||
}
|
||||
}
|
||||
weekIndex := scMod(diffWeeks+int(plan.WeekOffset), weeks)
|
||||
return scRuleOfDay(rules, weekIndex*7+scWeekdayOffset(day))
|
||||
|
||||
@@ -250,6 +369,41 @@ func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fall
|
||||
cycle = 1
|
||||
}
|
||||
diffDays := int(day.Sub(start).Hours() / 24)
|
||||
// 基准周期内:按涂刷结果原样回放(涂刷月自己的节假日 / 周末已在保存时处理)
|
||||
if diffDays < cycle || ext == nil {
|
||||
return scRuleOfDay(rules, scMod(diffDays, cycle))
|
||||
}
|
||||
// 周期之外:先看目标日的节假日 / 调休特殊规则
|
||||
if hType, ok := ext.holidays[day.Format("2006-01-02")]; ok {
|
||||
if hType == holiday.TypeHoliday {
|
||||
return scPatternDay{}, false
|
||||
}
|
||||
if hType == holiday.TypeWorkday && ext.defaultWork.ShiftID != 0 {
|
||||
return ext.defaultWork, true
|
||||
}
|
||||
}
|
||||
// 整月涂刷方案(周期=开始月天数):按星期节奏顺延,规避节假日 / 周末错位;
|
||||
// 其他滚动周期(三班倒 / 做一休一等模板原生周期):保持相位纯滚动
|
||||
daysInStartMonth := time.Date(start.Year(), start.Month()+1, 0, 0, 0, 0, 0, start.Location()).Day()
|
||||
if cycle == daysInStartMonth {
|
||||
w := scWeekdayOffset(day) + 1
|
||||
rule := ext.weekdayRules[w]
|
||||
// 隔周交替的星期(如大小周的周六):按周序奇偶保持交替
|
||||
if ext.altRules[w] {
|
||||
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
|
||||
curMonday := day.AddDate(0, 0, -scWeekdayOffset(day))
|
||||
weekSeq := int(curMonday.Sub(startMonday).Hours()/24) / 7
|
||||
if weekSeq%2 == 0 {
|
||||
rule = ext.altEven[w]
|
||||
} else {
|
||||
rule = ext.altOdd[w]
|
||||
}
|
||||
}
|
||||
if rule.DayType != models.SchedTplDayWork {
|
||||
return scPatternDay{}, false
|
||||
}
|
||||
return rule, true
|
||||
}
|
||||
return scRuleOfDay(rules, scMod(diffDays, cycle))
|
||||
|
||||
case models.SchedTplAlignMonth:
|
||||
|
||||
@@ -98,7 +98,13 @@ func (c *BackendOaSchedulingController) scPlanInputFromBody(body map[string]inte
|
||||
input.IncludeChild = scBoolValue(body, "include_child")
|
||||
}
|
||||
|
||||
if _, msg := scDateSpan(input.StartDate, input.EndDate); msg != "" {
|
||||
// 无期限方案:结束日期允许为空,落库时存哨兵值,发布时按滚动窗口分批生成
|
||||
if scIsNoEnd(input.EndDate) {
|
||||
if _, ok := scParseDate(input.StartDate); !ok {
|
||||
return nil, "开始日期格式错误"
|
||||
}
|
||||
input.EndDate = scPlanNoEndDate
|
||||
} else if _, msg := scDateSpan(input.StartDate, input.EndDate); msg != "" {
|
||||
return nil, msg
|
||||
}
|
||||
|
||||
@@ -181,6 +187,7 @@ func scPlanView(plan *models.OaSchedulePlan) map[string]interface{} {
|
||||
"employee_names": plan.EmployeeNames,
|
||||
"start_date": normalizeOaDate(plan.StartDate),
|
||||
"end_date": normalizeOaDate(plan.EndDate),
|
||||
"no_end": scIsNoEnd(plan.EndDate),
|
||||
"weekdays": weekdays,
|
||||
"shift_id": plan.ShiftID,
|
||||
"shift_name": plan.ShiftName,
|
||||
@@ -330,7 +337,7 @@ func (c *BackendOaSchedulingController) PlanPreview() {
|
||||
return
|
||||
}
|
||||
|
||||
dates, msg := scDateSpan(input.StartDate, input.EndDate)
|
||||
dates, msg := scPlanGenWindow(input.StartDate, input.EndDate)
|
||||
if msg != "" {
|
||||
c.scFail(400, 400, msg)
|
||||
return
|
||||
@@ -369,6 +376,9 @@ func (c *BackendOaSchedulingController) PlanPreview() {
|
||||
"workday_count": len(workdays),
|
||||
"rest_count": len(dates) - len(workdays),
|
||||
"entry_count": entryCount,
|
||||
"no_end": scIsNoEnd(input.EndDate),
|
||||
"gen_start": dates[0],
|
||||
"gen_end": dates[len(dates)-1],
|
||||
"shift": shift,
|
||||
"scope_name": c.scScopeName(tid, input),
|
||||
"template_name": input.TemplateName,
|
||||
@@ -673,7 +683,7 @@ func (c *BackendOaSchedulingController) scPublishPlan(tid, uid uint64, username
|
||||
return nil, fmt.Errorf("范围覆盖 %d 名员工,超过单次 %d 人上限", len(employees), scPlanMaxEmployees)
|
||||
}
|
||||
|
||||
dates, msg := scDateSpan(normalizeOaDate(plan.StartDate), normalizeOaDate(plan.EndDate))
|
||||
dates, msg := scPlanGenWindow(normalizeOaDate(plan.StartDate), normalizeOaDate(plan.EndDate))
|
||||
if msg != "" {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
@@ -121,71 +121,63 @@ func (c *BackendOaSchedulingController) TemplateApply() {
|
||||
c.scFail(500, 500, "查询班次失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 班次匹配索引:精确(名称+时间)→ 名称 → 兜底(默认班次 / 第一个可用班次)。
|
||||
// 不强制时间一致,也不自动创建班次,由用户在方案里自行判断调整
|
||||
shiftMap := make(map[string]*models.OaShift, len(shifts))
|
||||
maxSort := 0
|
||||
shiftNameMap := make(map[string]*models.OaShift, len(shifts))
|
||||
var fallbackShift *models.OaShift
|
||||
for i := range shifts {
|
||||
shift := &shifts[i]
|
||||
shiftMap[schedShiftKey(shift.Name, shift.WorkStart, shift.WorkEnd)] = shift
|
||||
if shift.Sort > maxSort {
|
||||
maxSort = shift.Sort
|
||||
if _, ok := shiftNameMap[shift.Name]; !ok {
|
||||
shiftNameMap[shift.Name] = shift
|
||||
}
|
||||
if fallbackShift == nil || (shift.IsDefault == 1 && fallbackShift.IsDefault != 1) {
|
||||
fallbackShift = shift
|
||||
}
|
||||
}
|
||||
|
||||
patterns := []scPatternDay{}
|
||||
createdShifts := 0
|
||||
matchedShifts := 0
|
||||
for _, day := range c.schedTplDays(tpl.ID) {
|
||||
if day.DayType != models.SchedTplDayWork {
|
||||
patterns = append(patterns, scPatternDay{DayIndex: day.DayIndex, DayType: models.SchedTplDayRest})
|
||||
continue
|
||||
}
|
||||
key := schedShiftKey(day.ShiftName, day.WorkStart, day.WorkEnd)
|
||||
shift, hit := shiftMap[key]
|
||||
if hit {
|
||||
matchedShifts++
|
||||
} else {
|
||||
maxSort += 10
|
||||
shift = &models.OaShift{
|
||||
Tid: int(tid),
|
||||
Name: day.ShiftName,
|
||||
ShortName: day.ShiftShort,
|
||||
Color: day.Color,
|
||||
WorkStart: day.WorkStart,
|
||||
WorkEnd: day.WorkEnd,
|
||||
RestStart: day.RestStart,
|
||||
RestEnd: day.RestEnd,
|
||||
WorkHours: day.WorkHours,
|
||||
IsDefault: 0,
|
||||
Status: 1,
|
||||
Sort: maxSort,
|
||||
IsDeleted: 0,
|
||||
}
|
||||
if shift.ShortName == "" {
|
||||
shift.ShortName = shift.Name
|
||||
}
|
||||
if shift.Color == "" {
|
||||
shift.Color = "#409EFF"
|
||||
}
|
||||
if _, err := models.Orm.Insert(shift); err != nil {
|
||||
c.scFail(500, 500, "创建班次失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
shiftMap[key] = shift
|
||||
createdShifts++
|
||||
shift := shiftMap[schedShiftKey(day.ShiftName, day.WorkStart, day.WorkEnd)]
|
||||
if shift == nil {
|
||||
shift = shiftNameMap[strings.TrimSpace(day.ShiftName)]
|
||||
}
|
||||
patterns = append(patterns, scPatternDay{
|
||||
if shift == nil {
|
||||
shift = fallbackShift
|
||||
}
|
||||
if shift != nil {
|
||||
matchedShifts++
|
||||
}
|
||||
item := scPatternDay{
|
||||
DayIndex: day.DayIndex,
|
||||
DayType: models.SchedTplDayWork,
|
||||
ShiftID: shift.ID,
|
||||
ShiftName: shift.Name,
|
||||
ShortName: shift.ShortName,
|
||||
Color: shift.Color,
|
||||
WorkStart: shift.WorkStart,
|
||||
WorkEnd: shift.WorkEnd,
|
||||
RestStart: shift.RestStart,
|
||||
RestEnd: shift.RestEnd,
|
||||
WorkHours: shift.WorkHours,
|
||||
})
|
||||
ShiftName: day.ShiftName,
|
||||
ShortName: day.ShiftShort,
|
||||
Color: day.Color,
|
||||
WorkStart: day.WorkStart,
|
||||
WorkEnd: day.WorkEnd,
|
||||
RestStart: day.RestStart,
|
||||
RestEnd: day.RestEnd,
|
||||
WorkHours: day.WorkHours,
|
||||
}
|
||||
if shift != nil {
|
||||
item.ShiftID = shift.ID
|
||||
item.ShiftName = shift.Name
|
||||
item.ShortName = shift.ShortName
|
||||
item.Color = shift.Color
|
||||
item.WorkStart = shift.WorkStart
|
||||
item.WorkEnd = shift.WorkEnd
|
||||
item.RestStart = shift.RestStart
|
||||
item.RestEnd = shift.RestEnd
|
||||
item.WorkHours = shift.WorkHours
|
||||
}
|
||||
patterns = append(patterns, item)
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
c.scFail(400, 400, "该模板没有配置排班规则")
|
||||
@@ -196,7 +188,6 @@ func (c *BackendOaSchedulingController) TemplateApply() {
|
||||
"template": schedTplView(tpl, nil),
|
||||
"pattern": patterns,
|
||||
"days": patterns,
|
||||
"created_shift": createdShifts,
|
||||
"matched_shift": matchedShifts,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user