Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
154 lines
6.2 KiB
Go
154 lines
6.2 KiB
Go
package repositories
|
|
|
|
import (
|
|
"agent-desk/internal/models"
|
|
"agent-desk/internal/pkg/enums"
|
|
"errors"
|
|
"github.com/mlogclub/simple/sqls"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
"time"
|
|
)
|
|
|
|
var SupportAvailabilityRepository = &supportAvailabilityRepository{}
|
|
|
|
type supportAvailabilityRepository struct{}
|
|
|
|
func (r *supportAvailabilityRepository) Calendar(db *gorm.DB, teamID int64) (*models.SupportCalendar, error) {
|
|
var item models.SupportCalendar
|
|
err := db.Where("team_id = ?", teamID).Take(&item).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return &item, err
|
|
}
|
|
func (r *supportAvailabilityRepository) SaveCalendar(db *gorm.DB, item *models.SupportCalendar) error {
|
|
return db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "team_id"}}, DoUpdates: clause.AssignmentColumns([]string{"mode", "timezone", "weekly_json", "exceptions_json", "updated_at"})}).Create(item).Error
|
|
}
|
|
func (r *supportAvailabilityRepository) RenewPresence(db *gorm.DB, item *models.AgentPresenceLease) error {
|
|
return db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"expires_at"})}).Create(item).Error
|
|
}
|
|
func (r *supportAvailabilityRepository) EndPresence(db *gorm.DB, id string, at time.Time) error {
|
|
return db.Model(&models.AgentPresenceLease{}).Where("id = ? AND expires_at > ?", id, at).Update("expires_at", at).Error
|
|
}
|
|
func (r *supportAvailabilityRepository) PrunePresence(db *gorm.DB, now time.Time) error {
|
|
return db.Where("expires_at <= ?", now).Delete(&models.AgentPresenceLease{}).Error
|
|
}
|
|
func (r *supportAvailabilityRepository) OnlineUsers(db *gorm.DB, userIDs []int64, now time.Time) (map[int64]bool, error) {
|
|
result := map[int64]bool{}
|
|
if len(userIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
var leases []models.AgentPresenceLease
|
|
if err := db.Where("user_id IN ? AND expires_at > ?", userIDs, now).Find(&leases).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
sessionIDs := make([]int64, 0, len(leases))
|
|
for _, lease := range leases {
|
|
sessionIDs = append(sessionIDs, lease.LoginSessionID)
|
|
}
|
|
if len(sessionIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
var sessions []models.LoginSession
|
|
if err := db.Select("id", "user_id", "expired_at", "revoked_at").Where("id IN ? AND expired_at > ? AND revoked_at IS NULL", sessionIDs, now).Find(&sessions).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
valid := map[int64]int64{}
|
|
for _, session := range sessions {
|
|
valid[session.ID] = session.UserID
|
|
}
|
|
for _, lease := range leases {
|
|
if valid[lease.LoginSessionID] == lease.UserID {
|
|
result[lease.UserID] = true
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// A portable write acquires the profile row's write lock on MySQL and the
|
|
// transaction write lock on SQLite. All assignment paths take this before count.
|
|
func (r *supportAvailabilityRepository) LockProfile(db *gorm.DB, userID int64) (*models.AgentProfile, error) {
|
|
result := db.Model(&models.AgentProfile{}).Where("user_id = ?", userID).UpdateColumn("assignment_version", gorm.Expr("assignment_version + 1"))
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
if result.RowsAffected == 0 {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
var item models.AgentProfile
|
|
err := db.Where("user_id = ?", userID).Take(&item).Error
|
|
return &item, err
|
|
}
|
|
func (r *supportAvailabilityRepository) ActiveCount(db *gorm.DB, userID, excludeConversationID int64) (int, error) {
|
|
var count int64
|
|
q := db.Model(&models.Conversation{}).Where("current_assignee_id = ? AND status = ?", userID, enums.IMConversationStatusActive)
|
|
if excludeConversationID > 0 {
|
|
q = q.Where("id <> ?", excludeConversationID)
|
|
}
|
|
err := q.Count(&count).Error
|
|
return int(count), err
|
|
}
|
|
func (r *supportAvailabilityRepository) LegacySchedules(db *gorm.DB, teamID int64, now time.Time) ([]models.AgentTeamSchedule, error) {
|
|
var items []models.AgentTeamSchedule
|
|
err := db.Where("team_id = ? AND status = ? AND end_at > ?", teamID, enums.StatusOk, now).Order("start_at ASC").Find(&items).Error
|
|
return items, err
|
|
}
|
|
func (r *supportAvailabilityRepository) Profiles(db *gorm.DB, teamIDs []int64) ([]models.AgentProfile, error) {
|
|
var items []models.AgentProfile
|
|
q := db.Where("status <> ?", enums.StatusDeleted)
|
|
if teamIDs != nil {
|
|
if len(teamIDs) == 0 {
|
|
return []models.AgentProfile{}, nil
|
|
}
|
|
q = q.Where("team_id IN ?", teamIDs)
|
|
}
|
|
err := q.Order("id ASC").Find(&items).Error
|
|
return items, err
|
|
}
|
|
func (r *supportAvailabilityRepository) User(db *gorm.DB, id int64) (*models.User, error) {
|
|
var item models.User
|
|
err := db.First(&item, id).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return &item, err
|
|
}
|
|
func (r *supportAvailabilityRepository) Team(db *gorm.DB, id int64) (*models.AgentTeam, error) {
|
|
var item models.AgentTeam
|
|
err := db.First(&item, id).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return &item, err
|
|
}
|
|
|
|
func (r *supportAvailabilityRepository) AssignConversation(db *gorm.DB, previous *models.Conversation, values map[string]any) error {
|
|
result := db.Model(&models.Conversation{}).Where("id = ? AND status = ? AND current_assignee_id = ?", previous.ID, previous.Status, previous.CurrentAssigneeID).Updates(values)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
return gorm.ErrInvalidTransaction
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *supportAvailabilityRepository) ProfileByUser(db *gorm.DB, userID int64) (*models.AgentProfile, error) {
|
|
var item models.AgentProfile
|
|
err := db.Where("user_id = ?", userID).Take(&item).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return &item, err
|
|
}
|
|
|
|
// EffectiveStatusCondition mirrors the vacation-boundary policy for paginated
|
|
// list filters. Filtering the stored Vacation value would contradict the badge.
|
|
func (r *supportAvailabilityRepository) EffectiveStatusCondition(cnd *sqls.Cnd, status enums.ServiceStatus, now time.Time) {
|
|
cnd.Where(`(CASE WHEN service_status <> ? THEN service_status
|
|
WHEN absence_start_at IS NOT NULL AND absence_end_at IS NOT NULL AND absence_start_at <= ? AND absence_end_at > ? THEN ?
|
|
WHEN availability_return_status >= ? AND availability_return_status <= ? THEN availability_return_status
|
|
ELSE ? END) = ?`, enums.ServiceStatusVacation, now, now, enums.ServiceStatusVacation, enums.ServiceStatusIdle, enums.ServiceStatusUnavailable, enums.ServiceStatusUnavailable, status)
|
|
}
|