feat: add calendar scheduling feature for agent teams
- Implemented FindCalendarSchedules method in agentTeamScheduleService to retrieve schedules within a specified time range. - Added new API endpoint fetchAgentTeamScheduleCalendar to fetch schedules for the calendar view. - Created ScheduleCalendar component to display agent team schedules in a calendar format with drag-and-drop functionality for moving and resizing schedules. - Updated EditDialog component to support deleting schedules and handling default values for new entries. - Enhanced DashboardAgentTeamSchedulesPage to toggle between calendar and list views, and to load calendar data based on selected week. - Added tests for FindCalendarSchedules to ensure correct schedule retrieval and validation of time ranges.
This commit is contained in:
@@ -31,6 +31,28 @@ func (c *AgentTeamScheduleController) AnyList() *web.JsonResult {
|
||||
return web.JsonData(&web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func (c *AgentTeamScheduleController) AnyCalendar() *web.JsonResult {
|
||||
if _, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionAgentTeamScheduleView); err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
startAt, _ := params.Get(c.Ctx, "startAt")
|
||||
endAt, _ := params.Get(c.Ctx, "endAt")
|
||||
teamID, _ := params.GetInt64(c.Ctx, "teamId")
|
||||
list, err := services.AgentTeamScheduleService.FindCalendarSchedules(request.AgentTeamScheduleCalendarRequest{
|
||||
StartAt: startAt,
|
||||
EndAt: endAt,
|
||||
TeamID: teamID,
|
||||
})
|
||||
if err != nil {
|
||||
return web.JsonError(err)
|
||||
}
|
||||
results := make([]response.AgentTeamScheduleResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAgentTeamScheduleResponse(&item))
|
||||
}
|
||||
return web.JsonData(results)
|
||||
}
|
||||
|
||||
func (c *AgentTeamScheduleController) GetBy(id int64) *web.JsonResult {
|
||||
if _, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionAgentTeamScheduleView); err != nil {
|
||||
return web.JsonError(err)
|
||||
|
||||
@@ -62,3 +62,9 @@ type UpdateAgentTeamScheduleRequest struct {
|
||||
type DeleteAgentTeamScheduleRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type AgentTeamScheduleCalendarRequest struct {
|
||||
StartAt string `json:"startAt"`
|
||||
EndAt string `json:"endAt"`
|
||||
TeamID int64 `json:"teamId"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"cs-agent/internal/models"
|
||||
"time"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web/params"
|
||||
@@ -38,6 +39,16 @@ func (r *agentTeamScheduleRepository) Find(db *gorm.DB, cnd *sqls.Cnd) (list []m
|
||||
return
|
||||
}
|
||||
|
||||
func (r *agentTeamScheduleRepository) FindByTimeRange(db *gorm.DB, startAt, endAt time.Time, teamID int64) (list []models.AgentTeamSchedule) {
|
||||
query := db.Model(&models.AgentTeamSchedule{}).
|
||||
Where("start_at < ? AND end_at > ?", endAt, startAt)
|
||||
if teamID > 0 {
|
||||
query = query.Where("team_id = ?", teamID)
|
||||
}
|
||||
query.Order("team_id ASC").Order("start_at ASC").Order("id ASC").Find(&list)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *agentTeamScheduleRepository) FindOne(db *gorm.DB, cnd *sqls.Cnd) *models.AgentTeamSchedule {
|
||||
ret := &models.AgentTeamSchedule{}
|
||||
if err := cnd.FindOne(db, &ret); err != nil {
|
||||
@@ -62,12 +73,12 @@ func (r *agentTeamScheduleRepository) FindPageByCnd(db *gorm.DB, cnd *sqls.Cnd)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *agentTeamScheduleRepository) FindBySql(db *gorm.DB, sqlStr string, paramArr... interface{}) (list []models.AgentTeamSchedule) {
|
||||
func (r *agentTeamScheduleRepository) FindBySql(db *gorm.DB, sqlStr string, paramArr ...interface{}) (list []models.AgentTeamSchedule) {
|
||||
db.Raw(sqlStr, paramArr...).Scan(&list)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *agentTeamScheduleRepository) CountBySql(db *gorm.DB, sqlStr string, paramArr... interface{}) (count int64) {
|
||||
func (r *agentTeamScheduleRepository) CountBySql(db *gorm.DB, sqlStr string, paramArr ...interface{}) (count int64) {
|
||||
db.Raw(sqlStr, paramArr...).Count(&count)
|
||||
return
|
||||
}
|
||||
@@ -99,4 +110,3 @@ func (r *agentTeamScheduleRepository) UpdateColumn(db *gorm.DB, id int64, name s
|
||||
func (r *agentTeamScheduleRepository) Delete(db *gorm.DB, id int64) {
|
||||
db.Delete(&models.AgentTeamSchedule{}, "id = ?", id)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,21 @@ func (s *agentTeamScheduleService) Count(cnd *sqls.Cnd) int64 {
|
||||
return repositories.AgentTeamScheduleRepository.Count(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *agentTeamScheduleService) FindCalendarSchedules(req request.AgentTeamScheduleCalendarRequest) ([]models.AgentTeamSchedule, error) {
|
||||
startAtValue, err := parseRequiredDateTime(req.StartAt, "开始时间格式错误")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endAtValue, err := parseRequiredDateTime(req.EndAt, "结束时间格式错误")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !endAtValue.After(startAtValue) {
|
||||
return nil, errorsx.InvalidParam("结束时间必须晚于开始时间")
|
||||
}
|
||||
return repositories.AgentTeamScheduleRepository.FindByTimeRange(sqls.DB(), startAtValue, endAtValue, req.TeamID), nil
|
||||
}
|
||||
|
||||
func (s *agentTeamScheduleService) Create(t *models.AgentTeamSchedule) error {
|
||||
return repositories.AgentTeamScheduleRepository.Create(sqls.DB(), t)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package services_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/dto/request"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"cs-agent/internal/services"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func TestAgentTeamScheduleServiceFindCalendarSchedulesReturnsIntersectingSchedules(t *testing.T) {
|
||||
db := setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestData(t, db)
|
||||
|
||||
list, err := services.AgentTeamScheduleService.FindCalendarSchedules(request.AgentTeamScheduleCalendarRequest{
|
||||
StartAt: "2026-04-27 00:00:00",
|
||||
EndAt: "2026-05-04 00:00:00",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FindCalendarSchedules() error = %v", err)
|
||||
}
|
||||
|
||||
if len(list) != 3 {
|
||||
t.Fatalf("expected 3 intersecting schedules, got %d: %+v", len(list), list)
|
||||
}
|
||||
gotIDs := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
gotIDs = append(gotIDs, item.ID)
|
||||
}
|
||||
wantIDs := []int64{1, 2, 3}
|
||||
for i, want := range wantIDs {
|
||||
if gotIDs[i] != want {
|
||||
t.Fatalf("expected ids %v, got %v", wantIDs, gotIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceFindCalendarSchedulesFiltersTeamID(t *testing.T) {
|
||||
db := setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestData(t, db)
|
||||
|
||||
list, err := services.AgentTeamScheduleService.FindCalendarSchedules(request.AgentTeamScheduleCalendarRequest{
|
||||
StartAt: "2026-04-27 00:00:00",
|
||||
EndAt: "2026-05-04 00:00:00",
|
||||
TeamID: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FindCalendarSchedules() error = %v", err)
|
||||
}
|
||||
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("expected 1 schedule for team 2, got %d: %+v", len(list), list)
|
||||
}
|
||||
if list[0].ID != 3 || list[0].TeamID != 2 {
|
||||
t.Fatalf("unexpected schedule: %+v", list[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceFindCalendarSchedulesValidatesTimeRange(t *testing.T) {
|
||||
setupAgentTeamScheduleTestDB(t)
|
||||
|
||||
_, err := services.AgentTeamScheduleService.FindCalendarSchedules(request.AgentTeamScheduleCalendarRequest{
|
||||
StartAt: "2026-05-04 00:00:00",
|
||||
EndAt: "2026-04-27 00:00:00",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected invalid time range to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func setupAgentTeamScheduleTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
||||
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "t_",
|
||||
SingularTable: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
if err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
if err := db.AutoMigrate(&models.AgentTeam{}, &models.AgentTeamSchedule{}); err != nil {
|
||||
t.Fatalf("auto migrate error = %v", err)
|
||||
}
|
||||
sqls.SetDB(db)
|
||||
return db
|
||||
}
|
||||
|
||||
func createAgentTeamScheduleTestData(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
teams := []models.AgentTeam{
|
||||
{ID: 1, Name: "售前组", Status: enums.StatusOk},
|
||||
{ID: 2, Name: "售后组", Status: enums.StatusOk},
|
||||
}
|
||||
if err := db.Create(&teams).Error; err != nil {
|
||||
t.Fatalf("create teams error = %v", err)
|
||||
}
|
||||
|
||||
parse := func(value string) time.Time {
|
||||
t.Helper()
|
||||
ret, err := time.ParseInLocation(time.DateTime, value, time.Local)
|
||||
if err != nil {
|
||||
t.Fatalf("parse time %q error = %v", value, err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
schedules := []models.AgentTeamSchedule{
|
||||
{ID: 1, TeamID: 1, StartAt: parse("2026-04-26 20:00:00"), EndAt: parse("2026-04-27 10:00:00"), SourceType: "manual", Status: enums.StatusOk},
|
||||
{ID: 2, TeamID: 1, StartAt: parse("2026-04-28 09:00:00"), EndAt: parse("2026-04-28 18:00:00"), SourceType: "manual", Status: enums.StatusOk},
|
||||
{ID: 3, TeamID: 2, StartAt: parse("2026-05-03 20:00:00"), EndAt: parse("2026-05-04 08:00:00"), SourceType: "manual", Status: enums.StatusOk},
|
||||
{ID: 4, TeamID: 1, StartAt: parse("2026-04-20 09:00:00"), EndAt: parse("2026-04-20 18:00:00"), SourceType: "manual", Status: enums.StatusOk},
|
||||
{ID: 5, TeamID: 2, StartAt: parse("2026-05-04 09:00:00"), EndAt: parse("2026-05-04 18:00:00"), SourceType: "manual", Status: enums.StatusOk},
|
||||
}
|
||||
if err := db.Create(&schedules).Error; err != nil {
|
||||
t.Fatalf("create schedules error = %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user