feat: add party-size queueing and call modes
This commit is contained in:
@@ -39,11 +39,17 @@ func projectViews(projects []model.Project) []map[string]any {
|
||||
func projectView(project model.Project) map[string]any {
|
||||
return map[string]any{
|
||||
"id": project.ID, "code": project.Code, "name": project.Name, "status": project.Status,
|
||||
"timezone": project.Timezone, "ticket_prefix": project.TicketPrefix, "call_batch_size": project.CallBatchSize, "batch_size": project.CallBatchSize,
|
||||
"grace_period_minutes": project.GracePeriodMinutes,
|
||||
"visitor_notice": project.VisitorNotice,
|
||||
"timezone": project.Timezone, "ticket_prefix": project.TicketPrefix,
|
||||
"call_mode": project.CallMode,
|
||||
"default_call_ticket_count": project.CallBatchSize, "call_batch_size": project.CallBatchSize, "batch_size": project.CallBatchSize,
|
||||
"max_call_ticket_count": project.MaxCallTicketCount,
|
||||
"default_call_people_count": project.DefaultCallPeopleCount, "max_call_people_count": project.MaxCallPeopleCount,
|
||||
"min_party_size": project.MinPartySize, "max_party_size": project.MaxPartySize,
|
||||
"grace_period_minutes": project.GracePeriodMinutes,
|
||||
"experienced_people_start": project.ExperiencedPeopleStart,
|
||||
"visitor_notice": project.VisitorNotice,
|
||||
"eta": map[string]any{
|
||||
"interval_per_number_seconds": project.ETAIntervalSeconds,
|
||||
"interval_per_person_seconds": project.ETAIntervalSeconds,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -69,8 +75,12 @@ func (s *Server) queueSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"project": projectView(project), "session": nil, "revision": 0,
|
||||
"counts": map[string]int64{}, "waiting": []any{}, "current_batch": nil, "recent_batches": []any{},
|
||||
"metrics": map[string]any{"waiting_count": 0, "called_count": 0, "estimated_wait": nil, "last_ticket_number": nil, "next_ticket_number": nil},
|
||||
"counts": map[string]int64{}, "people_counts": map[string]int64{}, "waiting": []any{}, "current_batch": nil, "recent_batches": []any{},
|
||||
"metrics": map[string]any{
|
||||
"waiting_count": 0, "waiting_ticket_count": 0, "waiting_people_count": 0,
|
||||
"called_count": 0, "called_ticket_count": 0, "called_people_count": 0,
|
||||
"estimated_wait": nil, "last_ticket_number": nil, "next_ticket_number": nil,
|
||||
},
|
||||
"server_time": s.now(), "last_success_at": s.now(),
|
||||
})
|
||||
return
|
||||
@@ -81,19 +91,22 @@ func (s *Server) queueSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type statusCount struct {
|
||||
Status string
|
||||
Count int64
|
||||
Status string
|
||||
TicketCount int64 `gorm:"column:ticket_count"`
|
||||
PeopleCount int64 `gorm:"column:people_count"`
|
||||
}
|
||||
var grouped []statusCount
|
||||
if err := s.db.WithContext(r.Context()).Model(&model.QueueTicket{}).
|
||||
Select("status, count(*) AS count").Where("project_id = ? AND queue_session_id = ?", projectID, session.ID).
|
||||
Select("status, count(*) AS ticket_count, COALESCE(sum(party_size), 0) AS people_count").Where("project_id = ? AND queue_session_id = ?", projectID, session.ID).
|
||||
Group("status").Scan(&grouped).Error; err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
counts := make(map[string]int64, len(grouped))
|
||||
ticketCounts := make(map[string]int64, len(grouped))
|
||||
peopleCounts := make(map[string]int64, len(grouped))
|
||||
for _, row := range grouped {
|
||||
counts[row.Status] = row.Count
|
||||
ticketCounts[row.Status] = row.TicketCount
|
||||
peopleCounts[row.Status] = row.PeopleCount
|
||||
}
|
||||
var waiting []model.QueueTicket
|
||||
if err := s.db.WithContext(r.Context()).Where("project_id = ? AND queue_session_id = ? AND status = ?", projectID, session.ID, model.TicketWaiting).
|
||||
@@ -130,10 +143,24 @@ func (s *Server) queueSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
lastTicketNumber = formatted
|
||||
}
|
||||
var lastWaiting model.QueueTicket
|
||||
lastWaitingErr := s.db.WithContext(r.Context()).
|
||||
Select("id", "party_size").
|
||||
Where("project_id = ? AND queue_session_id = ? AND status = ?", projectID, session.ID, model.TicketWaiting).
|
||||
Order("ticket_number DESC").First(&lastWaiting).Error
|
||||
if lastWaitingErr != nil && !errors.Is(lastWaitingErr, gorm.ErrRecordNotFound) {
|
||||
writeError(w, lastWaitingErr)
|
||||
return
|
||||
}
|
||||
waitingPeople := peopleCounts[model.TicketWaiting]
|
||||
peopleAheadOfLast := int(waitingPeople)
|
||||
if lastWaitingErr == nil {
|
||||
peopleAheadOfLast = max(0, peopleAheadOfLast-lastWaiting.PartySize)
|
||||
}
|
||||
estimatedWait, err := domain.CalculateETA(domain.ETAInput{
|
||||
PeopleAhead: max(0, int(counts[model.TicketWaiting])-1),
|
||||
IntervalPerNumber: time.Duration(project.ETAIntervalSeconds) * time.Second,
|
||||
Running: project.Status == model.ProjectRunning && session.Status == "RUNNING" && counts[model.TicketWaiting] > 0,
|
||||
PeopleAhead: peopleAheadOfLast,
|
||||
IntervalPerPerson: time.Duration(project.ETAIntervalSeconds) * time.Second,
|
||||
Running: project.Status == model.ProjectRunning && session.Status == "RUNNING" && ticketCounts[model.TicketWaiting] > 0,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
@@ -142,13 +169,17 @@ func (s *Server) queueSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"project": projectView(project),
|
||||
"session": map[string]any{"id": session.ID, "business_date": session.BusinessDate.Format("2006-01-02"), "status": session.Status},
|
||||
"revision": session.Revision, "counts": counts, "waiting": waitingViews, "current_batch": batchView,
|
||||
"revision": session.Revision, "counts": ticketCounts, "people_counts": peopleCounts, "waiting": waitingViews, "current_batch": batchView,
|
||||
"metrics": map[string]any{
|
||||
"waiting_count": counts[model.TicketWaiting],
|
||||
"called_count": counts[model.TicketCalled] + counts[model.TicketArrived],
|
||||
"estimated_wait": estimatedWait,
|
||||
"last_ticket_number": lastTicketNumber,
|
||||
"next_ticket_number": nextTicketNumber,
|
||||
"waiting_count": ticketCounts[model.TicketWaiting],
|
||||
"waiting_ticket_count": ticketCounts[model.TicketWaiting],
|
||||
"waiting_people_count": peopleCounts[model.TicketWaiting],
|
||||
"called_count": ticketCounts[model.TicketCalled] + ticketCounts[model.TicketArrived],
|
||||
"called_ticket_count": ticketCounts[model.TicketCalled] + ticketCounts[model.TicketArrived],
|
||||
"called_people_count": peopleCounts[model.TicketCalled] + peopleCounts[model.TicketArrived],
|
||||
"estimated_wait": estimatedWait,
|
||||
"last_ticket_number": lastTicketNumber,
|
||||
"next_ticket_number": nextTicketNumber,
|
||||
},
|
||||
"server_time": s.now(), "last_success_at": s.now(),
|
||||
})
|
||||
@@ -175,6 +206,7 @@ type createTicketRequest struct {
|
||||
Phone string `json:"phone"`
|
||||
LastName string `json:"last_name"`
|
||||
Honorific string `json:"honorific"`
|
||||
PartySize *int `json:"party_size"`
|
||||
AllowDuplicate bool `json:"allow_duplicate"`
|
||||
}
|
||||
|
||||
@@ -188,6 +220,12 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
user := currentPrincipal(r.Context()).User
|
||||
s.createTicketForActor(w, r, user.ID, false)
|
||||
}
|
||||
|
||||
func (s *Server) createTicketForActor(w http.ResponseWriter, r *http.Request, actorID string, publicView bool) {
|
||||
projectID := r.PathValue("id")
|
||||
key, err := readIdempotencyKey(r)
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
@@ -198,6 +236,10 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
if input.PartySize == nil || *input.PartySize < 1 {
|
||||
writeError(w, &apiError{Status: http.StatusUnprocessableEntity, Code: "INVALID_PARTY_SIZE", Message: "请选择本号人数"})
|
||||
return
|
||||
}
|
||||
phone, err := security.NormalizePhone(input.Phone)
|
||||
if err != nil {
|
||||
writeError(w, &apiError{Status: http.StatusUnprocessableEntity, Code: "INVALID_PHONE", Message: err.Error()})
|
||||
@@ -223,16 +265,30 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user := currentPrincipal(r.Context()).User
|
||||
scope := "CREATE_TICKET"
|
||||
auditAction := "TICKET_CREATED"
|
||||
duplicateMessage := "该手机号已有活动号码,请员工确认后继续"
|
||||
if publicView {
|
||||
scope = "PUBLIC_CREATE_TICKET"
|
||||
auditAction = "PUBLIC_TICKET_CREATED"
|
||||
duplicateMessage = "该手机号已有活动号码,请确认后继续"
|
||||
}
|
||||
var responseBody []byte
|
||||
responseCode := http.StatusCreated
|
||||
var revision int64
|
||||
err = s.db.WithContext(r.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
_, session, err := s.lockRunningProjectAndSession(tx, projectID)
|
||||
project, session, err := s.lockRunningProjectAndSession(tx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stored, code, found, err := loadIdempotent(tx, projectID, user.ID, "CREATE_TICKET", key, hash, s.now()); err != nil {
|
||||
if *input.PartySize < project.MinPartySize || *input.PartySize > project.MaxPartySize {
|
||||
return &apiError{
|
||||
Status: http.StatusUnprocessableEntity, Code: "INVALID_PARTY_SIZE",
|
||||
Message: fmt.Sprintf("本项目每个号码可绑定 %d 到 %d 人", project.MinPartySize, project.MaxPartySize),
|
||||
Details: map[string]any{"min_party_size": project.MinPartySize, "max_party_size": project.MaxPartySize},
|
||||
}
|
||||
}
|
||||
if stored, code, found, err := loadIdempotent(tx, projectID, actorID, scope, key, hash, s.now()); err != nil {
|
||||
return err
|
||||
} else if found {
|
||||
responseBody, responseCode = stored, code
|
||||
@@ -252,7 +308,11 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
for _, duplicate := range duplicates {
|
||||
existing = append(existing, map[string]any{"id": duplicate.ID, "display_number": duplicate.DisplayNumber, "status": duplicate.Status})
|
||||
}
|
||||
return &apiError{Status: http.StatusConflict, Code: "DUPLICATE_PHONE", Message: "该手机号已有活动号码,请员工确认后继续", Details: map[string]any{"tickets": existing}}
|
||||
var details any = map[string]any{"tickets": existing}
|
||||
if publicView {
|
||||
details = nil
|
||||
}
|
||||
return &apiError{Status: http.StatusConflict, Code: "DUPLICATE_PHONE", Message: duplicateMessage, Details: details}
|
||||
}
|
||||
|
||||
publicToken, err := security.GenerateToken()
|
||||
@@ -277,11 +337,11 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
now := s.now()
|
||||
ticket := model.QueueTicket{
|
||||
ID: uuid.NewString(), ProjectID: projectID, QueueSessionID: session.ID,
|
||||
TicketNumber: session.NextTicketNumber, DisplayNumber: displayNumber,
|
||||
TicketNumber: session.NextTicketNumber, DisplayNumber: displayNumber, PartySize: *input.PartySize,
|
||||
PublicTokenHash: security.HashToken(publicToken), PhoneCiphertext: phoneCiphertext, PhoneNonce: phoneNonce,
|
||||
PhoneHMAC: stringPointer(phoneDigest), LastNameCiphertext: lastNameCiphertext, LastNameNonce: lastNameNonce,
|
||||
Honorific: input.Honorific, Status: model.TicketWaiting, JoinedAt: now,
|
||||
PersonalDataPurgeAt: now.Add(30 * 24 * time.Hour), CreatedBy: user.ID, CreatedAt: now, UpdatedAt: now,
|
||||
PersonalDataPurgeAt: now.Add(30 * 24 * time.Hour), CreatedBy: actorID, CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
if err := tx.Create(&ticket).Error; err != nil {
|
||||
return err
|
||||
@@ -291,25 +351,39 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
Updates(map[string]any{"next_ticket_number": session.NextTicketNumber + 1, "revision": revision, "updated_at": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.addAudit(tx, r, &projectID, &user.ID, "TICKET_CREATED", "QUEUE_TICKET", &ticket.ID,
|
||||
map[string]any{"display_number": displayNumber, "duplicate_count": len(duplicates), "duplicate_confirmed": input.AllowDuplicate}); err != nil {
|
||||
if err := s.addAudit(tx, r, &projectID, &actorID, auditAction, "QUEUE_TICKET", &ticket.ID,
|
||||
map[string]any{"display_number": displayNumber, "party_size": ticket.PartySize, "duplicate_count": len(duplicates), "duplicate_confirmed": input.AllowDuplicate}); err != nil {
|
||||
return err
|
||||
}
|
||||
ticketResponse, err := s.staffTicketView(ticket)
|
||||
if err != nil {
|
||||
return err
|
||||
var ticketResponse map[string]any
|
||||
if publicView {
|
||||
phoneLast4, err := s.ticketPhoneLast4(ticket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ticketResponse = map[string]any{
|
||||
"id": ticket.ID, "display_number": ticket.DisplayNumber, "ticket_number": ticket.DisplayNumber,
|
||||
"status": ticket.Status, "party_size": ticket.PartySize, "phone_last4": phoneLast4, "created_at": ticket.JoinedAt,
|
||||
}
|
||||
} else {
|
||||
ticketResponse, err = s.staffTicketView(ticket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !publicView {
|
||||
ticketResponse["public_token"] = publicToken
|
||||
ticketResponse["public_url"] = "/visitor/" + publicToken
|
||||
}
|
||||
ticketResponse["public_token"] = publicToken
|
||||
ticketResponse["public_url"] = "/visitor/" + publicToken
|
||||
response := map[string]any{
|
||||
"ticket": ticketResponse, "public_token": publicToken,
|
||||
"ticket": ticketResponse, "public_token": publicToken, "public_url": "/visitor/" + publicToken,
|
||||
"status_path": "/api/public/status/" + publicToken, "revision": revision,
|
||||
}
|
||||
responseBody, err = marshalResponse(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return saveIdempotent(tx, projectID, user.ID, "CREATE_TICKET", key, hash, responseCode, responseBody, &ticket.ID, now)
|
||||
return saveIdempotent(tx, projectID, actorID, scope, key, hash, responseCode, responseBody, &ticket.ID, now)
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
@@ -323,6 +397,7 @@ func (s *Server) createTicket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type callNextRequest struct {
|
||||
ExpectedRevision *int64 `json:"expected_revision"`
|
||||
Mode string `json:"mode"`
|
||||
Count *int `json:"count"`
|
||||
}
|
||||
|
||||
@@ -345,12 +420,20 @@ func (s *Server) callNext(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, &apiError{Status: http.StatusUnprocessableEntity, Code: "EXPECTED_REVISION_REQUIRED", Message: "必须提供当前队列 revision"})
|
||||
return
|
||||
}
|
||||
mode := strings.ToUpper(strings.TrimSpace(input.Mode))
|
||||
if mode == "" {
|
||||
mode = model.CallModeTicket
|
||||
}
|
||||
if mode != model.CallModeTicket && mode != model.CallModePeople {
|
||||
writeError(w, &apiError{Status: http.StatusUnprocessableEntity, Code: "INVALID_CALL_MODE", Message: "叫号方式不正确"})
|
||||
return
|
||||
}
|
||||
count := 1
|
||||
if input.Count != nil {
|
||||
count = *input.Count
|
||||
}
|
||||
if count < 1 || count > 100 {
|
||||
writeError(w, &apiError{Status: http.StatusUnprocessableEntity, Code: "INVALID_CALL_COUNT", Message: "叫号数量必须在 1 到 100 之间"})
|
||||
if count < 1 || count > 10000 {
|
||||
writeError(w, &apiError{Status: http.StatusUnprocessableEntity, Code: "INVALID_CALL_COUNT", Message: "叫号数量必须是有效的正整数"})
|
||||
return
|
||||
}
|
||||
key, err := readIdempotencyKey(r)
|
||||
@@ -358,7 +441,7 @@ func (s *Server) callNext(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
hash, _ := requestHash(map[string]any{"operation": "CALL_NEXT", "project_id": projectID, "expected_revision": *input.ExpectedRevision, "count": count})
|
||||
hash, _ := requestHash(map[string]any{"operation": "CALL_NEXT", "project_id": projectID, "expected_revision": *input.ExpectedRevision, "mode": mode, "count": count})
|
||||
user := currentPrincipal(r.Context()).User
|
||||
var responseBody []byte
|
||||
responseCode := http.StatusOK
|
||||
@@ -380,15 +463,42 @@ func (s *Server) callNext(w http.ResponseWriter, r *http.Request) {
|
||||
Details: map[string]any{"expected_revision": *input.ExpectedRevision, "current_revision": session.Revision},
|
||||
}
|
||||
}
|
||||
var tickets []model.QueueTicket
|
||||
if !projectAllowsCallMode(project.CallMode, mode) {
|
||||
return &apiError{Status: http.StatusUnprocessableEntity, Code: "CALL_MODE_NOT_ALLOWED", Message: "当前项目不支持该叫号方式"}
|
||||
}
|
||||
limit := project.MaxCallTicketCount
|
||||
unit := "个号码"
|
||||
if mode == model.CallModePeople {
|
||||
limit = project.MaxCallPeopleCount
|
||||
unit = "人"
|
||||
}
|
||||
if count > limit {
|
||||
return &apiError{
|
||||
Status: http.StatusUnprocessableEntity, Code: "CALL_COUNT_EXCEEDS_LIMIT",
|
||||
Message: fmt.Sprintf("本次最多可输入 %d %s", limit, unit),
|
||||
Details: map[string]any{"mode": mode, "max_count": limit},
|
||||
}
|
||||
}
|
||||
var waitingTickets []model.QueueTicket
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("project_id = ? AND queue_session_id = ? AND status = ?", projectID, session.ID, model.TicketWaiting).
|
||||
Order("ticket_number ASC").Limit(count).Find(&tickets).Error; err != nil {
|
||||
Order("ticket_number ASC").Limit(count).Find(&waitingTickets).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tickets) == 0 {
|
||||
if len(waitingTickets) == 0 {
|
||||
return &apiError{Status: http.StatusConflict, Code: "QUEUE_EMPTY", Message: "当前没有等待中的号码"}
|
||||
}
|
||||
tickets, peopleCount, err := domain.SelectTicketsForCall(waitingTickets, mode, count)
|
||||
if errors.Is(err, domain.ErrFirstTicketExceedsPeopleTarget) {
|
||||
return &apiError{
|
||||
Status: http.StatusConflict, Code: "NEXT_TICKET_EXCEEDS_PEOPLE_TARGET",
|
||||
Message: fmt.Sprintf("队首号码有 %d 人,请输入不少于 %d 人", waitingTickets[0].PartySize, waitingTickets[0].PartySize),
|
||||
Details: map[string]any{"next_ticket_number": waitingTickets[0].DisplayNumber, "party_size": waitingTickets[0].PartySize},
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := s.now()
|
||||
autoCompletedBatchIDs, autoCompletedTicketCount, err := autoCompleteActiveBatches(tx, projectID, now)
|
||||
if err != nil {
|
||||
@@ -403,6 +513,7 @@ func (s *Server) callNext(w http.ResponseWriter, r *http.Request) {
|
||||
batch := model.CallBatch{
|
||||
ID: uuid.NewString(), ProjectID: projectID, QueueSessionID: session.ID,
|
||||
BatchSequence: lastSequence + 1, Revision: revision, Status: "CALLED",
|
||||
CallMode: mode, RequestedCount: count, TicketCount: len(tickets), PeopleCount: peopleCount,
|
||||
RequestedBy: user.ID, CalledAt: now, CreatedAt: now,
|
||||
}
|
||||
if err := tx.Create(&batch).Error; err != nil {
|
||||
@@ -438,7 +549,8 @@ func (s *Server) callNext(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if err := s.addAudit(tx, r, &projectID, &user.ID, "CALL_NEXT", "CALL_BATCH", &batch.ID,
|
||||
map[string]any{
|
||||
"ticket_count": len(tickets), "revision": revision, "device_outcome": outcome,
|
||||
"call_mode": mode, "requested_count": count, "ticket_count": len(tickets), "people_count": peopleCount,
|
||||
"revision": revision, "device_outcome": outcome,
|
||||
"auto_completed_batch_ids": autoCompletedBatchIDs, "auto_completed_ticket_count": autoCompletedTicketCount,
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -469,6 +581,10 @@ func (s *Server) callNext(w http.ResponseWriter, r *http.Request) {
|
||||
writeRawJSON(w, responseCode, responseBody)
|
||||
}
|
||||
|
||||
func projectAllowsCallMode(projectMode, requestedMode string) bool {
|
||||
return projectMode == model.CallModeBoth || projectMode == requestedMode
|
||||
}
|
||||
|
||||
func autoCompleteActiveBatches(tx *gorm.DB, projectID string, now time.Time) ([]string, int64, error) {
|
||||
var batches []model.CallBatch
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
@@ -693,6 +809,7 @@ func (s *Server) staffTicketView(ticket model.QueueTicket) (map[string]any, erro
|
||||
}
|
||||
return map[string]any{
|
||||
"id": ticket.ID, "display_number": ticket.DisplayNumber, "ticket_number": ticket.DisplayNumber, "status": ticket.Status,
|
||||
"party_size": ticket.PartySize,
|
||||
"reissued_from_ticket_id": ticket.ReissuedFromTicketID,
|
||||
"phone": phone, "last_name": lastName, "honorific": ticket.Honorific,
|
||||
"joined_at": ticket.JoinedAt, "called_at": ticket.CalledAt,
|
||||
@@ -712,6 +829,8 @@ func (s *Server) staffCallBatchView(batch model.CallBatch, tickets []model.Queue
|
||||
}
|
||||
return map[string]any{
|
||||
"id": batch.ID, "sequence": batch.BatchSequence, "batch_number": batch.BatchSequence, "status": batch.Status,
|
||||
"call_mode": batch.CallMode, "requested_count": batch.RequestedCount,
|
||||
"ticket_count": batch.TicketCount, "people_count": batch.PeopleCount,
|
||||
"called_at": batch.CalledAt, "revision": batch.Revision, "tickets": views,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user