diff --git a/graphql2/generated.go b/graphql2/generated.go index 2ede94a005..d7d1eeab69 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -507,6 +507,7 @@ type ComplexityRoot struct { SetScheduleOnCallNotificationRules func(childComplexity int, input SetScheduleOnCallNotificationRulesInput) int SetSystemLimits func(childComplexity int, input []SystemLimitInput) int SetTemporarySchedule func(childComplexity int, input SetTemporaryScheduleInput) int + SetTemporarySchedulePickOrder func(childComplexity int, input SetTempSchedPickOrderInput) int SwoAction func(childComplexity int, action SWOAction) int TestContactMethod func(childComplexity int, id string) int UpdateAlerts func(childComplexity int, input UpdateAlertsInput) int @@ -684,9 +685,11 @@ type ComplexityRoot struct { Schedule struct { AssignedTo func(childComplexity int) int + AssociatedUsers func(childComplexity int) int Description func(childComplexity int) int ID func(childComplexity int) int IsFavorite func(childComplexity int) int + LastTempSchedPickOrder func(childComplexity int) int Name func(childComplexity int) int OnCallNotificationRules func(childComplexity int) int Shifts func(childComplexity int, start time.Time, end time.Time, userIDs []string) int @@ -963,6 +966,7 @@ type MutationResolver interface { LinkAccount(ctx context.Context, token string) (bool, error) ReEncryptKeyringsAndConfig(ctx context.Context) (bool, error) SetTemporarySchedule(ctx context.Context, input SetTemporaryScheduleInput) (bool, error) + SetTemporarySchedulePickOrder(ctx context.Context, input SetTempSchedPickOrderInput) (bool, error) ClearTemporarySchedules(ctx context.Context, input ClearTemporarySchedulesInput) (bool, error) SetScheduleOnCallNotificationRules(ctx context.Context, input SetScheduleOnCallNotificationRulesInput) (bool, error) DebugCarrierInfo(ctx context.Context, input DebugCarrierInfoInput) (*twilio.CarrierInfo, error) @@ -1090,6 +1094,8 @@ type ScheduleResolver interface { TimeZone(ctx context.Context, obj *schedule.Schedule) (string, error) AssignedTo(ctx context.Context, obj *schedule.Schedule) ([]assignment.RawTarget, error) Shifts(ctx context.Context, obj *schedule.Schedule, start time.Time, end time.Time, userIDs []string) ([]oncall.Shift, error) + AssociatedUsers(ctx context.Context, obj *schedule.Schedule) ([]user.User, error) + LastTempSchedPickOrder(ctx context.Context, obj *schedule.Schedule) ([]string, error) Targets(ctx context.Context, obj *schedule.Schedule) ([]ScheduleTarget, error) Target(ctx context.Context, obj *schedule.Schedule, input assignment.RawTarget) (*ScheduleTarget, error) IsFavorite(ctx context.Context, obj *schedule.Schedule) (bool, error) @@ -2954,6 +2960,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.SetTemporarySchedule(childComplexity, args["input"].(SetTemporaryScheduleInput)), true + case "Mutation.setTemporarySchedulePickOrder": + if e.complexity.Mutation.SetTemporarySchedulePickOrder == nil { + break + } + + args, err := ec.field_Mutation_setTemporarySchedulePickOrder_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SetTemporarySchedulePickOrder(childComplexity, args["input"].(SetTempSchedPickOrderInput)), true case "Mutation.swoAction": if e.complexity.Mutation.SwoAction == nil { break @@ -4071,6 +4088,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Schedule.AssignedTo(childComplexity), true + case "Schedule.associatedUsers": + if e.complexity.Schedule.AssociatedUsers == nil { + break + } + + return e.complexity.Schedule.AssociatedUsers(childComplexity), true case "Schedule.description": if e.complexity.Schedule.Description == nil { break @@ -4089,6 +4112,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Schedule.IsFavorite(childComplexity), true + case "Schedule.lastTempSchedPickOrder": + if e.complexity.Schedule.LastTempSchedPickOrder == nil { + break + } + + return e.complexity.Schedule.LastTempSchedPickOrder(childComplexity), true case "Schedule.name": if e.complexity.Schedule.Name == nil { break @@ -4946,6 +4975,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputSetLabelInput, ec.unmarshalInputSetScheduleOnCallNotificationRulesInput, ec.unmarshalInputSetScheduleShiftInput, + ec.unmarshalInputSetTempSchedPickOrderInput, ec.unmarshalInputSetTemporaryScheduleInput, ec.unmarshalInputSlackChannelSearchOptions, ec.unmarshalInputSlackUserGroupSearchOptions, @@ -5560,6 +5590,17 @@ func (ec *executionContext) field_Mutation_setSystemLimits_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_setTemporarySchedulePickOrder_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNSetTempSchedPickOrderInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐSetTempSchedPickOrderInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_setTemporarySchedule_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -13375,6 +13416,47 @@ func (ec *executionContext) fieldContext_Mutation_setTemporarySchedule(ctx conte return fc, nil } +func (ec *executionContext) _Mutation_setTemporarySchedulePickOrder(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_setTemporarySchedulePickOrder, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().SetTemporarySchedulePickOrder(ctx, fc.Args["input"].(SetTempSchedPickOrderInput)) + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_setTemporarySchedulePickOrder(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_setTemporarySchedulePickOrder_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_clearTemporarySchedules(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -14749,6 +14831,10 @@ func (ec *executionContext) fieldContext_Mutation_createSchedule(ctx context.Con return ec.fieldContext_Schedule_assignedTo(ctx, field) case "shifts": return ec.fieldContext_Schedule_shifts(ctx, field) + case "associatedUsers": + return ec.fieldContext_Schedule_associatedUsers(ctx, field) + case "lastTempSchedPickOrder": + return ec.fieldContext_Schedule_lastTempSchedPickOrder(ctx, field) case "targets": return ec.fieldContext_Schedule_targets(ctx, field) case "target": @@ -17819,6 +17905,10 @@ func (ec *executionContext) fieldContext_Query_schedule(ctx context.Context, fie return ec.fieldContext_Schedule_assignedTo(ctx, field) case "shifts": return ec.fieldContext_Schedule_shifts(ctx, field) + case "associatedUsers": + return ec.fieldContext_Schedule_associatedUsers(ctx, field) + case "lastTempSchedPickOrder": + return ec.fieldContext_Schedule_lastTempSchedPickOrder(ctx, field) case "targets": return ec.fieldContext_Schedule_targets(ctx, field) case "target": @@ -20818,6 +20908,94 @@ func (ec *executionContext) fieldContext_Schedule_shifts(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _Schedule_associatedUsers(ctx context.Context, field graphql.CollectedField, obj *schedule.Schedule) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Schedule_associatedUsers, + func(ctx context.Context) (any, error) { + return ec.resolvers.Schedule().AssociatedUsers(ctx, obj) + }, + nil, + ec.marshalNUser2ᚕgithubᚗcomᚋtargetᚋgoalertᚋuserᚐUserᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Schedule_associatedUsers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Schedule", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_User_id(ctx, field) + case "role": + return ec.fieldContext_User_role(ctx, field) + case "name": + return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "contactMethods": + return ec.fieldContext_User_contactMethods(ctx, field) + case "notificationRules": + return ec.fieldContext_User_notificationRules(ctx, field) + case "calendarSubscriptions": + return ec.fieldContext_User_calendarSubscriptions(ctx, field) + case "statusUpdateContactMethodID": + return ec.fieldContext_User_statusUpdateContactMethodID(ctx, field) + case "authSubjects": + return ec.fieldContext_User_authSubjects(ctx, field) + case "sessions": + return ec.fieldContext_User_sessions(ctx, field) + case "onCallSteps": + return ec.fieldContext_User_onCallSteps(ctx, field) + case "onCallOverview": + return ec.fieldContext_User_onCallOverview(ctx, field) + case "isFavorite": + return ec.fieldContext_User_isFavorite(ctx, field) + case "assignedSchedules": + return ec.fieldContext_User_assignedSchedules(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type User", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Schedule_lastTempSchedPickOrder(ctx context.Context, field graphql.CollectedField, obj *schedule.Schedule) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Schedule_lastTempSchedPickOrder, + func(ctx context.Context) (any, error) { + return ec.resolvers.Schedule().LastTempSchedPickOrder(ctx, obj) + }, + nil, + ec.marshalNID2ᚕstringᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Schedule_lastTempSchedPickOrder(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Schedule", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Schedule_targets(ctx context.Context, field graphql.CollectedField, obj *schedule.Schedule) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -21047,6 +21225,10 @@ func (ec *executionContext) fieldContext_ScheduleConnection_nodes(_ context.Cont return ec.fieldContext_Schedule_assignedTo(ctx, field) case "shifts": return ec.fieldContext_Schedule_shifts(ctx, field) + case "associatedUsers": + return ec.fieldContext_Schedule_associatedUsers(ctx, field) + case "lastTempSchedPickOrder": + return ec.fieldContext_Schedule_lastTempSchedPickOrder(ctx, field) case "targets": return ec.fieldContext_Schedule_targets(ctx, field) case "target": @@ -23569,6 +23751,10 @@ func (ec *executionContext) fieldContext_User_assignedSchedules(_ context.Contex return ec.fieldContext_Schedule_assignedTo(ctx, field) case "shifts": return ec.fieldContext_Schedule_shifts(ctx, field) + case "associatedUsers": + return ec.fieldContext_Schedule_associatedUsers(ctx, field) + case "lastTempSchedPickOrder": + return ec.fieldContext_Schedule_lastTempSchedPickOrder(ctx, field) case "targets": return ec.fieldContext_Schedule_targets(ctx, field) case "target": @@ -23767,6 +23953,10 @@ func (ec *executionContext) fieldContext_UserCalendarSubscription_schedule(_ con return ec.fieldContext_Schedule_assignedTo(ctx, field) case "shifts": return ec.fieldContext_Schedule_shifts(ctx, field) + case "associatedUsers": + return ec.fieldContext_Schedule_associatedUsers(ctx, field) + case "lastTempSchedPickOrder": + return ec.fieldContext_Schedule_lastTempSchedPickOrder(ctx, field) case "targets": return ec.fieldContext_Schedule_targets(ctx, field) case "target": @@ -29442,6 +29632,40 @@ func (ec *executionContext) unmarshalInputSetScheduleShiftInput(ctx context.Cont return it, nil } +func (ec *executionContext) unmarshalInputSetTempSchedPickOrderInput(ctx context.Context, obj any) (SetTempSchedPickOrderInput, error) { + var it SetTempSchedPickOrderInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"scheduleID", "userIDs"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "scheduleID": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scheduleID")) + data, err := ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + it.ScheduleID = data + case "userIDs": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userIDs")) + data, err := ec.unmarshalNID2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.UserIDs = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputSetTemporaryScheduleInput(ctx context.Context, obj any) (SetTemporaryScheduleInput, error) { var it SetTemporaryScheduleInput asMap := map[string]any{} @@ -34704,6 +34928,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "setTemporarySchedulePickOrder": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_setTemporarySchedulePickOrder(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "clearTemporarySchedules": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_clearTemporarySchedules(ctx, field) @@ -37304,6 +37535,78 @@ func (ec *executionContext) _Schedule(ctx context.Context, sel ast.SelectionSet, continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "associatedUsers": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Schedule_associatedUsers(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "lastTempSchedPickOrder": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Schedule_lastTempSchedPickOrder(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "targets": field := field @@ -43244,6 +43547,11 @@ func (ec *executionContext) unmarshalNSetScheduleShiftInput2ᚕgithubᚗcomᚋta return res, nil } +func (ec *executionContext) unmarshalNSetTempSchedPickOrderInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐSetTempSchedPickOrderInput(ctx context.Context, v any) (SetTempSchedPickOrderInput, error) { + res, err := ec.unmarshalInputSetTempSchedPickOrderInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNSetTemporaryScheduleInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐSetTemporaryScheduleInput(ctx context.Context, v any) (SetTemporaryScheduleInput, error) { res, err := ec.unmarshalInputSetTemporaryScheduleInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/graphql2/graph/_Mutation.graphqls b/graphql2/graph/_Mutation.graphqls index 0d614e3321..3bdb0080d6 100644 --- a/graphql2/graph/_Mutation.graphqls +++ b/graphql2/graph/_Mutation.graphqls @@ -10,6 +10,7 @@ type Mutation { reEncryptKeyringsAndConfig: Boolean! setTemporarySchedule(input: SetTemporaryScheduleInput!): Boolean! + setTemporarySchedulePickOrder(input: SetTempSchedPickOrderInput!): Boolean! clearTemporarySchedules(input: ClearTemporarySchedulesInput!): Boolean! setScheduleOnCallNotificationRules( diff --git a/graphql2/graphqlapp/schedule.go b/graphql2/graphqlapp/schedule.go index 189a157b37..674bb5a66d 100644 --- a/graphql2/graphqlapp/schedule.go +++ b/graphql2/graphqlapp/schedule.go @@ -19,6 +19,7 @@ import ( "github.com/target/goalert/schedule" "github.com/target/goalert/schedule/rule" "github.com/target/goalert/search" + "github.com/target/goalert/user" "github.com/target/goalert/util" "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" @@ -172,6 +173,34 @@ func (s *Schedule) Targets(ctx context.Context, raw *schedule.Schedule) ([]graph return result, nil } +func (s *Schedule) AssociatedUsers(ctx context.Context, raw *schedule.Schedule) ([]user.User, error) { + userIDs, err := s.ScheduleStore.FindAssociatedUserIDs(ctx, raw.ID) + if err != nil { + return nil, err + } + users, err := s.UserStore.FindMany(ctx, userIDs) + if err != nil { + return nil, err + } + return users, nil +} + +func (s *Schedule) LastTempSchedPickOrder(ctx context.Context, raw *schedule.Schedule) ([]string, error) { + userIDs, err := s.ScheduleStore.FindLastTempSchedPickOrder(ctx, raw.ID) + if err != nil { + return nil, err + } + return userIDs, nil +} + +func (m *Mutation) SetTemporarySchedulePickOrder(ctx context.Context, input graphql2.SetTempSchedPickOrderInput) (ok bool, err error) { + ok, err = m.ScheduleStore.SetTempSchedPickOrder(ctx, input.ScheduleID, input.UserIDs) + if err != nil { + return false, err + } + return true, nil +} + func (s *Schedule) AssignedTo(ctx context.Context, raw *schedule.Schedule) ([]assignment.RawTarget, error) { pols, err := s.PolicyStore.FindAllPoliciesBySchedule(ctx, raw.ID) if err != nil { diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index f14f1d02d0..f12a894c4b 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -732,6 +732,11 @@ type SetScheduleOnCallNotificationRulesInput struct { Rules []OnCallNotificationRuleInput `json:"rules"` } +type SetTempSchedPickOrderInput struct { + ScheduleID string `json:"scheduleID"` + UserIDs []string `json:"userIDs"` +} + type SetTemporaryScheduleInput struct { ScheduleID string `json:"scheduleID"` ClearStart *time.Time `json:"clearStart,omitempty"` diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 7413b075b5..191d131360 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -692,6 +692,9 @@ type Schedule { userIDs: [ID!] ): [OnCallShift!]! + associatedUsers: [User!]! + lastTempSchedPickOrder: [ID!]! + targets: [ScheduleTarget!]! target(input: TargetInput!): ScheduleTarget isFavorite: Boolean! @@ -700,6 +703,11 @@ type Schedule { onCallNotificationRules: [OnCallNotificationRule!]! } +input SetTempSchedPickOrderInput { + scheduleID: ID! + userIDs: [ID!]! +} + input SetScheduleOnCallNotificationRulesInput { scheduleID: ID! rules: [OnCallNotificationRuleInput!]! diff --git a/schedule/store.go b/schedule/store.go index b7c702e74f..48e21a3ef6 100644 --- a/schedule/store.go +++ b/schedule/store.go @@ -3,6 +3,7 @@ package schedule import ( "context" "database/sql" + "encoding/json" "github.com/google/uuid" "github.com/pkg/errors" @@ -16,12 +17,61 @@ import ( type Store struct { db *sql.DB usr *user.Store + + findAssociatedUserIDs *sql.Stmt + findLastTempSchedPickOrder *sql.Stmt + setTempSchedPickOrder *sql.Stmt } func NewStore(ctx context.Context, db *sql.DB, usr *user.Store) (*Store, error) { + p := &util.Prepare{ + DB: db, + Ctx: ctx, + } + return &Store{ db: db, usr: usr, + + findAssociatedUserIDs: p.P(` + SELECT DISTINCT COALESCE(s.tgt_user_id, r.user_id) + FROM schedule_rules s + LEFT JOIN rotation_participants r ON r.rotation_id = s.tgt_rotation_id + WHERE s.schedule_id = $1 + `), + findLastTempSchedPickOrder: p.P(` + SELECT data -> 'user_ids' AS user_ids + FROM schedule_data + WHERE schedule_id = $1; + `), + setTempSchedPickOrder: p.P(` + INSERT INTO schedule_data (schedule_id, data) + VALUES ( + $1, + jsonb_build_object('user_ids', to_jsonb($2::text[])) + ) + ON CONFLICT (schedule_id) + DO UPDATE SET data = jsonb_set( + COALESCE(schedule_data.data, '{}'), + '{user_ids}', + to_jsonb(( + SELECT ARRAY( + SELECT * FROM ( + SELECT unnest($2::text[]) AS user_id + UNION ALL + SELECT user_id + FROM ( + SELECT jsonb_array_elements_text(sd.data->'user_ids') AS user_id + FROM schedule_data sd + WHERE sd.schedule_id = $1 + ) existing + WHERE user_id <> ALL($2::text[]) + ) merged + ) + )), + true + ); + `), }, nil } @@ -345,3 +395,78 @@ func (store *Store) DeleteManyTx(ctx context.Context, tx *sql.Tx, ids []string) err = db.SchedDeleteMany(ctx, uuids) return err } + +func (store *Store) FindAssociatedUserIDs(ctx context.Context, id string) ([]string, error) { + err := validate.UUID("ScheduleID", id) + if err != nil { + return nil, err + } + err = permission.LimitCheckAny(ctx, permission.All) + if err != nil { + return nil, err + } + + rows, err := store.findAssociatedUserIDs.QueryContext(ctx, id) + if err != nil { + return nil, err + } + defer rows.Close() + + var userIDs []string + for rows.Next() { + var userID string + if err := rows.Scan(&userID); err != nil { + return nil, err + } + userIDs = append(userIDs, userID) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return userIDs, nil +} + +func (store *Store) FindLastTempSchedPickOrder(ctx context.Context, scheduleID string) ([]string, error) { + err := validate.UUID("ScheduleID", scheduleID) + if err != nil { + return nil, err + } + + var userIDs []string + var rawUserIDs []byte + row := store.findLastTempSchedPickOrder.QueryRowContext(ctx, scheduleID) + err = row.Scan(&rawUserIDs) + if errors.Is(err, sql.ErrNoRows) { + return userIDs, nil + } + if err != nil { + return nil, err + } + + err = json.Unmarshal(rawUserIDs, &userIDs) + if err != nil { + return nil, err + } + + return userIDs, nil +} + +func (store *Store) SetTempSchedPickOrder(ctx context.Context, scheduleID string, userIDs []string) (bool, error) { + err := validate.UUID("ScheduleID", scheduleID) + if err != nil { + return false, err + } + + err = validate.ManyUUID("UserID", userIDs, 200) + if err != nil { + return false, err + } + + _, err = store.setTempSchedPickOrder.ExecContext(ctx, scheduleID, userIDs) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/web/src/app/dialogs/components/DialogTitleWrapper.jsx b/web/src/app/dialogs/components/DialogTitleWrapper.jsx index 3d870dbab6..4f642d46ea 100644 --- a/web/src/app/dialogs/components/DialogTitleWrapper.jsx +++ b/web/src/app/dialogs/components/DialogTitleWrapper.jsx @@ -18,6 +18,7 @@ const useStyles = makeStyles((theme) => { subtitle: { overflowY: 'unset', flexGrow: 0, + paddingBottom: 0, }, topRightActions, } diff --git a/web/src/app/schedules/temp-sched/TempSchedAddNewShift.tsx b/web/src/app/schedules/temp-sched/TempSchedAddNewShift.tsx index 69d5c48c53..6cec921b5e 100644 --- a/web/src/app/schedules/temp-sched/TempSchedAddNewShift.tsx +++ b/web/src/app/schedules/temp-sched/TempSchedAddNewShift.tsx @@ -1,9 +1,25 @@ -import React, { useEffect, useState } from 'react' -import { Button, Checkbox, FormControlLabel, Grid } from '@mui/material' +import React, { useEffect, useMemo, useState } from 'react' +import { + Avatar, + Button, + Checkbox, + Chip, + FormControlLabel, + Grid, + Popover, + IconButton, + ButtonGroup, + useTheme, +} from '@mui/material' import Typography from '@mui/material/Typography' import ToggleIcon from '@mui/icons-material/CompareArrows' import _ from 'lodash' -import { dtToDuration, Shift, TempSchedValue } from './sharedUtils' +import { + dtToDuration, + Shift, + sortUsersByLastPickOrder, + TempSchedValue, +} from './sharedUtils' import { FormContainer, FormField } from '../../forms' import { DateTime, Duration, Interval } from 'luxon' import { FieldError } from '../../util/errutil' @@ -14,18 +30,44 @@ import { UserSelect } from '../../selection' import ClickableText from '../../util/ClickableText' import NumberField from '../../util/NumberField' import { fmtLocal } from '../../util/timeFormat' +import { User } from 'web/src/schema' +import { + ArrowRight, + ShuffleVariant, + Sort, + SortAlphabeticalAscending, + SortAlphabeticalDescending, +} from 'mdi-material-ui' +import Chance from 'chance' +import { gql, useQuery } from 'urql' +const c = new Chance() + +const query = gql` + query ($id: ID!) { + schedule(id: $id) { + lastTempSchedPickOrder + } + } +` type AddShiftsStepProps = { value: TempSchedValue onChange: (newValue: Shift[]) => void scheduleID: string + associatedUsers: Array showForm: boolean setShowForm: (showForm: boolean) => void - shift: Shift | null + shift: Shift setShift: (shift: Shift) => void + isCustomShiftTimeRange: boolean + setIsCustomShiftTimeRange: (bool: boolean) => void + pickOrder: string[] + setPickOrder: (pickOrder: string[]) => void } +type SortType = 'A-Z' | 'Z-A' | 'RAND' | 'LAST-PICKS' + type DTShift = { userID: string span: Interval @@ -72,17 +114,47 @@ function mergeShifts(_shifts: Shift[]): Shift[] { export default function TempSchedAddNewShift({ scheduleID, + associatedUsers, onChange, value, shift, setShift, + isCustomShiftTimeRange, + setIsCustomShiftTimeRange, + pickOrder, + setPickOrder, }: AddShiftsStepProps): JSX.Element { + const theme = useTheme() const [submitted, setSubmitted] = useState(false) - const [custom, setCustom] = useState(false) const [manualEntry, setManualEntry] = useState(true) const { q, zone, isLocalZone } = useScheduleTZ(scheduleID) + const [sortType, setSortType] = useState('A-Z') + const [sortTypeAnchor, setSortTypeAnchor] = + useState(null) + const sortPopoverOpen = Boolean(sortTypeAnchor) + const sortTypeID = sortPopoverOpen ? 'sort-type-select' : undefined + + const [{ fetching, error, data }] = useQuery({ + query, + variables: { + id: scheduleID, + }, + }) + const lastTempSchedPickOrder: Array = + data.schedule.lastTempSchedPickOrder + + const handleFilterTypeClick = ( + event: React.MouseEvent, + ): void => { + setSortTypeAnchor(event.currentTarget) + } + + const handleFilterTypeClose = (): void => { + setSortTypeAnchor(null) + } + // set start equal to the temporary schedule's start // can't this do on mount since the step renderer puts everyone on the DOM at once useEffect(() => { @@ -130,6 +202,10 @@ export default function TempSchedAddNewShift({ } if (!shift) return // ts sanity check + if (!pickOrder.includes(shift.userID)) { + setPickOrder([...pickOrder, shift.userID]) + } + onChange(mergeShifts(value.shifts.concat(shift))) const end = DateTime.fromISO(shift.end, { zone }) setShift({ @@ -138,10 +214,59 @@ export default function TempSchedAddNewShift({ start: shift.end, end: end.plus(value.shiftDur as Duration).toISO(), }) - setCustom(false) + setIsCustomShiftTimeRange(false) setSubmitted(false) } + function getUserIDCountInValue(userID: string): number { + const uIDs = value.shifts.map((s) => s.userID) + const count = uIDs.filter((id) => id === userID).length + return count + } + + function getChipColor( + userID: string, + count: number, + ): 'primary' | 'success' | 'default' { + if (shift.userID === userID) return 'primary' + if (count > 0) return 'success' + return 'default' + } + + function handleSetSortType(sortType: SortType): void { + setSortType(sortType) + setSortTypeAnchor(null) + } + + function sortFn(_a: User, _b: User): number { + const a = _a.name + const b = _b.name + + if (sortType === 'A-Z') { + if (a > b) return 1 + if (a < b) return -1 + return 0 + } + + if (sortType === 'Z-A') { + if (a < b) return 1 + if (a > b) return -1 + return 0 + } + + if (sortType === 'RAND') { + return c.pickone([1, -1, 0]) + } + + if (sortType === 'LAST-PICKS') { + return sortUsersByLastPickOrder(_a, _b, lastTempSchedPickOrder) + } + + return 0 + } + + const users = useMemo(() => associatedUsers.sort(sortFn), [sortType]) + return ( setShift(val)} > - - Add Shift + + Add Shift + + {sortType === 'A-Z' && ( + + )} + {sortType === 'Z-A' && ( + + )} + {sortType === 'RAND' && } + {sortType === 'LAST-PICKS' && !fetching && !error && ( + + )} + + + + + + + + + + + + + Showing all users assigned to this schedule. Select a user to add to + the next shift. + + + + + {users.map((u) => { + const count = getUserIDCountInValue(u.id) + + return ( + { + setShift({ + ...shift, + userID: u.id, + }) + }} + icon={ + count > 0 ? ( + + {count} + + ) : undefined + } + /> + ) + })} + - + + + } + label={ + + Add user to next shift + + } + onChange={() => setIsCustomShiftTimeRange(false)} + /> + + } + control={ + + } label={ - - Configure custom shift + + Add user to custom time range } - onChange={() => setCustom(!custom)} + onChange={() => setIsCustomShiftTimeRange(true)} /> @@ -194,7 +454,7 @@ export default function TempSchedAddNewShift({ return value }} timeZone={zone} - disabled={q.loading || !custom} + disabled={q.loading || !isCustomShiftTimeRange} hint={isLocalZone ? '' : fmtLocal(value?.start)} /> @@ -211,7 +471,7 @@ export default function TempSchedAddNewShift({ .plus({ year: 1 }) .toISO()} hint={ - custom ? ( + isCustomShiftTimeRange ? ( {!isLocalZone && fmtLocal(value?.end)}
@@ -227,7 +487,7 @@ export default function TempSchedAddNewShift({ ) : null } timeZone={zone} - disabled={q.loading || !custom} + disabled={q.loading || !isCustomShiftTimeRange} /> ) : ( setManualEntry(true)} @@ -275,12 +535,14 @@ export default function TempSchedAddNewShift({ diff --git a/web/src/app/schedules/temp-sched/TempSchedDialog.tsx b/web/src/app/schedules/temp-sched/TempSchedDialog.tsx index 97f190e258..71632b3655 100644 --- a/web/src/app/schedules/temp-sched/TempSchedDialog.tsx +++ b/web/src/app/schedules/temp-sched/TempSchedDialog.tsx @@ -1,7 +1,6 @@ -import React, { useState, useRef, Suspense } from 'react' -import { useMutation, gql } from 'urql' +import React, { useState, useRef, Suspense, useMemo } from 'react' +import { useMutation, gql, useQuery } from 'urql' import Checkbox from '@mui/material/Checkbox' -import DialogContentText from '@mui/material/DialogContentText' import FormControlLabel from '@mui/material/FormControlLabel' import FormHelperText from '@mui/material/FormHelperText' import Grid from '@mui/material/Grid' @@ -14,23 +13,16 @@ import _ from 'lodash' import { DateTime, Duration, Interval } from 'luxon' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' -import { - contentText, - inferDuration, - Shift, - TempSchedValue, -} from './sharedUtils' -import { FormContainer, FormField } from '../../forms' -import TempSchedAddNewShift from './TempSchedAddNewShift' -import { isISOAfter, parseInterval } from '../../util/shifts' +import { inferDuration, Shift, TempSchedValue } from './sharedUtils' +import { FormContainer } from '../../forms' +import { parseInterval } from '../../util/shifts' import { useScheduleTZ } from '../useScheduleTZ' import TempSchedShiftsList from './TempSchedShiftsList' -import { ISODateTimePicker } from '../../util/ISOPickers' import { getCoverageGapItems } from './shiftsListUtil' -import { fmtLocal } from '../../util/timeFormat' import { ensureInterval } from '../timeUtil' import TempSchedConfirmation from './TempSchedConfirmation' -import { TextField, MenuItem, Divider } from '@mui/material' +import { User } from 'web/src/schema' +import TempSchedForm from './TempSchedForm' const mutation = gql` mutation ($input: SetTemporaryScheduleInput!) { @@ -38,17 +30,33 @@ const mutation = gql` } ` +const pickOrderMutation = gql` + mutation ($input: SetTempSchedPickOrderInput!) { + setTemporarySchedulePickOrder(input: $input) + } +` + +const query = gql` + query ($id: ID!) { + schedule(id: $id) { + associatedUsers { + id + name + } + } + } +` + function shiftEquals(a: Shift, b: Shift): boolean { return a.start === b.start && a.end === b.end && a.userID === b.userID } const useStyles = makeStyles((theme: Theme) => ({ - contentText, - avatar: { - backgroundColor: theme.palette.primary.main, - }, formContainer: { height: '100%', + marginTop: '-16px', + paddingLeft: '.5rem', + paddingRight: '.5rem', }, noCoverageError: { marginTop: '.5rem', @@ -59,17 +67,10 @@ const useStyles = makeStyles((theme: Theme) => ({ marginTop: '1rem', }, [theme.breakpoints.up('md')]: { - paddingLeft: '1rem', + paddingLeft: '4rem', }, overflow: 'hidden', }, - sticky: { - position: 'sticky', - top: 0, - }, - tzNote: { - fontStyle: 'italic', - }, })) type TempScheduleDialogProps = { @@ -90,7 +91,7 @@ const clampForward = (nowISO: string, iso: string): string => { return iso } -interface DurationValues { +export interface DurationValues { dur: number ivl: string } @@ -102,9 +103,23 @@ export default function TempSchedDialog({ edit = false, }: TempScheduleDialogProps): JSX.Element { const classes = useStyles() - const { q, zone, isLocalZone } = useScheduleTZ(scheduleID) - const [now] = useState(DateTime.utc().startOf('minute').toISO()) + const { q, zone } = useScheduleTZ(scheduleID) + const now = useMemo(() => DateTime.utc().startOf('minute').toISO(), []) const [showForm, setShowForm] = useState(false) + const [isCustomShiftTimeRange, setIsCustomShiftTimeRange] = useState(false) + const [pickOrder, setPickOrder] = useState>([]) + + const [{ fetching: fetchingUsers, error: errorUsers, data: dataUsers }] = + useQuery({ + query, + variables: { + id: scheduleID, + }, + }) + const associatedUsers: Array = dataUsers.schedule.associatedUsers + + const [{ fetching, error }, commit] = useMutation(mutation) + const [, commitPickOrder] = useMutation(pickOrderMutation) let defaultShiftDur = {} as DurationValues @@ -126,7 +141,7 @@ export default function TempSchedDialog({ const [durValues, setDurValues] = useState(defaultShiftDur) - const [value, setValue] = useState({ + const [value, setValue] = useState({ start: clampForward(now, _value.start), end: _value.end, clearStart: edit ? _value.start : null, @@ -157,15 +172,6 @@ export default function TempSchedDialog({ const [submitAttempt, setSubmitAttempt] = useState(false) // helps with error messaging on step 1 const [submitSuccess, setSubmitSuccess] = useState(false) - const [{ fetching, error }, commit] = useMutation(mutation) - - function validate(): Error | null { - if (isISOAfter(value.start, value.end)) { - return new Error('Start date/time cannot be after end date/time.') - } - return null - } - const hasInvalidShift = (() => { if (q.loading) return false const schedInterval = parseInterval(value, zone) @@ -191,6 +197,7 @@ export default function TempSchedDialog({ const nextStart = coverageGap?.start const nextEnd = nextStart.plus(value.shiftDur) + setIsCustomShiftTimeRange(true) setShift({ userID: shift?.userID ?? '', truncated: !!shift?.truncated, @@ -263,6 +270,13 @@ export default function TempSchedDialog({ }, { additionalTypenames: ['Schedule'] }, ).then((result) => { + commitPickOrder({ + input: { + scheduleID, + userIDs: pickOrder, + }, + }) + if (!result.error) { onClose() } @@ -285,9 +299,14 @@ export default function TempSchedDialog({ .concat(shiftErrors) .concat(noCoverageErrs) + // if error from loading associated users + if (errorUsers?.message) { + errs.concat({ message: errorUsers.message }) + } + return ( { setValue({ ...value, ...ensureInterval(value, newValue) }) @@ -319,133 +338,22 @@ export default function TempSchedDialog({ justifyContent='space-between' > {/* left pane */} - - - - The schedule will be exactly as configured here for the - entire duration (ignoring all assignments and overrides). - - - - - - Times shown in schedule timezone ({zone}) - - - - - validate()} - timeZone={zone} - disabled={q.loading} - hint={isLocalZone ? '' : fmtLocal(value.start)} - /> - - - validate()} - timeZone={zone} - disabled={q.loading} - hint={isLocalZone ? '' : fmtLocal(value.end)} - /> - - - { - setDurValues({ ...durValues, ...newValue }) - setValue({ - ...value, - shiftDur: Duration.fromObject({ - [newValue.ivl]: newValue.dur, - }), - }) - }} - > - - validate()} - disabled={q.loading} - /> - - - validate()} - disabled={q.loading} - > - Hour - Day - Week - - - - - - - - - - - setValue({ ...value, shifts }) - } - scheduleID={scheduleID} - showForm={showForm} - setShowForm={setShowForm} - shift={shift} - setShift={setShift} - /> - - + {/* right pane */} !shiftEquals(shift, s), ), }) + + const order = pickOrder.slice() + const i = order.indexOf(shift.userID) + if (i !== -1) { + order.splice(i, 1) + setPickOrder(order) + } }} edit={edit} handleCoverageGapClick={handleCoverageGapClick} diff --git a/web/src/app/schedules/temp-sched/TempSchedForm.tsx b/web/src/app/schedules/temp-sched/TempSchedForm.tsx new file mode 100644 index 0000000000..e30d4d6edb --- /dev/null +++ b/web/src/app/schedules/temp-sched/TempSchedForm.tsx @@ -0,0 +1,195 @@ +import React, { useMemo } from 'react' +import { Grid, Typography, TextField, MenuItem, Divider } from '@mui/material' +import makeStyles from '@mui/styles/makeStyles' +import { DateTime, Duration } from 'luxon' +import { FormField, FormContainer } from '../../forms' +import { ISODateTimePicker } from '../../util/ISOPickers' +import { fmtLocal } from '../../util/timeFormat' +import { contentText, Shift, TempSchedValue } from './sharedUtils' +import TempSchedAddNewShift from './TempSchedAddNewShift' +import { useScheduleTZ } from '../useScheduleTZ' +import { DurationValues } from './TempSchedDialog' +import { isISOAfter } from '../../util/shifts' +import { User } from 'web/src/schema' + +const useStyles = makeStyles(() => ({ + contentText, + sticky: { + position: 'sticky', + top: 0, + }, + tzNote: { + fontStyle: 'italic', + }, +})) + +interface TempSchedFormProps { + scheduleID: string + associatedUsers: Array + duration: DurationValues + setDuration: React.Dispatch> + value: TempSchedValue + setValue: React.Dispatch> + showForm: boolean + setShowForm: React.Dispatch> + shift: Shift + setShift: React.Dispatch> + isCustomShiftTimeRange: boolean + setIsCustomShiftTimeRange: (bool: boolean) => void + pickOrder: string[] + setPickOrder: (pickOrder: string[]) => void +} + +export default function TempSchedForm(props: TempSchedFormProps): JSX.Element { + const { + scheduleID, + associatedUsers, + duration, + setDuration, + value, + setValue, + showForm, + setShowForm, + shift, + setShift, + isCustomShiftTimeRange, + setIsCustomShiftTimeRange, + pickOrder, + setPickOrder, + } = props + + const classes = useStyles() + const now = useMemo(() => DateTime.utc().startOf('minute').toISO(), []) + const { q, zone, isLocalZone } = useScheduleTZ(scheduleID) + + function validate(): Error | null { + if (isISOAfter(value.start, value.end)) { + return new Error('Start date/time cannot be after end date/time.') + } + return null + } + + return ( + + + + Once submitted, the schedule will be exactly as configured here for + the entire duration. All overrides and current assignments will be + ignored. + + + + + + Times shown in schedule timezone ({zone}) + + + + + validate()} + timeZone={zone} + disabled={q.loading} + hint={isLocalZone ? '' : fmtLocal(value.start)} + /> + + + validate()} + timeZone={zone} + disabled={q.loading} + hint={isLocalZone ? '' : fmtLocal(value.end)} + /> + + + { + setDuration({ ...duration, ...newValue }) + setValue({ + ...value, + shiftDur: Duration.fromObject({ + [newValue.ivl]: newValue.dur, + }), + }) + }} + > + + validate()} + disabled={q.loading} + /> + + + validate()} + disabled={q.loading} + > + Hour + Day + Week + + + + + + + + + + + + setValue({ ...value, shifts })} + scheduleID={scheduleID} + showForm={showForm} + setShowForm={setShowForm} + shift={shift} + setShift={setShift} + isCustomShiftTimeRange={isCustomShiftTimeRange} + setIsCustomShiftTimeRange={setIsCustomShiftTimeRange} + pickOrder={pickOrder} + setPickOrder={setPickOrder} + /> + + + ) +} diff --git a/web/src/app/schedules/temp-sched/sharedUtils.tsx b/web/src/app/schedules/temp-sched/sharedUtils.tsx index 5ea6edeb01..379073dbe5 100644 --- a/web/src/app/schedules/temp-sched/sharedUtils.tsx +++ b/web/src/app/schedules/temp-sched/sharedUtils.tsx @@ -1,11 +1,15 @@ import { DateTime, Duration, Interval } from 'luxon' import React, { ReactNode } from 'react' +import { User } from 'web/src/schema' export type TempSchedValue = { start: string end: string shifts: Shift[] - shiftDur?: Duration + shiftDur: Duration + + clearStart?: string | null + clearEnd?: string | null } export type Shift = { @@ -105,3 +109,28 @@ export function dtToDuration(a: DateTime, b: DateTime): number { if (!a.isValid || !b.isValid) return -1 return b.diff(a, 'hours').hours } + +export function sortUsersByLastPickOrder( + a: User, + b: User, + lastTempSchedPickOrder: string[], +): number { + const aIndex = lastTempSchedPickOrder.indexOf(a.id) + const bIndex = lastTempSchedPickOrder.indexOf(b.id) + + const aInLast = aIndex !== -1 + const bInLast = bIndex !== -1 + + if (!aInLast && bInLast) return -1 // a is new + if (aInLast && !bInLast) return 1 // b is new + if (!aInLast && !bInLast) { + return a.name.localeCompare(b.name) // both new + } + + if (aIndex !== bIndex) { + // Reverse order: higher index = more recently picked = comes earlier + return bIndex - aIndex + } + + return a.name.localeCompare(b.name) // tie-breaker +} diff --git a/web/src/app/schedules/temp-sched/shiftsListUtil.tsx b/web/src/app/schedules/temp-sched/shiftsListUtil.tsx index f04365e257..292620c0e5 100644 --- a/web/src/app/schedules/temp-sched/shiftsListUtil.tsx +++ b/web/src/app/schedules/temp-sched/shiftsListUtil.tsx @@ -10,9 +10,8 @@ import { import { ExplicitZone, splitShift } from '../../util/luxon-helpers' import { parseInterval } from '../../util/shifts' import { Shift } from './sharedUtils' -import { Tooltip } from '@mui/material' +import { Tooltip, Typography } from '@mui/material' import { fmtLocal, fmtTime } from '../../util/timeFormat' -import { InfoOutlined } from '@mui/icons-material' export type Sortable = T & { // at is the earliest point in time for a list item @@ -167,12 +166,12 @@ export function getCoverageGapItems( message: '', details: ( - {details} - - ), - action: ( - - +
+ {details} + + Click to use this custom time range for the next shift + +
), at: gap.start, diff --git a/web/src/explore/explore.tsx b/web/src/explore/explore.tsx index 0ac56e9e85..477d3bbd50 100644 --- a/web/src/explore/explore.tsx +++ b/web/src/explore/explore.tsx @@ -35,7 +35,7 @@ const App = (): JSX.Element => { return ( { const resp = await fetch(location.protocol + '//' + path, { method: 'POST', diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 9699d529d7..e1a2fbe6b8 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -793,6 +793,7 @@ export interface Mutation { setScheduleOnCallNotificationRules: boolean setSystemLimits: boolean setTemporarySchedule: boolean + setTemporarySchedulePickOrder: boolean swoAction: boolean testContactMethod: boolean updateAlerts?: null | Alert[] @@ -1006,9 +1007,11 @@ export interface SWOStatus { export interface Schedule { assignedTo: Target[] + associatedUsers: User[] description: string id: string isFavorite: boolean + lastTempSchedPickOrder: string[] name: string onCallNotificationRules: OnCallNotificationRule[] shifts: OnCallShift[] @@ -1137,6 +1140,11 @@ export interface SetScheduleShiftInput { userID: string } +export interface SetTempSchedPickOrderInput { + scheduleID: string + userIDs: string[] +} + export interface SetTemporaryScheduleInput { clearEnd?: null | ISOTimestamp clearStart?: null | ISOTimestamp