Update
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsService interface {
|
||||
GetDashboard(ctx context.Context, req *contract.AnalyticsDashboardRequest) (*contract.AnalyticsDashboardResponse, error)
|
||||
GetLetterVolume(ctx context.Context) (*contract.LetterVolumeByTypeResponse, error)
|
||||
}
|
||||
|
||||
type AnalyticsServiceImpl struct {
|
||||
analyticsRepo *repository.AnalyticsRepository
|
||||
}
|
||||
|
||||
func NewAnalyticsService(analyticsRepo *repository.AnalyticsRepository) *AnalyticsServiceImpl {
|
||||
return &AnalyticsServiceImpl{
|
||||
analyticsRepo: analyticsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.AnalyticsDashboardRequest) (*contract.AnalyticsDashboardResponse, error) {
|
||||
// Parse dates
|
||||
var startDate, endDate time.Time
|
||||
if req.StartDate != "" {
|
||||
if date, err := time.Parse("2006-01-02", req.StartDate); err == nil {
|
||||
startDate = date
|
||||
}
|
||||
} else {
|
||||
// Default to last 30 days
|
||||
startDate = time.Now().AddDate(0, 0, -30)
|
||||
}
|
||||
|
||||
if req.EndDate != "" {
|
||||
if date, err := time.Parse("2006-01-02", req.EndDate); err == nil {
|
||||
endDate = date.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
}
|
||||
} else {
|
||||
endDate = time.Now()
|
||||
}
|
||||
|
||||
// Apply user context filters if not admin
|
||||
var userID *uuid.UUID
|
||||
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
if appCtx != nil && appCtx.UserRole != "admin" && appCtx.UserRole != "superadmin" {
|
||||
userID = &appCtx.UserID
|
||||
}
|
||||
|
||||
response := &contract.AnalyticsDashboardResponse{}
|
||||
|
||||
// Get summary statistics - don't filter by department for overall stats
|
||||
summaryData, err := s.analyticsRepo.GetLetterSummaryStats(ctx, startDate, endDate, userID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.Summary = s.mapSummaryStats(summaryData)
|
||||
|
||||
// Calculate growth metrics
|
||||
response.Summary.WeekOverWeekGrowth = s.calculateWeekOverWeekGrowth(ctx)
|
||||
response.Summary.MonthOverMonthGrowth = s.calculateMonthOverMonthGrowth(ctx)
|
||||
|
||||
// Get priority distribution
|
||||
priorityData, err := s.analyticsRepo.GetPriorityDistribution(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.PriorityDistribution = s.mapPriorityDistribution(priorityData)
|
||||
|
||||
// Get department statistics
|
||||
deptData, err := s.analyticsRepo.GetDepartmentStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.DepartmentStats = s.mapDepartmentStats(deptData)
|
||||
|
||||
// Get monthly trend (last 12 months)
|
||||
monthlyData, err := s.analyticsRepo.GetMonthlyTrend(ctx, 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.MonthlyTrend = s.mapMonthlyTrend(monthlyData)
|
||||
|
||||
// Get simplified department stats (departments_stats)
|
||||
response.DepartmentsStats = s.getSimpleDepartmentStats(ctx, startDate, endDate)
|
||||
|
||||
// Get institution statistics
|
||||
instData, err := s.analyticsRepo.GetInstitutionStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.InstitutionStats = s.mapInstitutionStats(instData)
|
||||
|
||||
// Get daily activity (last 7 days)
|
||||
dailyData, err := s.analyticsRepo.GetDailyActivity(ctx, 7)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.DailyActivity = s.mapDailyActivity(dailyData)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) GetLetterVolume(ctx context.Context) (*contract.LetterVolumeByTypeResponse, error) {
|
||||
// This would be implemented with specific queries for volume metrics
|
||||
// For now, returning a placeholder
|
||||
return &contract.LetterVolumeByTypeResponse{
|
||||
Incoming: contract.IncomingLetterVolume{
|
||||
Today: 0,
|
||||
ThisWeek: 0,
|
||||
ThisMonth: 0,
|
||||
ThisYear: 0,
|
||||
Total: 0,
|
||||
},
|
||||
Outgoing: contract.OutgoingLetterVolume{
|
||||
Today: 0,
|
||||
ThisWeek: 0,
|
||||
ThisMonth: 0,
|
||||
ThisYear: 0,
|
||||
Total: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions to map repository data to contract types
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapSummaryStats(data map[string]interface{}) contract.LetterSummaryStats {
|
||||
return contract.LetterSummaryStats{
|
||||
TotalIncoming: getInt64Value(data["total_incoming"]),
|
||||
TotalOutgoing: getInt64Value(data["total_outgoing"]),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateWeekOverWeekGrowth calculates the week over week growth rate
|
||||
func (s *AnalyticsServiceImpl) calculateWeekOverWeekGrowth(ctx context.Context) float64 {
|
||||
// Get this week's data
|
||||
thisWeekStart := time.Now().AddDate(0, 0, -int(time.Now().Weekday()))
|
||||
thisWeekEnd := time.Now()
|
||||
|
||||
// Get last week's data
|
||||
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
|
||||
lastWeekEnd := thisWeekStart.AddDate(0, 0, -1)
|
||||
|
||||
thisWeekData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, thisWeekStart, thisWeekEnd, nil, nil)
|
||||
lastWeekData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, lastWeekStart, lastWeekEnd, nil, nil)
|
||||
|
||||
thisWeekTotal := getInt64Value(thisWeekData["total_incoming"]) + getInt64Value(thisWeekData["total_outgoing"])
|
||||
lastWeekTotal := getInt64Value(lastWeekData["total_incoming"]) + getInt64Value(lastWeekData["total_outgoing"])
|
||||
|
||||
if lastWeekTotal > 0 {
|
||||
return float64((thisWeekTotal - lastWeekTotal) * 100 / lastWeekTotal)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// calculateMonthOverMonthGrowth calculates the month over month growth rate
|
||||
func (s *AnalyticsServiceImpl) calculateMonthOverMonthGrowth(ctx context.Context) float64 {
|
||||
// Get this month's data
|
||||
now := time.Now()
|
||||
thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
thisMonthEnd := now
|
||||
|
||||
// Get last month's data
|
||||
lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
|
||||
lastMonthEnd := thisMonthStart.AddDate(0, 0, -1)
|
||||
|
||||
thisMonthData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, thisMonthStart, thisMonthEnd, nil, nil)
|
||||
lastMonthData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, lastMonthStart, lastMonthEnd, nil, nil)
|
||||
|
||||
thisMonthTotal := getInt64Value(thisMonthData["total_incoming"]) + getInt64Value(thisMonthData["total_outgoing"])
|
||||
lastMonthTotal := getInt64Value(lastMonthData["total_incoming"]) + getInt64Value(lastMonthData["total_outgoing"])
|
||||
|
||||
if lastMonthTotal > 0 {
|
||||
return float64((thisMonthTotal - lastMonthTotal) * 100 / lastMonthTotal)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getSimpleDepartmentStats gets simplified department statistics
|
||||
func (s *AnalyticsServiceImpl) getSimpleDepartmentStats(ctx context.Context, startDate, endDate time.Time) []contract.SimpleDepartmentStats {
|
||||
// Get department stats with letter counts
|
||||
deptData, err := s.analyticsRepo.GetDepartmentStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return []contract.SimpleDepartmentStats{}
|
||||
}
|
||||
|
||||
result := make([]contract.SimpleDepartmentStats, 0, len(deptData))
|
||||
for _, item := range deptData {
|
||||
deptIDStr := getStringValue(item["department_id"])
|
||||
deptID, err := uuid.Parse(deptIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate total letter count (incoming + outgoing)
|
||||
letterCount := getInt64Value(item["incoming_count"]) + getInt64Value(item["outgoing_count"])
|
||||
|
||||
result = append(result, contract.SimpleDepartmentStats{
|
||||
DepartmentID: deptID,
|
||||
Department: getStringValue(item["department_name"]),
|
||||
LetterCount: letterCount,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapStatusDistribution(data []map[string]interface{}) []contract.StatusDistribution {
|
||||
result := make([]contract.StatusDistribution, 0, len(data))
|
||||
for _, item := range data {
|
||||
result = append(result, contract.StatusDistribution{
|
||||
Status: getStringValue(item["status"]),
|
||||
Count: getInt64Value(item["count"]),
|
||||
Percentage: getFloat64Value(item["percentage"]),
|
||||
Type: getStringValue(item["type"]),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapPriorityDistribution(data []map[string]interface{}) []contract.PriorityDistribution {
|
||||
result := make([]contract.PriorityDistribution, 0, len(data))
|
||||
for _, item := range data {
|
||||
result = append(result, contract.PriorityDistribution{
|
||||
PriorityID: getStringValue(item["priority_id"]),
|
||||
PriorityName: getStringValue(item["priority_name"]),
|
||||
Level: getIntValue(item["level"]),
|
||||
Count: getInt64Value(item["count"]),
|
||||
Percentage: getFloat64Value(item["percentage"]),
|
||||
AvgResponseTime: getFloat64Value(item["avg_response_time"]),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapDepartmentStats(data []map[string]interface{}) []contract.DepartmentStats {
|
||||
result := make([]contract.DepartmentStats, 0, len(data))
|
||||
for _, item := range data {
|
||||
if deptID, err := uuid.Parse(getStringValue(item["department_id"])); err == nil {
|
||||
result = append(result, contract.DepartmentStats{
|
||||
DepartmentID: deptID,
|
||||
DepartmentName: getStringValue(item["department_name"]),
|
||||
DepartmentCode: getStringValue(item["department_code"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
PendingCount: getInt64Value(item["pending_count"]),
|
||||
AvgResponseTime: getFloat64Value(item["avg_response_time"]),
|
||||
CompletionRate: getFloat64Value(item["completion_rate"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapMonthlyTrend(data []map[string]interface{}) []contract.MonthlyTrend {
|
||||
result := make([]contract.MonthlyTrend, 0, len(data))
|
||||
for _, item := range data {
|
||||
result = append(result, contract.MonthlyTrend{
|
||||
Month: getStringValue(item["month"]),
|
||||
Year: getIntValue(item["year"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
TotalCount: getInt64Value(item["total_count"]),
|
||||
GrowthRate: getFloat64Value(item["growth_rate"]),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapTopUsers(data []map[string]interface{}) []contract.TopUserStats {
|
||||
result := make([]contract.TopUserStats, 0, len(data))
|
||||
for _, item := range data {
|
||||
if userID, err := uuid.Parse(getStringValue(item["user_id"])); err == nil {
|
||||
result = append(result, contract.TopUserStats{
|
||||
UserID: userID,
|
||||
UserName: getStringValue(item["user_name"]),
|
||||
UserEmail: getStringValue(item["user_email"]),
|
||||
Department: getStringValue(item["department"]),
|
||||
LetterCount: getInt64Value(item["letter_count"]),
|
||||
AvgResponseTime: getFloat64Value(item["avg_response_time"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapInstitutionStats(data []map[string]interface{}) []contract.InstitutionStats {
|
||||
result := make([]contract.InstitutionStats, 0, len(data))
|
||||
for _, item := range data {
|
||||
if instID, err := uuid.Parse(getStringValue(item["institution_id"])); err == nil {
|
||||
stat := contract.InstitutionStats{
|
||||
InstitutionID: instID,
|
||||
InstitutionName: getStringValue(item["institution_name"]),
|
||||
InstitutionType: getStringValue(item["institution_type"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
TotalCount: getInt64Value(item["total_count"]),
|
||||
}
|
||||
|
||||
if lastActivity, ok := item["last_activity"].(time.Time); ok {
|
||||
stat.LastActivity = lastActivity
|
||||
}
|
||||
|
||||
result = append(result, stat)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapApprovalMetrics(data map[string]interface{}) contract.ApprovalMetrics {
|
||||
return contract.ApprovalMetrics{
|
||||
TotalSubmitted: getInt64Value(data["total_submitted"]),
|
||||
TotalApproved: getInt64Value(data["total_approved"]),
|
||||
TotalRejected: getInt64Value(data["total_rejected"]),
|
||||
TotalPending: getInt64Value(data["total_pending"]),
|
||||
ApprovalRate: getFloat64Value(data["approval_rate"]),
|
||||
RejectionRate: getFloat64Value(data["rejection_rate"]),
|
||||
AvgApprovalTime: getFloat64Value(data["avg_approval_time"]),
|
||||
AvgApprovalSteps: getFloat64Value(data["avg_approval_steps"]),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapResponseTimeStats(data map[string]interface{}) contract.ResponseTimeStats {
|
||||
return contract.ResponseTimeStats{
|
||||
MinResponseTime: getFloat64Value(data["min_response_time"]),
|
||||
MaxResponseTime: getFloat64Value(data["max_response_time"]),
|
||||
AvgResponseTime: getFloat64Value(data["avg_response_time"]),
|
||||
MedianResponseTime: getFloat64Value(data["median_response_time"]),
|
||||
P95ResponseTime: getFloat64Value(data["p95_response_time"]),
|
||||
P99ResponseTime: getFloat64Value(data["p99_response_time"]),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapDailyActivity(data []map[string]interface{}) []contract.DailyActivity {
|
||||
result := make([]contract.DailyActivity, 0, len(data))
|
||||
for _, item := range data {
|
||||
activity := contract.DailyActivity{
|
||||
Date: "", // Leave date empty as per requirement
|
||||
DayOfWeek: getStringValue(item["day_of_week"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
}
|
||||
result = append(result, activity)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper functions to safely extract values from interface{}
|
||||
|
||||
func getStringValue(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getInt64Value(v interface{}) int64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case int64:
|
||||
return val
|
||||
case float64:
|
||||
return int64(val)
|
||||
case int:
|
||||
return int64(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func getIntValue(v interface{}) int {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
return val
|
||||
case int64:
|
||||
return int(val)
|
||||
case float64:
|
||||
return int(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func getFloat64Value(v interface{}) float64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case int64:
|
||||
return float64(val)
|
||||
case int:
|
||||
return float64(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/contract"
|
||||
@@ -39,9 +41,15 @@ type LetterOutgoingService interface {
|
||||
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
|
||||
|
||||
GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error)
|
||||
|
||||
|
||||
// GetLetterApprovals returns all approvals and their status for a letter
|
||||
GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error)
|
||||
|
||||
// GetApprovalDiscussions returns both approvals and discussions for an outgoing letter
|
||||
GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error)
|
||||
|
||||
// GetApprovalTimeline returns a chronological timeline of all events for a letter
|
||||
GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error)
|
||||
}
|
||||
|
||||
type LetterOutgoingServiceImpl struct {
|
||||
@@ -106,16 +114,48 @@ func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, i
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) {
|
||||
filter := repository.ListOutgoingLettersFilter{
|
||||
Status: req.Status,
|
||||
Query: req.Query,
|
||||
CreatedBy: req.CreatedBy,
|
||||
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||
FromDate: req.FromDate,
|
||||
ToDate: req.ToDate,
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, req.Offset)
|
||||
filter := repository.ListOutgoingLettersFilter{
|
||||
CreatedBy: req.CreatedBy,
|
||||
DepartmentID: req.DepartmentID,
|
||||
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||
PriorityID: req.PriorityID,
|
||||
}
|
||||
|
||||
if req.Status != "" {
|
||||
filter.Status = &req.Status
|
||||
}
|
||||
if req.Query != "" {
|
||||
filter.Query = &req.Query
|
||||
}
|
||||
if req.SortBy != "" {
|
||||
filter.SortBy = &req.SortBy
|
||||
}
|
||||
if req.SortOrder != "" {
|
||||
filter.SortOrder = &req.SortOrder
|
||||
}
|
||||
|
||||
if req.FromDate != "" {
|
||||
if date, err := time.Parse("2006-01-02", req.FromDate); err == nil {
|
||||
filter.FromDate = &date
|
||||
}
|
||||
}
|
||||
|
||||
if req.ToDate != "" {
|
||||
if date, err := time.Parse("2006-01-02", req.ToDate); err == nil {
|
||||
endOfDay := date.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
filter.ToDate = &endOfDay
|
||||
}
|
||||
}
|
||||
|
||||
// Apply access control overrides based on user context
|
||||
ApplyLetterFilterOverrides(ctx, &filter)
|
||||
|
||||
letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -523,67 +563,104 @@ func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discus
|
||||
func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error) {
|
||||
userID := getUserIDFromContext(ctx)
|
||||
|
||||
_, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
||||
// Verify letter exists
|
||||
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all approvals for this letter
|
||||
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var currentApproval *entities.LetterOutgoingApproval
|
||||
// Group approvals by step order to understand the workflow
|
||||
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
|
||||
for _, approval := range approvals {
|
||||
approvalsByStep[approval.StepOrder] = append(approvalsByStep[approval.StepOrder], approval)
|
||||
}
|
||||
|
||||
// Find the current active step (lowest step order with pending approvals)
|
||||
var currentStepOrder int = -1
|
||||
var userApproval *entities.LetterOutgoingApproval
|
||||
var isApproverOnActiveStep bool
|
||||
var canApprove bool
|
||||
|
||||
for _, approval := range approvals {
|
||||
if approval.Status == entities.ApprovalStatusPending {
|
||||
currentApproval = &approval
|
||||
break
|
||||
// Find the minimum step order that has pending approvals
|
||||
for stepOrder, stepApprovals := range approvalsByStep {
|
||||
hasPending := false
|
||||
for _, approval := range stepApprovals {
|
||||
if approval.Status == entities.ApprovalStatusPending {
|
||||
hasPending = true
|
||||
// Check if this user is an approver for this pending approval
|
||||
if approval.ApproverID != nil && *approval.ApproverID == userID {
|
||||
if currentStepOrder == -1 || stepOrder < currentStepOrder {
|
||||
currentStepOrder = stepOrder
|
||||
userApproval = &approval
|
||||
isApproverOnActiveStep = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Track the lowest pending step
|
||||
if hasPending && (currentStepOrder == -1 || stepOrder < currentStepOrder) {
|
||||
currentStepOrder = stepOrder
|
||||
}
|
||||
}
|
||||
|
||||
// Check if current user is the approver for the active step
|
||||
if currentApproval != nil && currentApproval.Step != nil {
|
||||
step := currentApproval.Step
|
||||
|
||||
// Check if user is the specific approver
|
||||
if step.ApproverUserID != nil && *step.ApproverUserID == userID {
|
||||
isApproverOnActiveStep = true
|
||||
canApprove = true
|
||||
}
|
||||
// Note: Role-based approval check would require additional implementation
|
||||
// For now, we only support user-specific approvers
|
||||
// User can approve if they have a pending approval on the current active step
|
||||
if isApproverOnActiveStep && userApproval != nil && userApproval.Status == entities.ApprovalStatusPending {
|
||||
canApprove = true
|
||||
}
|
||||
|
||||
// Build actions based on current status
|
||||
// Build actions based on eligibility
|
||||
var actions []contract.ApprovalAction
|
||||
if canApprove && currentApproval != nil {
|
||||
if canApprove && userApproval != nil {
|
||||
actions = []contract.ApprovalAction{
|
||||
{
|
||||
Type: "APPROVE",
|
||||
Href: fmt.Sprintf("/v1/letters/%s/approvals/%s/decision", letterID, currentApproval.ID),
|
||||
Href: fmt.Sprintf("/api/v1/letters/outgoing/%s/approve", letterID),
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Type: "REJECT",
|
||||
Href: fmt.Sprintf("/v1/letters/%s/approvals/%s/decision", letterID, currentApproval.ID),
|
||||
Href: fmt.Sprintf("/api/v1/letters/outgoing/%s/reject", letterID),
|
||||
Method: "POST",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Determine decision status
|
||||
// Determine overall decision status
|
||||
decisionStatus := "PENDING"
|
||||
if currentApproval == nil {
|
||||
|
||||
// Check if all required approvals are completed
|
||||
allCompleted := true
|
||||
hasRejection := false
|
||||
for _, approval := range approvals {
|
||||
// Check required approvals only
|
||||
if approval.IsRequired {
|
||||
if approval.Status == entities.ApprovalStatusPending || approval.Status == entities.ApprovalStatusNotStarted {
|
||||
allCompleted = false
|
||||
}
|
||||
if approval.Status == entities.ApprovalStatusRejected {
|
||||
hasRejection = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasRejection {
|
||||
decisionStatus = "REJECTED"
|
||||
} else if allCompleted {
|
||||
decisionStatus = "COMPLETED"
|
||||
} else if letter.Status == entities.LetterOutgoingStatusPendingApproval {
|
||||
decisionStatus = "PENDING"
|
||||
}
|
||||
|
||||
// Determine notes visibility
|
||||
notesVisibility := "FULL"
|
||||
if !isApproverOnActiveStep {
|
||||
notesVisibility = "READONLY"
|
||||
notesVisibility := "READONLY"
|
||||
if canApprove {
|
||||
notesVisibility = "FULL"
|
||||
}
|
||||
|
||||
info := &contract.LetterApprovalInfoResponse{
|
||||
@@ -597,6 +674,127 @@ func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, l
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error) {
|
||||
// Get letter details
|
||||
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all approvals for this letter
|
||||
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort approvals by step order and parallel group
|
||||
sort.Slice(approvals, func(i, j int) bool {
|
||||
if approvals[i].StepOrder != approvals[j].StepOrder {
|
||||
return approvals[i].StepOrder < approvals[j].StepOrder
|
||||
}
|
||||
return approvals[i].ParallelGroup < approvals[j].ParallelGroup
|
||||
})
|
||||
|
||||
// Transform to response format
|
||||
approvalResponses := make([]contract.EnhancedOutgoingLetterApprovalResponse, 0, len(approvals))
|
||||
totalSteps := 0
|
||||
currentStep := 0
|
||||
stepOrdersSeen := make(map[int]bool)
|
||||
|
||||
for _, approval := range approvals {
|
||||
// Count unique step orders for total steps
|
||||
if !stepOrdersSeen[approval.StepOrder] {
|
||||
stepOrdersSeen[approval.StepOrder] = true
|
||||
totalSteps++
|
||||
}
|
||||
|
||||
// Determine current step (lowest step with pending/not_started status)
|
||||
if approval.Status == entities.ApprovalStatusPending && (currentStep == 0 || approval.StepOrder < currentStep) {
|
||||
currentStep = approval.StepOrder
|
||||
}
|
||||
|
||||
approvalResp := contract.EnhancedOutgoingLetterApprovalResponse{
|
||||
ID: approval.ID,
|
||||
LetterID: approval.LetterID,
|
||||
StepID: approval.StepID,
|
||||
StepOrder: approval.StepOrder,
|
||||
ParallelGroup: approval.ParallelGroup,
|
||||
IsRequired: approval.IsRequired,
|
||||
ApproverID: approval.ApproverID,
|
||||
Status: string(approval.Status),
|
||||
Remarks: approval.Remarks,
|
||||
ActedAt: approval.ActedAt,
|
||||
CreatedAt: approval.CreatedAt,
|
||||
}
|
||||
|
||||
// Add step details if available
|
||||
if approval.Step != nil {
|
||||
approvalResp.Step = &contract.ApprovalFlowStepResponse{
|
||||
ID: approval.Step.ID,
|
||||
StepOrder: approval.Step.StepOrder,
|
||||
ParallelGroup: approval.Step.ParallelGroup,
|
||||
Required: approval.Step.Required,
|
||||
CreatedAt: approval.Step.CreatedAt,
|
||||
UpdatedAt: approval.Step.UpdatedAt,
|
||||
}
|
||||
|
||||
// Add approver role if available
|
||||
if approval.Step.ApproverRole != nil {
|
||||
approvalResp.Step.ApproverRole = &contract.RoleResponse{
|
||||
ID: approval.Step.ApproverRole.ID,
|
||||
Name: approval.Step.ApproverRole.Name,
|
||||
Code: approval.Step.ApproverRole.Code,
|
||||
}
|
||||
}
|
||||
|
||||
// Add approver user if available
|
||||
if approval.Step.ApproverUser != nil {
|
||||
approvalResp.Step.ApproverUser = &contract.UserResponse{
|
||||
ID: approval.Step.ApproverUser.ID,
|
||||
Name: approval.Step.ApproverUser.Name,
|
||||
Email: approval.Step.ApproverUser.Email,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add approver details if available
|
||||
if approval.Approver != nil {
|
||||
approvalResp.Approver = &contract.UserResponse{
|
||||
ID: approval.Approver.ID,
|
||||
Name: approval.Approver.Name,
|
||||
Email: approval.Approver.Email,
|
||||
}
|
||||
}
|
||||
|
||||
approvalResponses = append(approvalResponses, approvalResp)
|
||||
}
|
||||
|
||||
// If no current step found but there are approvals, check if all are completed
|
||||
if currentStep == 0 && len(approvals) > 0 {
|
||||
allCompleted := true
|
||||
for _, approval := range approvals {
|
||||
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
|
||||
allCompleted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allCompleted {
|
||||
currentStep = totalSteps // All steps completed
|
||||
}
|
||||
}
|
||||
|
||||
response := &contract.GetLetterApprovalsResponse{
|
||||
LetterID: letter.ID,
|
||||
LetterNumber: letter.LetterNumber,
|
||||
LetterStatus: string(letter.Status),
|
||||
TotalSteps: totalSteps,
|
||||
CurrentStep: currentStep,
|
||||
Approvals: approvalResponses,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func getUserIDFromContext(ctx context.Context) uuid.UUID {
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
if appCtx != nil {
|
||||
@@ -628,14 +826,17 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
approvals := make([]contract.EnhancedOutgoingLetterApprovalResponse, 0, len(letter.Approvals))
|
||||
for _, approval := range letter.Approvals {
|
||||
approvalResp := contract.EnhancedOutgoingLetterApprovalResponse{
|
||||
ID: approval.ID,
|
||||
LetterID: approval.LetterID,
|
||||
StepID: approval.StepID,
|
||||
ApproverID: approval.ApproverID,
|
||||
Status: string(approval.Status),
|
||||
Remarks: approval.Remarks,
|
||||
ActedAt: approval.ActedAt,
|
||||
CreatedAt: approval.CreatedAt,
|
||||
ID: approval.ID,
|
||||
LetterID: approval.LetterID,
|
||||
StepID: approval.StepID,
|
||||
StepOrder: approval.StepOrder,
|
||||
ParallelGroup: approval.ParallelGroup,
|
||||
IsRequired: approval.IsRequired,
|
||||
ApproverID: approval.ApproverID,
|
||||
Status: string(approval.Status),
|
||||
Remarks: approval.Remarks,
|
||||
ActedAt: approval.ActedAt,
|
||||
CreatedAt: approval.CreatedAt,
|
||||
}
|
||||
|
||||
// Add step details if available
|
||||
@@ -648,14 +849,14 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
CreatedAt: approval.Step.CreatedAt,
|
||||
UpdatedAt: approval.Step.UpdatedAt,
|
||||
}
|
||||
|
||||
|
||||
if approval.Step.ApproverRoleID != nil {
|
||||
approvalResp.Step.ApproverRoleID = approval.Step.ApproverRoleID
|
||||
}
|
||||
if approval.Step.ApproverUserID != nil {
|
||||
approvalResp.Step.ApproverUserID = approval.Step.ApproverUserID
|
||||
}
|
||||
|
||||
|
||||
// Add role information if available
|
||||
if approval.Step.ApproverRole != nil {
|
||||
approvalResp.Step.ApproverRole = &contract.RoleResponse{
|
||||
@@ -664,7 +865,7 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
Code: approval.Step.ApproverRole.Code,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add user information if available
|
||||
if approval.Step.ApproverUser != nil {
|
||||
approvalResp.Step.ApproverUser = &contract.UserResponse{
|
||||
@@ -682,7 +883,7 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
Name: approval.Approver.Name,
|
||||
Email: approval.Approver.Email,
|
||||
}
|
||||
|
||||
|
||||
// Add profile if available
|
||||
if approval.Approver.Profile != nil {
|
||||
approvalResp.Approver.Profile = &contract.UserProfileResponse{
|
||||
@@ -708,7 +909,7 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
for _, discussion := range letter.Discussions {
|
||||
// Extract mentioned user IDs from mentions
|
||||
mentionedUserIDs := extractMentionedUserIDs(discussion.Mentions)
|
||||
|
||||
|
||||
discussionResp := contract.OutgoingLetterDiscussionResponse{
|
||||
ID: discussion.ID,
|
||||
LetterID: discussion.LetterID,
|
||||
@@ -731,7 +932,7 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
CreatedAt: discussion.User.CreatedAt,
|
||||
UpdatedAt: discussion.User.UpdatedAt,
|
||||
}
|
||||
|
||||
|
||||
// Add profile if available
|
||||
if discussion.User.Profile != nil {
|
||||
discussionResp.User.Profile = &contract.UserProfileResponse{
|
||||
@@ -761,7 +962,7 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
|
||||
if user.Profile != nil {
|
||||
mentionedUserResp.Profile = &contract.UserProfileResponse{
|
||||
UserID: user.Profile.UserID,
|
||||
@@ -771,7 +972,7 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
Locale: user.Profile.Locale,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
discussionResp.MentionedUsers = append(discussionResp.MentionedUsers, mentionedUserResp)
|
||||
}
|
||||
}
|
||||
@@ -802,11 +1003,11 @@ func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context,
|
||||
// Helper function to extract user IDs from mentions
|
||||
func extractMentionedUserIDs(mentions map[string]interface{}) []uuid.UUID {
|
||||
var userIDs []uuid.UUID
|
||||
|
||||
|
||||
if mentions == nil {
|
||||
return userIDs
|
||||
}
|
||||
|
||||
|
||||
if userIDsInterface, ok := mentions["user_ids"]; ok {
|
||||
if userIDsList, ok := userIDsInterface.([]interface{}); ok {
|
||||
for _, id := range userIDsList {
|
||||
@@ -818,7 +1019,7 @@ func extractMentionedUserIDs(mentions map[string]interface{}) []uuid.UUID {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return userIDs
|
||||
}
|
||||
|
||||
@@ -916,17 +1117,15 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi
|
||||
resp.Approvals = make([]contract.OutgoingLetterApprovalResponse, len(letter.Approvals))
|
||||
for i, approval := range letter.Approvals {
|
||||
approvalResp := contract.OutgoingLetterApprovalResponse{
|
||||
ID: approval.ID,
|
||||
ApproverID: approval.ApproverID,
|
||||
Status: string(approval.Status),
|
||||
Remarks: approval.Remarks,
|
||||
ActedAt: approval.ActedAt,
|
||||
CreatedAt: approval.CreatedAt,
|
||||
}
|
||||
|
||||
// Include step order if step is loaded
|
||||
if approval.Step != nil {
|
||||
approvalResp.StepOrder = approval.Step.StepOrder
|
||||
ID: approval.ID,
|
||||
StepOrder: approval.StepOrder,
|
||||
ParallelGroup: approval.ParallelGroup,
|
||||
IsRequired: approval.IsRequired,
|
||||
ApproverID: approval.ApproverID,
|
||||
Status: string(approval.Status),
|
||||
Remarks: approval.Remarks,
|
||||
ActedAt: approval.ActedAt,
|
||||
CreatedAt: approval.CreatedAt,
|
||||
}
|
||||
|
||||
resp.Approvals[i] = approvalResp
|
||||
@@ -945,3 +1144,220 @@ func transformDiscussionToResponse(discussion *entities.LetterOutgoingDiscussion
|
||||
UpdatedAt: discussion.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// GetApprovalTimeline generates a chronological timeline of all events for a letter
|
||||
func (s *LetterOutgoingServiceImpl) GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error) {
|
||||
// Get letter details
|
||||
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get approvals and discussions
|
||||
approvalDiscussions, err := s.GetApprovalDiscussions(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create timeline events
|
||||
timeline := make([]contract.TimelineEvent, 0)
|
||||
|
||||
// Add letter creation event
|
||||
timeline = append(timeline, contract.TimelineEvent{
|
||||
ID: letter.ID.String(),
|
||||
Type: "submission",
|
||||
Timestamp: letter.CreatedAt,
|
||||
Actor: nil, // Could add creator info here if needed
|
||||
Action: "created",
|
||||
Description: "Letter was created",
|
||||
Status: "created",
|
||||
})
|
||||
|
||||
// Add approval events
|
||||
for _, approval := range approvalDiscussions.Approvals {
|
||||
if approval.ActedAt != nil {
|
||||
eventType := "approval"
|
||||
action := "approved"
|
||||
status := "approved"
|
||||
|
||||
if approval.Status == "rejected" {
|
||||
eventType = "rejection"
|
||||
action = "rejected"
|
||||
status = "rejected"
|
||||
} else if approval.Status == "pending" {
|
||||
continue // Skip pending approvals as they haven't happened yet
|
||||
}
|
||||
|
||||
description := fmt.Sprintf("Step %d: %s by %s",
|
||||
approval.StepOrder,
|
||||
action,
|
||||
getApproverName(approval.Approver))
|
||||
|
||||
timeline = append(timeline, contract.TimelineEvent{
|
||||
ID: approval.ID.String(),
|
||||
Type: eventType,
|
||||
Timestamp: *approval.ActedAt,
|
||||
Actor: approval.Approver,
|
||||
Action: action,
|
||||
Description: description,
|
||||
Status: status,
|
||||
StepOrder: approval.StepOrder,
|
||||
Message: getLetterStringValue(approval.Remarks),
|
||||
Data: approval,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add discussion events
|
||||
for _, discussion := range approvalDiscussions.Discussions {
|
||||
timeline = append(timeline, contract.TimelineEvent{
|
||||
ID: discussion.ID.String(),
|
||||
Type: "discussion",
|
||||
Timestamp: discussion.CreatedAt,
|
||||
Actor: discussion.User,
|
||||
Action: "commented",
|
||||
Description: fmt.Sprintf("%s added a comment", getUserName(discussion.User)),
|
||||
Message: discussion.Message,
|
||||
Data: discussion,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort timeline by timestamp
|
||||
sort.Slice(timeline, func(i, j int) bool {
|
||||
return timeline[i].Timestamp.Before(timeline[j].Timestamp)
|
||||
})
|
||||
|
||||
// Calculate summary statistics
|
||||
summary := s.calculateTimelineSummary(letter, approvalDiscussions.Approvals, timeline)
|
||||
|
||||
return &contract.ApprovalTimelineResponse{
|
||||
LetterID: letter.ID,
|
||||
LetterNumber: letter.LetterNumber,
|
||||
Subject: letter.Subject,
|
||||
Status: string(letter.Status),
|
||||
CreatedAt: letter.CreatedAt,
|
||||
Timeline: timeline,
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) calculateTimelineSummary(
|
||||
letter *entities.LetterOutgoing,
|
||||
approvals []contract.EnhancedOutgoingLetterApprovalResponse,
|
||||
timeline []contract.TimelineEvent,
|
||||
) contract.TimelineSummary {
|
||||
totalSteps := 0
|
||||
completedSteps := 0
|
||||
pendingSteps := 0
|
||||
currentStep := 0
|
||||
|
||||
// Count unique step orders
|
||||
stepMap := make(map[int]string)
|
||||
for _, approval := range approvals {
|
||||
if _, exists := stepMap[approval.StepOrder]; !exists {
|
||||
stepMap[approval.StepOrder] = approval.Status
|
||||
totalSteps++
|
||||
}
|
||||
|
||||
switch approval.Status {
|
||||
case "approved":
|
||||
if stepMap[approval.StepOrder] == "approved" {
|
||||
completedSteps++
|
||||
currentStep = approval.StepOrder + 1
|
||||
}
|
||||
case "pending":
|
||||
pendingSteps++
|
||||
if currentStep == 0 {
|
||||
currentStep = approval.StepOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate duration
|
||||
totalDuration := ""
|
||||
averageStepTime := ""
|
||||
|
||||
if len(timeline) > 0 {
|
||||
lastEvent := timeline[len(timeline)-1]
|
||||
duration := lastEvent.Timestamp.Sub(letter.CreatedAt)
|
||||
totalDuration = formatDuration(duration)
|
||||
|
||||
if completedSteps > 0 {
|
||||
avgDuration := duration / time.Duration(completedSteps)
|
||||
averageStepTime = formatDuration(avgDuration)
|
||||
}
|
||||
}
|
||||
|
||||
status := "in_progress"
|
||||
if letter.Status == entities.LetterOutgoingStatusApproved {
|
||||
status = "completed"
|
||||
} else if letter.Status == "rejected" {
|
||||
status = "rejected"
|
||||
}
|
||||
|
||||
return contract.TimelineSummary{
|
||||
TotalSteps: totalSteps,
|
||||
CompletedSteps: completedSteps,
|
||||
PendingSteps: pendingSteps,
|
||||
CurrentStep: currentStep,
|
||||
TotalDuration: totalDuration,
|
||||
AverageStepTime: averageStepTime,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
func getApproverName(user *contract.UserResponse) string {
|
||||
if user == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
if user.Name != "" {
|
||||
return user.Name
|
||||
}
|
||||
return user.Email
|
||||
}
|
||||
|
||||
func getUserName(user *contract.UserResponse) string {
|
||||
if user == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
if user.Name != "" {
|
||||
return user.Name
|
||||
}
|
||||
return user.Email
|
||||
}
|
||||
|
||||
func getLetterStringValue(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
days := int(d.Hours() / 24)
|
||||
hours := int(d.Hours()) % 24
|
||||
minutes := int(d.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
||||
} else if hours > 0 {
|
||||
return fmt.Sprintf("%dh %dm", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%dm", minutes)
|
||||
}
|
||||
|
||||
func ApplyLetterFilterOverrides(ctx context.Context, filter *repository.ListOutgoingLettersFilter) {
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
if appCtx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
isSuperAdmin := false
|
||||
if appCtx.UserRole == "superadmin" || appCtx.UserRole == "admin" {
|
||||
isSuperAdmin = true
|
||||
}
|
||||
|
||||
if !isSuperAdmin && appCtx.UserID != uuid.Nil {
|
||||
filter.UserID = &appCtx.UserID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ func (s *OnlyOfficeServiceImpl) GetEditorConfig(ctx context.Context, req *contra
|
||||
Mode: req.Mode,
|
||||
User: &contract.OnlyOfficeUserConfig{
|
||||
ID: userCtx.UserID.String(),
|
||||
Name: fmt.Sprintf("User-%s", userCtx.UserID.String()[:8]),
|
||||
Name: userCtx.UserName,
|
||||
},
|
||||
Customization: &contract.OnlyOfficeCustomization{
|
||||
Autosave: true,
|
||||
|
||||
Reference in New Issue
Block a user