优化排班功能

This commit is contained in:
hero920103 committed 2026-11-05 18:00:50 +08:00
1 parent 8acbe732ff
commit 8ba15d87b3
13 files changed
+2463 -565

No files matched your search

+204 -1
View File
@@ -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() {