package controllers import ( "fmt" "sort" "strconv" "strings" "time" "server/models" "github.com/beego/beego/v2/client/orm" ) const scBoardMaxDays = 62 // scEntryView 看板单元格里的排班信息。 type scEntryView struct { ID uint64 `json:"id"` PlanID uint64 `json:"plan_id"` ShiftID uint64 `json:"shift_id"` ShiftName string `json:"shift_name"` ShortName string `json:"short_name"` Color string `json:"color"` WorkStart string `json:"work_start"` WorkEnd string `json:"work_end"` Source int8 `json:"source"` Note string `json:"note"` } type scBoardRow struct { ID uint `json:"id"` Name string `json:"name"` Account string `json:"account"` OrgID uint64 `json:"org_id"` OrgName string `json:"org_name"` Position string `json:"position"` Days map[string]scEntryView `json:"days"` } type scDateCell struct { Date string `json:"date"` Weekday int `json:"weekday"` IsToday bool `json:"is_today"` } func (c *BackendOaSchedulingController) scEntryQuery(tid uint64) orm.QuerySeter { return models.Orm.QueryTable(new(models.OaScheduleEntry)). Filter("tid", tid). Filter("is_deleted", 0) } // Board 排班看板:按员工 × 日期返回区间内的排班矩阵。 // GET /backend/oa/scheduling/board?start_date=&end_date=&org_id=&include_child=1&keyword= func (c *BackendOaSchedulingController) Board() { tid, _, _, ok := c.scOperator() if !ok { return } startDate := strings.TrimSpace(c.GetString("start_date")) endDate := strings.TrimSpace(c.GetString("end_date")) if startDate == "" || endDate == "" { // 默认展示本周(周一至周日) now := time.Now() weekday := int(now.Weekday()) if weekday == 0 { weekday = 7 } monday := now.AddDate(0, 0, -(weekday - 1)) startDate = monday.Format("2006-01-02") endDate = monday.AddDate(0, 0, 6).Format("2006-01-02") } dates, msg := scDateSpan(startDate, endDate) if msg != "" { c.scFail(400, 400, msg) return } if len(dates) > scBoardMaxDays { c.scFail(400, 400, fmt.Sprintf("看板一次最多展示 %d 天", scBoardMaxDays)) return } orgID := uint64(0) if raw := strings.TrimSpace(c.GetString("org_id")); raw != "" { if parsed, err := strconvParseUint(raw); err == nil { orgID = parsed } } includeChild := c.GetString("include_child") != "0" keyword := strings.TrimSpace(c.GetString("keyword")) onlyScheduled := c.GetString("only_scheduled") == "1" shiftFilter := uint64(0) if raw := strings.TrimSpace(c.GetString("shift_id")); raw != "" { if parsed, err := strconvParseUint(raw); err == nil { shiftFilter = parsed } } employees, err := c.scEmployeesByOrg(tid, orgID, includeChild) if err != nil { c.scFail(500, 500, "查询员工失败: "+err.Error()) return } index := c.scLoadOrgIndex(tid) rows := make([]scBoardRow, 0, len(employees)) employeeIDs := make([]uint64, 0, len(employees)) for _, employee := range employees { if keyword != "" && !strings.Contains(employee.Name, keyword) && !strings.Contains(employee.Account, keyword) { continue } orgID := scEmployeeDepartmentID(employee) position := "" if employee.Position != nil { position = *employee.Position } rows = append(rows, scBoardRow{ ID: employee.ID, Name: employee.Name, Account: employee.Account, OrgID: orgID, OrgName: index.name(orgID), Position: position, Days: map[string]scEntryView{}, }) employeeIDs = append(employeeIDs, uint64(employee.ID)) } truncated := false const boardMaxEmployees = 500 if len(rows) > boardMaxEmployees { rows = rows[:boardMaxEmployees] employeeIDs = employeeIDs[:boardMaxEmployees] truncated = true } if len(employeeIDs) > 0 { var entries []models.OaScheduleEntry _, err = c.scEntryQuery(tid). Filter("employee_id__in", employeeIDs). Filter("schedule_date__gte", dates[0]). Filter("schedule_date__lte", dates[len(dates)-1]). All(&entries) if err != nil && err != orm.ErrNoRows { c.scFail(500, 500, "查询排班失败: "+err.Error()) return } rowIndex := map[uint]*scBoardRow{} for i := range rows { rowIndex[rows[i].ID] = &rows[i] } for _, entry := range entries { if shiftFilter > 0 && entry.ShiftID != shiftFilter { continue } row, hit := rowIndex[entry.EmployeeID] if !hit { continue } row.Days[normalizeOaDate(entry.ScheduleDate)] = scEntryView{ ID: entry.ID, PlanID: entry.PlanID, ShiftID: entry.ShiftID, ShiftName: entry.ShiftName, ShortName: entry.ShortName, Color: entry.Color, WorkStart: entry.WorkStart, WorkEnd: entry.WorkEnd, Source: entry.Source, Note: entry.Note, } } } if onlyScheduled { filtered := rows[:0] for _, row := range rows { if len(row.Days) > 0 { filtered = append(filtered, row) } } rows = filtered } today := time.Now().Format("2006-01-02") dateCells := make([]scDateCell, 0, len(dates)) for _, date := range dates { parsed, _ := scParseDate(date) weekday := int(parsed.Weekday()) if weekday == 0 { weekday = 7 } dateCells = append(dateCells, scDateCell{Date: date, Weekday: weekday, IsToday: date == today}) } // 汇总:排班人次、临时调配数、未排班人数、今日在岗、班次分布 entryCount, tempCount, unassigned := 0, 0, 0 todayOnDuty := 0 shiftCounter := map[string]int{} shiftColor := map[string]string{} for _, row := range rows { if len(row.Days) == 0 { unassigned++ } for date, entry := range row.Days { entryCount++ if entry.Source == models.OaEntrySourceTemp { tempCount++ } if date == today { todayOnDuty++ } name := entry.ShiftName if name == "" { name = "未命名班次" } shiftCounter[name]++ shiftColor[name] = entry.Color } } shiftDist := make([]map[string]interface{}, 0, len(shiftCounter)) for name, count := range shiftCounter { shiftDist = append(shiftDist, map[string]interface{}{ "name": name, "count": count, "color": shiftColor[name], }) } sort.Slice(shiftDist, func(i, j int) bool { return shiftDist[i]["count"].(int) > shiftDist[j]["count"].(int) }) var shifts []models.OaShift _, _ = c.shiftQuery(tid).Filter("status", 1).OrderBy("sort", "id").All(&shifts) if shifts == nil { shifts = []models.OaShift{} } c.scOK(map[string]interface{}{ "start_date": startDate, "end_date": endDate, "dates": dateCells, "employees": rows, "shifts": shifts, "truncated": truncated, "summary": map[string]interface{}{ "employee_count": len(rows), "entry_count": entryCount, "temp_count": tempCount, "unassigned": unassigned, "today_on_duty": todayOnDuty, "shift_dist": shiftDist, }, }) } func strconvParseUint(value string) (uint64, error) { return strconv.ParseUint(strings.TrimSpace(value), 10, 64) } // scEntryTargets 解析临时排班/批量清除的目标员工与日期。 func (c *BackendOaSchedulingController) scEntryTargets(tid uint64, body map[string]interface{}) ([]models.BackendEmployee, []string, string) { employeeIDs := scUintSlice(body, "employee_ids") if len(employeeIDs) == 0 { return nil, nil, "请选择员工" } if len(employeeIDs) > scPlanMaxEmployees { return nil, nil, fmt.Sprintf("单次最多操作 %d 名员工", scPlanMaxEmployees) } startDate := scStringValue(body, "start_date", "schedule_date", "date") endDate := scStringValue(body, "end_date") if endDate == "" { endDate = startDate } dates, msg := scDateSpan(startDate, endDate) if msg != "" { return nil, nil, msg } workdays := scWorkdays(dates, scWeekdaySet(body)) if len(workdays) == 0 { return nil, nil, "所选日期区间内没有符合条件的日期" } var employees []models.BackendEmployee _, err := c.scBaseEmployeeQuery(tid).Filter("id__in", employeeIDs).All(&employees) if err != nil && err != orm.ErrNoRows { return nil, nil, "查询员工失败: " + err.Error() } if len(employees) == 0 { return nil, nil, "员工不存在或已离职" } if len(employees)*len(workdays) > scPlanMaxEntries { return nil, nil, fmt.Sprintf("本次将产生 %d 条排班,超过单次 %d 条上限", len(employees)*len(workdays), scPlanMaxEntries) } return employees, workdays, "" } // SaveEntries 临时排班:给一个或多个员工在指定日期/星期安排班次。 // shift_id=0 时表示清除这些日期的排班。 // POST /backend/oa/scheduling/entries/save func (c *BackendOaSchedulingController) SaveEntries() { tid, uid, username, ok := c.scOperator() if !ok { return } body := c.scBody() employees, workdays, msg := c.scEntryTargets(tid, body) if msg != "" { c.scFail(400, 400, msg) 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 } note := scStringValue(body, "note") overwrite := true if _, has := body["overwrite"]; has { overwrite = scBoolValue(body, "overwrite") } empIDs := make([]uint64, 0, len(employees)) for _, employee := range employees { empIDs = append(empIDs, uint64(employee.ID)) } var existing []models.OaScheduleEntry _, err := c.scEntryQuery(tid). Filter("employee_id__in", empIDs). Filter("schedule_date__gte", workdays[0]). Filter("schedule_date__lte", workdays[len(workdays)-1]). All(&existing) if 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) stats := map[string]int{"created": 0, "updated": 0, "cleared": 0, "skipped": 0} now := time.Now() for _, employee := range employees { orgID := scEmployeeDepartmentID(employee) position := "" if employee.Position != nil { position = *employee.Position } for _, date := range workdays { key := fmt.Sprintf("%d|%s", employee.ID, date) row := existingMap[key] // shift_id=0:清除排班 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, date, models.OaChangeClear, oldShiftID, oldShiftName, 0, "", note, uid, username) delete(existingMap, key) stats["cleared"]++ continue } if row != nil { if !overwrite { stats["skipped"]++ continue } 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 = models.OaEntrySourceTemp 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, 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: date, ShiftID: shift.ID, ShiftName: shift.Name, ShortName: shift.ShortName, Color: shift.Color, WorkStart: shift.WorkStart, WorkEnd: shift.WorkEnd, Source: models.OaEntrySourceTemp, 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, date, models.OaChangeManual, 0, "", shift.ID, shift.Name, note, uid, username) stats["created"]++ } } stats["entry_count"] = stats["created"] + stats["updated"] + stats["cleared"] 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() { tid, uid, username, ok := c.scOperator() if !ok { return } id, valid := c.scPathID(":id") if !valid { c.scFail(400, 400, "无效的排班ID") return } body := c.scBody() shiftID := scUintValue(body, "shift_id") if shiftID == 0 { c.scFail(400, 400, "请选择调换后的班次") return } shift, found := c.scLoadShift(tid, shiftID) if !found { c.scFail(400, 400, "班次不存在或已停用") return } var entry models.OaScheduleEntry if err := c.scEntryQuery(tid).Filter("id", id).One(&entry); err != nil { c.scFail(404, 404, "排班记录不存在") return } if entry.ShiftID == shift.ID { c.scFail(400, 400, "该排班已经是目标班次") return } reason := scStringValue(body, "reason") oldShiftID, oldShiftName := entry.ShiftID, entry.ShiftName entry.ShiftID = shift.ID entry.ShiftName = shift.Name entry.ShortName = shift.ShortName entry.Color = shift.Color entry.WorkStart = shift.WorkStart entry.WorkEnd = shift.WorkEnd entry.Source = models.OaEntrySourceTemp if reason != "" { entry.Note = reason } if _, err := models.Orm.Update(&entry); err != nil { c.scFail(500, 500, "调班失败: "+err.Error()) return } c.scWriteChange(models.Orm, tid, entry.ID, entry.EmployeeID, entry.EmployeeName, normalizeOaDate(entry.ScheduleDate), models.OaChangeShift, oldShiftID, oldShiftName, shift.ID, shift.Name, reason, uid, username) c.scOK(map[string]interface{}{"entry": entry, "old_shift_name": oldShiftName}) } // ClearEntries 批量清除排班:按明细 ID,或按员工 + 日期区间。 // POST /backend/oa/scheduling/entries/clear func (c *BackendOaSchedulingController) ClearEntries() { tid, uid, username, ok := c.scOperator() if !ok { return } body := c.scBody() entryIDs := scUintSlice(body, "entry_ids") var entries []models.OaScheduleEntry if len(entryIDs) > 0 { _, err := c.scEntryQuery(tid).Filter("id__in", entryIDs).All(&entries) if err != nil && err != orm.ErrNoRows { c.scFail(500, 500, "查询排班失败: "+err.Error()) return } } else { employees, workdays, msg := c.scEntryTargets(tid, body) if msg != "" { c.scFail(400, 400, msg) return } empIDs := make([]uint64, 0, len(employees)) for _, employee := range employees { empIDs = append(empIDs, uint64(employee.ID)) } _, err := c.scEntryQuery(tid). Filter("employee_id__in", empIDs). Filter("schedule_date__in", workdays). All(&entries) if err != nil && err != orm.ErrNoRows { c.scFail(500, 500, "查询排班失败: "+err.Error()) return } } if len(entries) == 0 { c.scOK(map[string]interface{}{"cleared": 0}) return } if len(entries) > scPlanMaxEntries { c.scFail(400, 400, fmt.Sprintf("单次最多清除 %d 条排班", scPlanMaxEntries)) return } reason := scStringValue(body, "reason") now := time.Now() tx, err := orm.NewOrm().Begin() if err != nil { c.scFail(500, 500, "清除排班失败: "+err.Error()) return } for i := range entries { entry := &entries[i] oldShiftID, oldShiftName := entry.ShiftID, entry.ShiftName entry.IsDeleted = entry.ID entry.DeleteTime = &now if _, err := tx.Update(entry, "is_deleted", "delete_time"); err != nil { _ = tx.Rollback() c.scFail(500, 500, "清除排班失败: "+err.Error()) return } c.scWriteChange(tx, tid, entry.ID, entry.EmployeeID, entry.EmployeeName, normalizeOaDate(entry.ScheduleDate), models.OaChangeClear, oldShiftID, oldShiftName, 0, "", reason, uid, username) } if err := tx.Commit(); err != nil { c.scFail(500, 500, "清除排班失败: "+err.Error()) return } c.scOK(map[string]interface{}{"cleared": len(entries)}) } // MyBoard 当前登录用户的排班:按登录账号匹配员工档案,返回区间内每天的排班, // 供日程日历渲染上班/休息背景。 // GET /backend/oa/scheduling/my?start_date=&end_date= func (c *BackendOaSchedulingController) MyBoard() { tid, _, username, ok := c.scOperator() if !ok { return } startDate := strings.TrimSpace(c.GetString("start_date")) endDate := strings.TrimSpace(c.GetString("end_date")) if startDate == "" || endDate == "" { now := time.Now() startDate = now.Format("2006-01-02") endDate = startDate } dates, msg := scDateSpan(startDate, endDate) if msg != "" { c.scFail(400, 400, msg) return } if len(dates) > scBoardMaxDays { c.scFail(400, 400, fmt.Sprintf("一次最多查询 %d 天", scBoardMaxDays)) return } // 账号未关联员工档案时返回空列表,前端静默不渲染排班背景 var employee models.BackendEmployee if err := c.scBaseEmployeeQuery(tid).Filter("account", username).One(&employee); err != nil { c.scOK(map[string]interface{}{"employee": nil, "list": []map[string]interface{}{}}) return } var entries []models.OaScheduleEntry _, _ = c.scEntryQuery(tid). Filter("employee_id", employee.ID). Filter("schedule_date__gte", startDate). Filter("schedule_date__lte", endDate). OrderBy("schedule_date"). All(&entries) list := make([]map[string]interface{}, 0, len(entries)) for i := range entries { e := entries[i] list = append(list, map[string]interface{}{ "date": e.ScheduleDate, "shift_id": e.ShiftID, "shift_name": e.ShiftName, "short_name": e.ShortName, "color": e.Color, "work_start": e.WorkStart, "work_end": e.WorkEnd, "note": e.Note, }) } c.scOK(map[string]interface{}{ "employee": map[string]interface{}{"id": employee.ID, "name": employee.Name}, "list": list, }) } // ChangeList 排班变更记录(最近 100 条),用于追溯临时调配。 // GET /backend/oa/scheduling/changes?start_date=&end_date=&employee_id= func (c *BackendOaSchedulingController) ChangeList() { tid, _, _, ok := c.scOperator() if !ok { return } qs := models.Orm.QueryTable(new(models.OaScheduleChange)).Filter("tid", tid) if start := strings.TrimSpace(c.GetString("start_date")); isValidOaDate(start) { qs = qs.Filter("schedule_date__gte", start) } if end := strings.TrimSpace(c.GetString("end_date")); isValidOaDate(end) { qs = qs.Filter("schedule_date__lte", end) } if raw := strings.TrimSpace(c.GetString("employee_id")); raw != "" { if employeeID, err := strconvParseUint(raw); err == nil { qs = qs.Filter("employee_id", employeeID) } } var rows []models.OaScheduleChange _, err := qs.OrderBy("-id").Limit(100).All(&rows) if err != nil && err != orm.ErrNoRows { c.scFail(500, 500, "查询变更记录失败: "+err.Error()) return } if rows == nil { rows = []models.OaScheduleChange{} } for i := range rows { rows[i].ScheduleDate = normalizeOaDate(rows[i].ScheduleDate) } c.scOK(map[string]interface{}{"list": rows}) } // ScheduleEmployeeOptions 排班可选员工(供“指定员工”范围选择)。 // GET /backend/oa/scheduling/employees?org_id=&include_child=1&keyword= func (c *BackendOaSchedulingController) ScheduleEmployeeOptions() { tid, _, _, ok := c.scOperator() if !ok { return } orgID := uint64(0) if raw := strings.TrimSpace(c.GetString("org_id")); raw != "" { if parsed, err := strconvParseUint(raw); err == nil { orgID = parsed } } includeChild := c.GetString("include_child") != "0" keyword := strings.TrimSpace(c.GetString("keyword")) employees, err := c.scEmployeesByOrg(tid, orgID, includeChild) if err != nil { c.scFail(500, 500, "查询员工失败: "+err.Error()) return } list := c.scEmployeeDTOs(tid, employees) if keyword != "" { filtered := make([]schedulingEmployee, 0, len(list)) for _, item := range list { if strings.Contains(item.Name, keyword) || strings.Contains(item.Account, keyword) { filtered = append(filtered, item) } } list = filtered } if list == nil { list = []schedulingEmployee{} } c.scOK(map[string]interface{}{"list": list, "total": len(list)}) }