add dukcapil

This commit is contained in:
Aditya Siregar
2026-05-07 04:01:32 +07:00
parent 9a975d146e
commit bc64eb20ea
235 changed files with 1197 additions and 28187 deletions
-959
View File
@@ -1,959 +0,0 @@
package repository
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type AnalyticsRepository struct {
db *gorm.DB
}
func NewAnalyticsRepository(db *gorm.DB) *AnalyticsRepository {
return &AnalyticsRepository{db: db}
}
// GetLetterSummaryStats gets overall summary statistics using summary tables for better performance
func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDate, endDate time.Time, userID, departmentID *uuid.UUID) (map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
stats := make(map[string]interface{})
// Use summary tables for better performance when possible
if userID == nil && departmentID != nil {
// Use department_letter_summary for department-specific stats
query := db.Table("department_letter_summary").
Where("department_id = ?", *departmentID)
if !startDate.IsZero() {
query = query.Where("summary_date >= ?", startDate)
}
if !endDate.IsZero() {
query = query.Where("summary_date <= ?", endDate)
}
var result struct {
TotalIncoming int64 `gorm:"column:total_incoming"`
TotalOutgoing int64 `gorm:"column:total_outgoing"`
PendingOutgoing int64 `gorm:"column:pending_outgoing"`
ApprovedOutgoing int64 `gorm:"column:approved_outgoing"`
RejectedOutgoing int64 `gorm:"column:rejected_outgoing"`
AvgResponseHours float64 `gorm:"column:avg_response_hours"`
CompletionRate float64 `gorm:"column:completion_rate"`
}
query.Select(`
COALESCE(SUM(incoming_count), 0) as total_incoming,
COALESCE(SUM(outgoing_count), 0) as total_outgoing,
COALESCE(SUM(pending_outgoing), 0) as pending_outgoing,
COALESCE(SUM(approved_outgoing), 0) as approved_outgoing,
COALESCE(SUM(rejected_outgoing), 0) as rejected_outgoing,
COALESCE(AVG(avg_response_hours), 0) as avg_response_hours,
COALESCE(AVG(completion_rate), 0) as completion_rate
`).Scan(&result)
stats["total_incoming"] = result.TotalIncoming
stats["total_outgoing"] = result.TotalOutgoing
stats["total_pending"] = result.PendingOutgoing
stats["total_approved"] = result.ApprovedOutgoing
stats["total_rejected"] = result.RejectedOutgoing
stats["total_archived"] = int64(0) // Calculate separately if needed
stats["avg_processing_time"] = result.AvgResponseHours
stats["completion_rate"] = result.CompletionRate
} else if userID == nil && departmentID == nil {
// Use letter_summary for overall stats
query := db.Table("letter_summary")
if !startDate.IsZero() {
query = query.Where("summary_date >= ?", startDate)
}
if !endDate.IsZero() {
query = query.Where("summary_date <= ?", endDate)
}
var result struct {
TotalIncoming int64 `gorm:"column:total_incoming"`
TotalOutgoing int64 `gorm:"column:total_outgoing"`
TotalPending int64 `gorm:"column:total_pending"`
TotalApproved int64 `gorm:"column:total_approved"`
TotalRejected int64 `gorm:"column:total_rejected"`
TotalArchived int64 `gorm:"column:total_archived"`
TotalSent int64 `gorm:"column:total_sent"`
AvgProcessing float64 `gorm:"column:avg_processing"`
}
query.Select(`
COALESCE(SUM(CASE WHEN letter_type = 'incoming' THEN total_count ELSE 0 END), 0) as total_incoming,
COALESCE(SUM(CASE WHEN letter_type = 'outgoing' THEN total_count ELSE 0 END), 0) as total_outgoing,
COALESCE(SUM(pending_count), 0) as total_pending,
COALESCE(SUM(approved_count), 0) as total_approved,
COALESCE(SUM(rejected_count), 0) as total_rejected,
COALESCE(SUM(archived_count), 0) as total_archived,
COALESCE(SUM(sent_count), 0) as total_sent,
COALESCE(AVG(avg_processing_hours), 0) as avg_processing
`).Scan(&result)
stats["total_incoming"] = result.TotalIncoming
stats["total_outgoing"] = result.TotalOutgoing
stats["total_pending"] = result.TotalPending
stats["total_approved"] = result.TotalApproved
stats["total_rejected"] = result.TotalRejected
stats["total_archived"] = result.TotalArchived
stats["avg_processing_time"] = result.AvgProcessing
// Calculate completion rate
completionRate := float64(0)
if result.TotalOutgoing > 0 {
completedCount := result.TotalSent + result.TotalArchived
completionRate = float64(completedCount) / float64(result.TotalOutgoing) * 100
}
stats["completion_rate"] = completionRate
} else {
// Fall back to original implementation for user-specific queries
// Base query builders
incomingQuery := db.Table("letters_incoming").Where("letters_incoming.deleted_at IS NULL")
outgoingQuery := db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL")
// Apply date filters
if !startDate.IsZero() {
incomingQuery = incomingQuery.Where("letters_incoming.created_at >= ?", startDate)
outgoingQuery = outgoingQuery.Where("letters_outgoing.created_at >= ?", startDate)
}
if !endDate.IsZero() {
incomingQuery = incomingQuery.Where("letters_incoming.created_at <= ?", endDate)
outgoingQuery = outgoingQuery.Where("letters_outgoing.created_at <= ?", endDate)
}
// Apply user/department filters for outgoing letters
if userID != nil {
outgoingQuery = outgoingQuery.
Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id").
Where("letter_outgoing_recipients.user_id = ?", *userID)
incomingQuery = incomingQuery.
Joins("LEFT JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
Where("letter_incoming_recipients.recipient_user_id = ?", *userID)
}
fmt.Printf("[DEBUG] userId analitycs: %v\n", userID)
// Count incoming letters
var totalIncoming int64
incomingQuery.Distinct("letters_incoming.id").Count(&totalIncoming)
stats["total_incoming"] = totalIncoming
// Count outgoing letters
var totalOutgoing int64
outgoingQuery.Distinct("letters_outgoing.id").Count(&totalOutgoing)
stats["total_outgoing"] = totalOutgoing
// Count by status - need to clone query for each count
var pendingCount, approvedCount, rejectedCount, archivedCount int64
db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL").
Where("letters_outgoing.status = ?", "pending_approval").
Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id").
Where("letter_outgoing_recipients.user_id = ?", *userID).
Count(&pendingCount)
db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL").
Where("letters_outgoing.status = ?", "approved").
Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id").
Where("letter_outgoing_recipients.user_id = ?", *userID).
Count(&approvedCount)
db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL").
Where("letters_outgoing.status = ?", "rejected").
Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id").
Where("letter_outgoing_recipients.user_id = ?", *userID).
Count(&rejectedCount)
db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL").
Where("letters_outgoing.status = ?", "archived").
Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id").
Where("letter_outgoing_recipients.user_id = ?", *userID).
Count(&archivedCount)
stats["total_pending"] = pendingCount
stats["total_approved"] = approvedCount
stats["total_rejected"] = rejectedCount
stats["total_archived"] = archivedCount
// Calculate average processing time
var avgProcessingTime *float64
db.Table("letters_outgoing").
Select("AVG(EXTRACT(EPOCH FROM (letters_outgoing.updated_at - letters_outgoing.created_at))/3600) as avg_hours").
Where("letters_outgoing.status IN ('approved', 'sent', 'archived')").
Where("letters_outgoing.deleted_at IS NULL").
Scan(&avgProcessingTime)
if avgProcessingTime != nil {
stats["avg_processing_time"] = *avgProcessingTime
} else {
stats["avg_processing_time"] = float64(0)
}
// Calculate completion rate
var completedCount int64
db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL").
Where("letters_outgoing.status IN ('sent', 'archived')").
Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id").
Where("letter_outgoing_recipients.user_id = ?", *userID).
Count(&completedCount)
completionRate := float64(0)
if totalOutgoing > 0 {
completionRate = float64(completedCount) / float64(totalOutgoing) * 100
}
stats["completion_rate"] = completionRate
}
return stats, nil
}
// GetStatusDistribution gets letter distribution by status
func (r *AnalyticsRepository) GetStatusDistribution(ctx context.Context, startDate, endDate time.Time, userID *uuid.UUID) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
query := `
WITH combined_letters AS (
SELECT
status,
'incoming' as type,
COUNT(*) as count
FROM letters_incoming
WHERE deleted_at IS NULL
%s
GROUP BY status
UNION ALL
SELECT
lo.status,
'outgoing' as type,
COUNT(DISTINCT lo.id) as count
FROM letters_outgoing lo
%s
WHERE lo.deleted_at IS NULL
%s
GROUP BY lo.status
)
SELECT
status,
type,
count,
ROUND(count * 100.0 / SUM(count) OVER (PARTITION BY type), 2) as percentage
FROM combined_letters
ORDER BY type, count DESC
`
incomingDateFilter := ""
outgoingDateFilter := ""
if !startDate.IsZero() {
incomingDateFilter += fmt.Sprintf(" AND created_at >= '%s'", startDate.Format("2006-01-02"))
outgoingDateFilter += fmt.Sprintf(" AND lo.created_at >= '%s'", startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
incomingDateFilter += fmt.Sprintf(" AND created_at <= '%s'", endDate.Format("2006-01-02"))
outgoingDateFilter += fmt.Sprintf(" AND lo.created_at <= '%s'", endDate.Format("2006-01-02"))
}
joinClause := ""
userFilter := ""
if userID != nil {
joinClause = "LEFT JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id"
userFilter = fmt.Sprintf(" AND lor.user_id = '%s'", userID.String())
}
query = fmt.Sprintf(query, incomingDateFilter, joinClause, outgoingDateFilter+userFilter)
if err := db.Raw(query).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// GetPriorityDistribution gets letter distribution by priority
func (r *AnalyticsRepository) GetPriorityDistribution(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
query := `
SELECT
p.id as priority_id,
p.name as priority_name,
p.level,
COUNT(lo.id) as count,
ROUND(COUNT(lo.id) * 100.0 / SUM(COUNT(lo.id)) OVER (), 2) as percentage,
AVG(EXTRACT(EPOCH FROM (lo.updated_at - lo.created_at))/3600) as avg_response_time
FROM priorities p
LEFT JOIN letters_outgoing lo ON lo.priority_id = p.id AND lo.deleted_at IS NULL
WHERE 1=1
%s
GROUP BY p.id, p.name, p.level
ORDER BY p.level ASC
`
dateFilter := ""
if !startDate.IsZero() {
dateFilter += fmt.Sprintf(" AND lo.created_at >= '%s'", startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
dateFilter += fmt.Sprintf(" AND lo.created_at <= '%s'", endDate.Format("2006-01-02"))
}
query = fmt.Sprintf(query, dateFilter)
if err := db.Raw(query).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// GetDepartmentStats gets statistics per department using summary tables
func (r *AnalyticsRepository) GetDepartmentStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
// Format tanggal untuk logging
log.Printf("GetDepartmentStats called with startDate: %v, endDate: %v", startDate, endDate)
// Try summary table first
query := `
SELECT
d.id as department_id,
d.name as department_name,
d.code as department_code,
COALESCE(SUM(dls.incoming_count), 0) as incoming_count,
COALESCE(SUM(dls.outgoing_count), 0) as outgoing_count,
COALESCE(SUM(dls.pending_outgoing), 0) as pending_count,
COALESCE(AVG(dls.avg_response_hours), 0) as avg_response_time,
COALESCE(AVG(dls.completion_rate), 0) as completion_rate
FROM departments d
LEFT JOIN department_letter_summary dls ON dls.department_id = d.id`
var conditions []string
var args []interface{}
if !startDate.IsZero() {
conditions = append(conditions, "dls.summary_date >= ?")
args = append(args, startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
conditions = append(conditions, "dls.summary_date <= ?")
args = append(args, endDate.Format("2006-01-02"))
}
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
query += `
GROUP BY d.id, d.name, d.code
ORDER BY (COALESCE(SUM(dls.incoming_count), 0) + COALESCE(SUM(dls.outgoing_count), 0)) DESC`
log.Printf("Summary query: %s, args: %v", query, args)
if err := db.Raw(query, args...).Scan(&results).Error; err != nil {
return nil, err
}
// Check if summary table has data for this period
var summaryCount int64
checkQuery := "SELECT COUNT(*) FROM department_letter_summary WHERE 1=1"
checkArgs := []interface{}{}
if !startDate.IsZero() {
checkQuery += " AND summary_date >= ?"
checkArgs = append(checkArgs, startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
checkQuery += " AND summary_date <= ?"
checkArgs = append(checkArgs, endDate.Format("2006-01-02"))
}
db.Raw(checkQuery, checkArgs...).Scan(&summaryCount)
log.Printf("Summary count: %d", summaryCount)
// If no summary data exists for this period, fall back to direct query
if summaryCount == 0 {
log.Println("Using fallback query (no summary data)")
// Use CTE for better performance
fallbackQuery := `
WITH filtered_incoming AS (
SELECT
li.id,
li.created_at,
li.updated_at,
lir.recipient_department_id
FROM letters_incoming li
INNER JOIN letter_incoming_recipients lir ON lir.letter_id = li.id
WHERE li.deleted_at IS NULL`
var fallbackArgs []interface{}
if !startDate.IsZero() {
fallbackQuery += " AND li.created_at >= ?"
fallbackArgs = append(fallbackArgs, startDate)
}
if !endDate.IsZero() {
fallbackQuery += " AND li.created_at <= ?"
fallbackArgs = append(fallbackArgs, endDate)
}
fallbackQuery += `
),
filtered_outgoing AS (
SELECT
lo.id,
lo.status,
lo.created_at,
lo.updated_at,
lor.department_id
FROM letters_outgoing lo
INNER JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
WHERE lo.deleted_at IS NULL`
if !startDate.IsZero() {
fallbackQuery += " AND lo.created_at >= ?"
fallbackArgs = append(fallbackArgs, startDate)
}
if !endDate.IsZero() {
fallbackQuery += " AND lo.created_at <= ?"
fallbackArgs = append(fallbackArgs, endDate)
}
fallbackQuery += `
)
SELECT
d.id as department_id,
d.name as department_name,
d.code as department_code,
COUNT(DISTINCT fi.id) as incoming_count,
COUNT(DISTINCT fo.id) as outgoing_count,
COUNT(DISTINCT CASE WHEN fo.status = 'pending_approval' THEN fo.id END) as pending_count,
COALESCE(AVG(CASE
WHEN fo.status IN ('approved', 'sent', 'archived')
THEN EXTRACT(EPOCH FROM (fo.updated_at - fo.created_at))/3600
END), 0) as avg_response_time,
CASE
WHEN COUNT(DISTINCT fo.id) > 0
THEN ROUND(COUNT(DISTINCT CASE WHEN fo.status IN ('sent', 'archived') THEN fo.id END) * 100.0 / COUNT(DISTINCT fo.id), 2)
ELSE 0
END as completion_rate
FROM departments d
LEFT JOIN filtered_incoming fi ON fi.recipient_department_id = d.id
LEFT JOIN filtered_outgoing fo ON fo.department_id = d.id
GROUP BY d.id, d.name, d.code
ORDER BY (COUNT(DISTINCT fi.id) + COUNT(DISTINCT fo.id)) DESC`
log.Printf("Fallback query: %s", fallbackQuery)
log.Printf("Fallback args: %v", fallbackArgs)
if err := db.Raw(fallbackQuery, fallbackArgs...).Scan(&results).Error; err != nil {
return nil, err
}
}
return results, nil
}
// GetMonthlyTrend gets monthly trend data using summary tables for better performance
func (r *AnalyticsRepository) GetMonthlyTrend(ctx context.Context, months int) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
// Use summary table for better performance
query := `
WITH monthly_aggregated AS (
SELECT
TO_CHAR(summary_date, 'Month') as month,
EXTRACT(YEAR FROM summary_date) as year,
EXTRACT(MONTH FROM summary_date) as month_num,
SUM(CASE WHEN letter_type = 'incoming' THEN total_count ELSE 0 END) as incoming_count,
SUM(CASE WHEN letter_type = 'outgoing' THEN total_count ELSE 0 END) as outgoing_count,
SUM(total_count) as total_count
FROM letter_summary
WHERE summary_date >= NOW() - INTERVAL '%d months'
GROUP BY TO_CHAR(summary_date, 'Month'),
EXTRACT(YEAR FROM summary_date),
EXTRACT(MONTH FROM summary_date)
)
SELECT
month,
year,
incoming_count,
outgoing_count,
total_count,
LAG(total_count) OVER (ORDER BY year, month_num) as prev_total
FROM monthly_aggregated
ORDER BY year DESC, month_num DESC
LIMIT %d
`
query = fmt.Sprintf(query, months, months)
if err := db.Raw(query).Scan(&results).Error; err != nil {
// If summary table is empty, fall back to direct query
if len(results) == 0 {
fallbackQuery := `
WITH monthly_data AS (
SELECT
TO_CHAR(date_trunc('month', created_at), 'Month') as month,
EXTRACT(YEAR FROM created_at) as year,
EXTRACT(MONTH FROM created_at) as month_num,
COUNT(*) as incoming_count,
0 as outgoing_count
FROM letters_incoming
WHERE deleted_at IS NULL
AND created_at >= NOW() - INTERVAL '%d months'
GROUP BY date_trunc('month', created_at), EXTRACT(YEAR FROM created_at), EXTRACT(MONTH FROM created_at)
UNION ALL
SELECT
TO_CHAR(date_trunc('month', created_at), 'Month') as month,
EXTRACT(YEAR FROM created_at) as year,
EXTRACT(MONTH FROM created_at) as month_num,
0 as incoming_count,
COUNT(*) as outgoing_count
FROM letters_outgoing
WHERE deleted_at IS NULL
AND created_at >= NOW() - INTERVAL '%d months'
GROUP BY date_trunc('month', created_at), EXTRACT(YEAR FROM created_at), EXTRACT(MONTH FROM created_at)
)
SELECT
month,
year,
SUM(incoming_count) as incoming_count,
SUM(outgoing_count) as outgoing_count,
SUM(incoming_count + outgoing_count) as total_count,
LAG(SUM(incoming_count + outgoing_count)) OVER (ORDER BY year, month_num) as prev_total
FROM monthly_data
GROUP BY month, year, month_num
ORDER BY year DESC, month_num DESC
LIMIT %d
`
fallbackQuery = fmt.Sprintf(fallbackQuery, months, months, months)
if err := db.Raw(fallbackQuery).Scan(&results).Error; err != nil {
return nil, err
}
}
}
// Calculate growth rate
for i := range results {
if results[i]["prev_total"] != nil {
prevVal, ok := results[i]["prev_total"].(float64)
if ok && prevVal > 0 {
current := getFloat64FromInterface(results[i]["total_count"])
results[i]["growth_rate"] = ((current - prevVal) / prevVal) * 100
} else {
results[i]["growth_rate"] = float64(0)
}
} else {
results[i]["growth_rate"] = float64(0)
}
delete(results[i], "prev_total")
}
return results, nil
}
func (r *AnalyticsRepository) GetMonthlyTrendByUserID(ctx context.Context, userID *uuid.UUID, months int) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
// Direct query (since we need to filter by user)
fallbackQuery := `
WITH monthly_data AS (
SELECT
TO_CHAR(date_trunc('month', li.created_at), 'Month') as month,
EXTRACT(YEAR FROM li.created_at) as year,
EXTRACT(MONTH FROM li.created_at) as month_num,
COUNT(DISTINCT li.id) as incoming_count,
0 as outgoing_count
FROM letters_incoming li
INNER JOIN letter_incoming_recipients lir ON lir.letter_id = li.id
WHERE li.deleted_at IS NULL
AND lir.recipient_user_id = ?
AND li.created_at >= NOW() - INTERVAL '%d months'
GROUP BY date_trunc('month', li.created_at), EXTRACT(YEAR FROM li.created_at), EXTRACT(MONTH FROM li.created_at)
UNION ALL
SELECT
TO_CHAR(date_trunc('month', lo.created_at), 'Month') as month,
EXTRACT(YEAR FROM lo.created_at) as year,
EXTRACT(MONTH FROM lo.created_at) as month_num,
0 as incoming_count,
COUNT(DISTINCT lo.id) as outgoing_count
FROM letters_outgoing lo
INNER JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
WHERE lo.deleted_at IS NULL
AND lor.user_id = ?
AND lo.created_at >= NOW() - INTERVAL '%d months'
GROUP BY date_trunc('month', lo.created_at), EXTRACT(YEAR FROM lo.created_at), EXTRACT(MONTH FROM lo.created_at)
)
SELECT
month,
year,
SUM(incoming_count) as incoming_count,
SUM(outgoing_count) as outgoing_count,
SUM(incoming_count + outgoing_count) as total_count,
LAG(SUM(incoming_count + outgoing_count)) OVER (ORDER BY year, month_num) as prev_total
FROM monthly_data
GROUP BY month, year, month_num
ORDER BY year DESC, month_num DESC
LIMIT %d
`
fallbackQuery = fmt.Sprintf(fallbackQuery, months, months, months)
if err := db.Raw(fallbackQuery, userID, userID).Scan(&results).Error; err != nil {
return nil, err
}
// Calculate growth rate
for i := range results {
if results[i]["prev_total"] != nil {
prevVal, ok := results[i]["prev_total"].(float64)
if ok && prevVal > 0 {
current := getFloat64FromInterface(results[i]["total_count"])
results[i]["growth_rate"] = ((current - prevVal) / prevVal) * 100
} else {
results[i]["growth_rate"] = float64(0)
}
} else {
results[i]["growth_rate"] = float64(0)
}
delete(results[i], "prev_total")
}
return results, nil
}
// Helper function to safely convert interface{} to float64
func getFloat64FromInterface(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
}
}
// GetTopSenders gets top letter senders
func (r *AnalyticsRepository) GetTopSenders(ctx context.Context, limit int, startDate, endDate time.Time) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
query := `
SELECT
u.id as user_id,
u.name as user_name,
u.email as user_email,
COALESCE(d.name, 'No Department') as department,
COUNT(lo.id) as letter_count,
AVG(EXTRACT(EPOCH FROM (lo.updated_at - lo.created_at))/3600) as avg_response_time
FROM users u
LEFT JOIN letters_outgoing lo ON lo.created_by = u.id
LEFT JOIN user_department ud ON ud.user_id = u.id
LEFT JOIN departments d ON d.id = ud.department_id
WHERE lo.deleted_at IS NULL
%s
GROUP BY u.id, u.name, u.email, d.name
ORDER BY letter_count DESC
LIMIT %d
`
dateFilter := ""
if !startDate.IsZero() {
dateFilter += fmt.Sprintf(" AND lo.created_at >= '%s'", startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
dateFilter += fmt.Sprintf(" AND lo.created_at <= '%s'", endDate.Format("2006-01-02"))
}
query = fmt.Sprintf(query, dateFilter, limit)
if err := db.Raw(query).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// GetInstitutionStats gets statistics per institution using summary tables
func (r *AnalyticsRepository) GetInstitutionStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
// Use summary table for better performance
query := `
SELECT
i.id as institution_id,
i.name as institution_name,
i.type as institution_type,
COALESCE(SUM(ils.incoming_sent), 0) as incoming_count,
COALESCE(SUM(ils.outgoing_received), 0) as outgoing_count,
COALESCE(SUM(ils.total_correspondence), 0) as total_count,
MAX(ils.last_activity_at) as last_activity
FROM institutions i
LEFT JOIN institution_letter_summary ils ON ils.institution_id = i.id
WHERE 1=1
%s
GROUP BY i.id, i.name, i.type
HAVING COALESCE(SUM(ils.total_correspondence), 0) > 0
ORDER BY total_count DESC
`
dateFilter := ""
if !startDate.IsZero() {
dateFilter += fmt.Sprintf(" AND ils.summary_date >= '%s'", startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
dateFilter += fmt.Sprintf(" AND ils.summary_date <= '%s'", endDate.Format("2006-01-02"))
}
query = fmt.Sprintf(query, dateFilter)
if err := db.Raw(query).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// GetApprovalMetrics gets approval-related metrics using summary tables
func (r *AnalyticsRepository) GetApprovalMetrics(ctx context.Context, startDate, endDate time.Time) (map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
metrics := make(map[string]interface{})
// Use summary table for better performance
query := db.Table("approval_sla_summary")
if !startDate.IsZero() {
query = query.Where("summary_date >= ?", startDate)
}
if !endDate.IsZero() {
query = query.Where("summary_date <= ?", endDate)
}
var result struct {
TotalApprovals int64 `gorm:"column:total_approvals"`
ApprovedCount int64 `gorm:"column:approved_count"`
RejectedCount int64 `gorm:"column:rejected_count"`
PendingCount int64 `gorm:"column:pending_count"`
AvgApprovalHours float64 `gorm:"column:avg_approval_hours"`
AvgApprovalSteps float64 `gorm:"column:avg_approval_steps"`
SLACompliance float64 `gorm:"column:sla_compliance"`
WithinSLA int64 `gorm:"column:within_sla"`
ExceededSLA int64 `gorm:"column:exceeded_sla"`
}
query.Select(`
COALESCE(SUM(total_approvals), 0) as total_approvals,
COALESCE(SUM(approved_count), 0) as approved_count,
COALESCE(SUM(rejected_count), 0) as rejected_count,
COALESCE(SUM(pending_count), 0) as pending_count,
COALESCE(AVG(avg_approval_hours), 0) as avg_approval_hours,
COALESCE(AVG(avg_approval_steps), 0) as avg_approval_steps,
COALESCE(AVG(sla_compliance_rate), 0) as sla_compliance,
COALESCE(SUM(within_sla_count), 0) as within_sla,
COALESCE(SUM(exceeded_sla_count), 0) as exceeded_sla
`).Scan(&result)
metrics["total_submitted"] = result.TotalApprovals
metrics["total_approved"] = result.ApprovedCount
metrics["total_rejected"] = result.RejectedCount
metrics["total_pending"] = result.PendingCount
metrics["avg_approval_time"] = result.AvgApprovalHours
metrics["avg_approval_steps"] = result.AvgApprovalSteps
metrics["sla_compliance_rate"] = result.SLACompliance
metrics["within_sla_count"] = result.WithinSLA
metrics["exceeded_sla_count"] = result.ExceededSLA
// Calculate rates
if result.TotalApprovals > 0 {
metrics["approval_rate"] = float64(result.ApprovedCount) / float64(result.TotalApprovals) * 100
metrics["rejection_rate"] = float64(result.RejectedCount) / float64(result.TotalApprovals) * 100
} else {
metrics["approval_rate"] = float64(0)
metrics["rejection_rate"] = float64(0)
}
return metrics, nil
}
// GetDailyActivity gets daily activity data
func (r *AnalyticsRepository) GetDailyActivity(ctx context.Context, days int) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
query := `
WITH daily_data AS (
SELECT
DATE(created_at) as date,
TO_CHAR(created_at, 'Day') as day_of_week,
COUNT(CASE WHEN type = 'incoming' THEN 1 END) as incoming_count,
COUNT(CASE WHEN type = 'outgoing' THEN 1 END) as outgoing_count,
0 as approved_count,
0 as rejected_count
FROM (
SELECT created_at, 'incoming' as type FROM letters_incoming WHERE deleted_at IS NULL
UNION ALL
SELECT created_at, 'outgoing' as type FROM letters_outgoing WHERE deleted_at IS NULL
) combined
WHERE created_at >= CURRENT_DATE - INTERVAL '%d days'
GROUP BY DATE(created_at), TO_CHAR(created_at, 'Day')
),
approval_data AS (
SELECT
DATE(acted_at) as date,
COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_count,
COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_count
FROM letter_outgoing_approvals
WHERE acted_at IS NOT NULL
AND acted_at >= CURRENT_DATE - INTERVAL '%d days'
GROUP BY DATE(acted_at)
)
SELECT
d.date,
d.day_of_week,
d.incoming_count,
d.outgoing_count,
COALESCE(a.approved_count, 0) as approved_count,
COALESCE(a.rejected_count, 0) as rejected_count
FROM daily_data d
LEFT JOIN approval_data a ON a.date = d.date
ORDER BY d.date DESC
LIMIT %d
`
query = fmt.Sprintf(query, days, days, days)
if err := db.Raw(query).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
func (r *AnalyticsRepository) GetDailyActivityByUserID(ctx context.Context, userID *uuid.UUID, days int) ([]map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
var results []map[string]interface{}
query := `
WITH daily_data AS (
SELECT
DATE(created_at) as date,
TO_CHAR(created_at, 'Day') as day_of_week,
COUNT(DISTINCT CASE WHEN type = 'incoming' THEN letter_id END) as incoming_count,
COUNT(DISTINCT CASE WHEN type = 'outgoing' THEN letter_id END) as outgoing_count,
0 as approved_count,
0 as rejected_count
FROM (
SELECT li.id as letter_id, li.created_at, 'incoming' as type
FROM letters_incoming li
INNER JOIN letter_incoming_recipients lir ON lir.letter_id = li.id
WHERE li.deleted_at IS NULL AND lir.recipient_user_id = ?
UNION ALL
SELECT lo.id as letter_id, lo.created_at, 'outgoing' as type
FROM letters_outgoing lo
INNER JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
WHERE lo.deleted_at IS NULL AND lor.user_id = ?
) combined
WHERE created_at >= CURRENT_DATE - INTERVAL '%d days'
GROUP BY DATE(created_at), TO_CHAR(created_at, 'Day')
),
approval_data AS (
SELECT
DATE(acted_at) as date,
COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_count,
COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_count
FROM letter_outgoing_approvals
WHERE acted_at IS NOT NULL
AND acted_at >= CURRENT_DATE - INTERVAL '%d days'
AND approver_id = ?
GROUP BY DATE(acted_at)
)
SELECT
d.date,
d.day_of_week,
d.incoming_count,
d.outgoing_count,
COALESCE(a.approved_count, 0) as approved_count,
COALESCE(a.rejected_count, 0) as rejected_count
FROM daily_data d
LEFT JOIN approval_data a ON a.date = d.date
ORDER BY d.date DESC
LIMIT %d
`
query = fmt.Sprintf(query, days, days, days)
if err := db.Raw(query, userID, userID, userID).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// GetResponseTimeStats gets response time statistics
func (r *AnalyticsRepository) GetResponseTimeStats(ctx context.Context, startDate, endDate time.Time) (map[string]interface{}, error) {
db := DBFromContext(ctx, r.db)
stats := make(map[string]interface{})
query := `
WITH response_times AS (
SELECT
EXTRACT(EPOCH FROM (updated_at - created_at))/3600 as response_time_hours
FROM letters_outgoing
WHERE status IN ('approved', 'sent', 'archived')
AND deleted_at IS NULL
%s
)
SELECT
MIN(response_time_hours) as min_response_time,
MAX(response_time_hours) as max_response_time,
AVG(response_time_hours) as avg_response_time,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY response_time_hours) as median_response_time,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_hours) as p95_response_time,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY response_time_hours) as p99_response_time
FROM response_times
`
dateFilter := ""
if !startDate.IsZero() {
dateFilter += fmt.Sprintf(" AND created_at >= '%s'", startDate.Format("2006-01-02"))
}
if !endDate.IsZero() {
dateFilter += fmt.Sprintf(" AND created_at <= '%s'", endDate.Format("2006-01-02"))
}
query = fmt.Sprintf(query, dateFilter)
if err := db.Raw(query).Scan(&stats).Error; err != nil {
return nil, err
}
return stats, nil
}
@@ -1,62 +0,0 @@
package repository
import (
"context"
"encoding/json"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type AppSettingRepository struct{ db *gorm.DB }
func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository {
return &AppSettingRepository{
db: db,
}
}
func (r *AppSettingRepository) Get(ctx context.Context, key string) (*entities.AppSetting, error) {
db := DBFromContext(ctx, r.db)
var e entities.AppSetting
if err := db.WithContext(ctx).First(&e, "key = ?", key).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *AppSettingRepository) Upsert(ctx context.Context, key string, value entities.JSONB) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Exec("INSERT INTO app_settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP", key, value).Error
}
func (r *AppSettingRepository) GetDepartmentRecipients(ctx context.Context) ([]uuid.UUID, error) {
setting, err := r.Get(ctx, contract.SettingIncomingLetterDepartmentRecipients)
if err != nil {
if err == gorm.ErrRecordNotFound {
return []uuid.UUID{}, nil
}
return nil, err
}
jsonBytes, err := json.Marshal(setting.Value)
if err != nil {
return []uuid.UUID{}, nil
}
// Try to unmarshal as the structured format first
var recipientSetting entities.DepartmentRecipientsSetting
if err := json.Unmarshal(jsonBytes, &recipientSetting); err == nil {
return recipientSetting.DepartmentIDs, nil
}
// If that fails, try to unmarshal as a direct array of UUIDs
var departmentIDs []uuid.UUID
if err := json.Unmarshal(jsonBytes, &departmentIDs); err == nil {
return departmentIDs, nil
}
// If both fail, return empty array
return []uuid.UUID{}, nil
}
@@ -1,271 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ApprovalFlowRepository struct{ db *gorm.DB }
func NewApprovalFlowRepository(db *gorm.DB) *ApprovalFlowRepository {
return &ApprovalFlowRepository{db: db}
}
func (r *ApprovalFlowRepository) Create(ctx context.Context, e *entities.ApprovalFlow) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *ApprovalFlowRepository) Get(ctx context.Context, id uuid.UUID) (*entities.ApprovalFlow, error) {
db := DBFromContext(ctx, r.db)
var e entities.ApprovalFlow
if err := db.WithContext(ctx).
Preload("Department").
Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC, parallel_group ASC")
}).
Preload("Steps.ApproverRole").
Preload("Steps.ApproverUser").
Where("id = ?", id).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *ApprovalFlowRepository) GetByDepartment(ctx context.Context, departmentID uuid.UUID) (*entities.ApprovalFlow, error) {
db := DBFromContext(ctx, r.db)
var e entities.ApprovalFlow
if err := db.WithContext(ctx).
Preload("Department").
Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC, parallel_group ASC")
}).
Preload("Steps.ApproverRole").
Preload("Steps.ApproverUser").
Where("department_id = ? AND is_active = true", departmentID).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *ApprovalFlowRepository) Update(ctx context.Context, e *entities.ApprovalFlow) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.ApprovalFlow{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *ApprovalFlowRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.ApprovalFlow{}).Error
}
type ListApprovalFlowsFilter struct {
DepartmentID *uuid.UUID
Search *string
IsActive *bool
}
func (r *ApprovalFlowRepository) List(ctx context.Context, filter ListApprovalFlowsFilter, limit, offset int) ([]entities.ApprovalFlow, int64, error) {
var list []entities.ApprovalFlow
var total int64
// Build base query for counting
countQuery := r.db.WithContext(ctx).Model(&entities.ApprovalFlow{})
if filter.DepartmentID != nil {
countQuery = countQuery.Where("department_id = ?", *filter.DepartmentID)
}
if filter.IsActive != nil {
countQuery = countQuery.Where("is_active = ?", *filter.IsActive)
}
if filter.Search != nil && *filter.Search != "" {
like := "%" + *filter.Search + "%"
countQuery = countQuery.Where("name ILIKE ? OR description ILIKE ?", like, like)
}
// Get total count
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
// Build query for fetching data - BUAT QUERY BARU DARI AWAL
dataQuery := r.db.WithContext(ctx).Model(&entities.ApprovalFlow{})
if filter.DepartmentID != nil {
dataQuery = dataQuery.Where("department_id = ?", *filter.DepartmentID)
}
if filter.IsActive != nil {
dataQuery = dataQuery.Where("is_active = ?", *filter.IsActive)
}
if filter.Search != nil && *filter.Search != "" {
like := "%" + *filter.Search + "%"
dataQuery = dataQuery.Where("name ILIKE ? OR description ILIKE ?", like, like)
}
// Fetch data with pagination and preloads
if err := dataQuery.
Order("created_at DESC").
Limit(limit).
Offset(offset).
Preload("Department").
Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC, parallel_group ASC")
}).
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
type ApprovalFlowStepRepository struct{ db *gorm.DB }
func NewApprovalFlowStepRepository(db *gorm.DB) *ApprovalFlowStepRepository {
return &ApprovalFlowStepRepository{db: db}
}
func (r *ApprovalFlowStepRepository) Create(ctx context.Context, e *entities.ApprovalFlowStep) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *ApprovalFlowStepRepository) CreateBulk(ctx context.Context, list []entities.ApprovalFlowStep) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
func (r *ApprovalFlowStepRepository) Update(ctx context.Context, e *entities.ApprovalFlowStep) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.ApprovalFlowStep{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *ApprovalFlowStepRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.ApprovalFlowStep{}).Error
}
func (r *ApprovalFlowStepRepository) DeleteByFlow(ctx context.Context, flowID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("flow_id = ?", flowID).Delete(&entities.ApprovalFlowStep{}).Error
}
func (r *ApprovalFlowStepRepository) ListByFlow(ctx context.Context, flowID uuid.UUID) ([]entities.ApprovalFlowStep, error) {
db := DBFromContext(ctx, r.db)
var list []entities.ApprovalFlowStep
if err := db.WithContext(ctx).
Preload("ApproverRole").
Preload("ApproverUser").
Where("flow_id = ?", flowID).
Order("step_order ASC, parallel_group ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterOutgoingApprovalRepository struct{ db *gorm.DB }
func NewLetterOutgoingApprovalRepository(db *gorm.DB) *LetterOutgoingApprovalRepository {
return &LetterOutgoingApprovalRepository{db: db}
}
func (r *LetterOutgoingApprovalRepository) Create(ctx context.Context, e *entities.LetterOutgoingApproval) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingApprovalRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingApproval) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterOutgoingApprovalRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingApproval, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterOutgoingApproval
if err := db.WithContext(ctx).
Preload("Letter").
Preload("Step").
Preload("Approver").
Where("id = ?", id).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterOutgoingApprovalRepository) GetByLetterAndStep(ctx context.Context, letterID, stepID uuid.UUID) (*entities.LetterOutgoingApproval, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterOutgoingApproval
if err := db.WithContext(ctx).
Where("letter_id = ? AND step_id = ?", letterID, stepID).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterOutgoingApprovalRepository) Update(ctx context.Context, e *entities.LetterOutgoingApproval) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterOutgoingApproval{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *LetterOutgoingApprovalRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingApproval
if err := db.WithContext(ctx).
Preload("Step.ApproverRole").
Preload("Step.ApproverUser").
Preload("Approver").
Where("letter_id = ?", letterID).
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingApprovalRepository) ListByLetterAndLasRevisionNumber(ctx context.Context, letterID uuid.UUID, revisionNumber int) ([]entities.LetterOutgoingApproval, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingApproval
if err := db.WithContext(ctx).
Preload("Step.ApproverRole").
Preload("Step.ApproverUser").
Preload("Approver").
Where("letter_id = ? AND revision_number = ?", letterID, revisionNumber).
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingApprovalRepository) GetPendingApprovals(ctx context.Context, userID uuid.UUID) ([]entities.LetterOutgoingApproval, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingApproval
if err := db.WithContext(ctx).
Preload("Letter").
Preload("Step").
Joins("JOIN approval_flow_steps afs ON afs.id = letter_outgoing_approvals.step_id").
Where("letter_outgoing_approvals.status = ? AND (afs.approver_user_id = ? OR afs.approver_role_id IN (SELECT role_id FROM user_roles WHERE user_id = ?))",
entities.ApprovalStatusPending, userID, userID).
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
@@ -1,212 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type DispositionRouteRepository struct{ db *gorm.DB }
func NewDispositionRouteRepository(db *gorm.DB) *DispositionRouteRepository {
return &DispositionRouteRepository{db: db}
}
func (r *DispositionRouteRepository) Create(ctx context.Context, e *entities.DispositionRoute) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
// Upsert creates or updates a disposition route based on from_department_id and to_department_id
func (r *DispositionRouteRepository) Upsert(ctx context.Context, e *entities.DispositionRoute) error {
db := DBFromContext(ctx, r.db)
// Check if route exists
var existing entities.DispositionRoute
err := db.WithContext(ctx).
Where("from_department_id = ? AND to_department_id = ?", e.FromDepartmentID, e.ToDepartmentID).
First(&existing).Error
if err == gorm.ErrRecordNotFound {
// Create new route
return db.WithContext(ctx).Create(e).Error
} else if err != nil {
return err
}
// Update existing route
e.ID = existing.ID
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).
Where("id = ?", existing.ID).
Updates(e).Error
}
// BulkUpsert performs bulk create or update for multiple routes
func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID uuid.UUID, toDeptIDs []uuid.UUID, isActive bool, allowedActions entities.JSONB) (created int, updated int, err error) {
db := DBFromContext(ctx, r.db)
// Start transaction
tx := db.WithContext(ctx).Begin()
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Get existing routes for this from_department_id
var existingRoutes []entities.DispositionRoute
if err = tx.Where("from_department_id = ?", fromDeptID).Find(&existingRoutes).Error; err != nil {
return 0, 0, err
}
// Create map of existing routes
existingMap := make(map[uuid.UUID]entities.DispositionRoute)
for _, route := range existingRoutes {
existingMap[route.ToDepartmentID] = route
}
// Process each to_department_id
for _, toDeptID := range toDeptIDs {
route := entities.DispositionRoute{
FromDepartmentID: fromDeptID,
ToDepartmentID: toDeptID,
IsActive: isActive,
AllowedActions: allowedActions,
}
if existing, exists := existingMap[toDeptID]; exists {
// Update existing route
route.ID = existing.ID
if err = tx.Model(&entities.DispositionRoute{}).
Where("id = ?", existing.ID).
Updates(&route).Error; err != nil {
return created, updated, err
}
updated++
// Remove from map to track which routes to delete
delete(existingMap, toDeptID)
} else {
// Create new route
if err = tx.Create(&route).Error; err != nil {
return created, updated, err
}
created++
}
}
// Optionally deactivate routes that are no longer in the list
// (routes that exist in DB but not in the new list)
for _, oldRoute := range existingMap {
if err = tx.Model(&entities.DispositionRoute{}).
Where("id = ?", oldRoute.ID).
Update("is_active", false).Error; err != nil {
return created, updated, err
}
}
// Commit transaction
if err = tx.Commit().Error; err != nil {
return 0, 0, err
}
return created, updated, nil
}
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var e entities.DispositionRoute
if err := db.WithContext(ctx).
Preload("FromDepartment").
Preload("ToDepartment").
First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionRoute
if err := db.WithContext(ctx).Where("from_department_id = ? and is_active=true", fromDept).
Preload("FromDepartment").
Preload("ToDepartment").
Order("to_department_id").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *DispositionRouteRepository) IsEligibleForDisposition(ctx context.Context, fromDept uuid.UUID) (bool, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionRoute
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).Find(&list).Error; err != nil {
return false, err
}
return len(list) > 0, nil
}
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
}
// ListAllGrouped returns all disposition routes grouped by from_department_id
func (r *DispositionRouteRepository) ListAllGrouped(ctx context.Context) (map[uuid.UUID][]uuid.UUID, error) {
db := DBFromContext(ctx, r.db)
var routes []entities.DispositionRoute
if err := db.WithContext(ctx).
Where("is_active = ?", true).
Order("from_department_id, to_department_id").
Find(&routes).Error; err != nil {
return nil, err
}
// Group by from_department_id
grouped := make(map[uuid.UUID][]uuid.UUID)
for _, route := range routes {
grouped[route.FromDepartmentID] = append(grouped[route.FromDepartmentID], route.ToDepartmentID)
}
return grouped, nil
}
// ListAllGroupedWithDepartments returns all disposition routes grouped by from_department_id with department details
func (r *DispositionRouteRepository) ListAllGroupedWithDepartments(ctx context.Context) ([]entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var routes []entities.DispositionRoute
if err := db.WithContext(ctx).
Preload("FromDepartment").
Preload("ToDepartment").
Where("is_active = ?", true).
Order("from_department_id, to_department_id").
Find(&routes).Error; err != nil {
return nil, err
}
return routes, nil
}
// ListAll returns all disposition routes with department details
func (r *DispositionRouteRepository) ListAll(ctx context.Context) ([]entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var routes []entities.DispositionRoute
if err := db.WithContext(ctx).
Preload("FromDepartment").
Preload("ToDepartment").
Where("is_active = ?", true).
Order("from_department_id, to_department_id").
Find(&routes).Error; err != nil {
return nil, err
}
return routes, nil
}
-218
View File
@@ -1,218 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
// DocumentSessionRepository handles document session operations
type DocumentSessionRepository struct {
db *gorm.DB
}
func NewDocumentSessionRepository(db *gorm.DB) *DocumentSessionRepository {
return &DocumentSessionRepository{db: db}
}
func (r *DocumentSessionRepository) Create(ctx context.Context, session *entities.DocumentSession) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(session).Error
}
func (r *DocumentSessionRepository) GetByKey(ctx context.Context, documentKey string) (*entities.DocumentSession, error) {
db := DBFromContext(ctx, r.db)
var session entities.DocumentSession
err := db.WithContext(ctx).
Preload("User").
Where("document_key = ?", documentKey).
First(&session).Error
if err != nil {
return nil, err
}
return &session, nil
}
func (r *DocumentSessionRepository) GetActiveByDocument(ctx context.Context, documentID uuid.UUID) (*entities.DocumentSession, error) {
db := DBFromContext(ctx, r.db)
var session entities.DocumentSession
err := db.WithContext(ctx).
Preload("User").
Where("document_id = ? AND status != 4", documentID). // Status 4 = closed
Order("created_at DESC").
First(&session).Error
if err != nil {
return nil, err
}
return &session, nil
}
func (r *DocumentSessionRepository) Update(ctx context.Context, session *entities.DocumentSession) error {
db := DBFromContext(ctx, r.db)
// Only update specific fields to avoid association issues
updates := map[string]interface{}{
"status": session.Status,
"is_locked": session.IsLocked,
"locked_by": session.LockedBy,
"locked_at": session.LockedAt,
"last_saved_at": session.LastSavedAt,
"version": session.Version,
"updated_at": session.UpdatedAt,
}
return db.WithContext(ctx).Model(&entities.DocumentSession{}).Where("id = ?", session.ID).Updates(updates).Error
}
func (r *DocumentSessionRepository) ListByDocument(ctx context.Context, documentID uuid.UUID) ([]entities.DocumentSession, error) {
db := DBFromContext(ctx, r.db)
var sessions []entities.DocumentSession
err := db.WithContext(ctx).
Preload("User").
Where("document_id = ?", documentID).
Order("created_at DESC").
Find(&sessions).Error
return sessions, err
}
// DocumentVersionRepository handles document version operations
type DocumentVersionRepository struct {
db *gorm.DB
}
func NewDocumentVersionRepository(db *gorm.DB) *DocumentVersionRepository {
return &DocumentVersionRepository{db: db}
}
func (r *DocumentVersionRepository) Create(ctx context.Context, version *entities.DocumentVersion) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(version).Error
}
func (r *DocumentVersionRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.DocumentVersion, error) {
db := DBFromContext(ctx, r.db)
var version entities.DocumentVersion
err := db.WithContext(ctx).
Preload("User").
Where("id = ?", id).
First(&version).Error
if err != nil {
return nil, err
}
return &version, nil
}
func (r *DocumentVersionRepository) GetActiveVersion(ctx context.Context, documentID uuid.UUID) (*entities.DocumentVersion, error) {
db := DBFromContext(ctx, r.db)
var version entities.DocumentVersion
err := db.WithContext(ctx).
Preload("User").
Where("document_id = ? AND is_active = ?", documentID, true).
First(&version).Error
if err != nil {
return nil, err
}
return &version, nil
}
func (r *DocumentVersionRepository) ListByDocument(ctx context.Context, documentID uuid.UUID) ([]entities.DocumentVersion, error) {
db := DBFromContext(ctx, r.db)
var versions []entities.DocumentVersion
err := db.WithContext(ctx).
Preload("User").
Where("document_id = ?", documentID).
Order("version DESC").
Find(&versions).Error
return versions, err
}
func (r *DocumentVersionRepository) DeactivateAllVersions(ctx context.Context, documentID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).
Model(&entities.DocumentVersion{}).
Where("document_id = ?", documentID).
Update("is_active", false).Error
}
// DocumentMetadataRepository handles document metadata operations
type DocumentMetadataRepository struct {
db *gorm.DB
}
func NewDocumentMetadataRepository(db *gorm.DB) *DocumentMetadataRepository {
return &DocumentMetadataRepository{db: db}
}
func (r *DocumentMetadataRepository) Create(ctx context.Context, metadata *entities.DocumentMetadata) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(metadata).Error
}
func (r *DocumentMetadataRepository) GetByDocumentID(ctx context.Context, documentID uuid.UUID) (*entities.DocumentMetadata, error) {
db := DBFromContext(ctx, r.db)
var metadata entities.DocumentMetadata
err := db.WithContext(ctx).
Where("document_id = ?", documentID).
First(&metadata).Error
if err != nil {
return nil, err
}
return &metadata, nil
}
func (r *DocumentMetadataRepository) Update(ctx context.Context, metadata *entities.DocumentMetadata) error {
db := DBFromContext(ctx, r.db)
// Only update specific fields to avoid association issues
updates := map[string]interface{}{
"document_type": metadata.DocumentType,
"reference_id": metadata.ReferenceID,
"file_name": metadata.FileName,
"file_type": metadata.FileType,
"file_size": metadata.FileSize,
"mime_type": metadata.MimeType,
"updated_at": metadata.UpdatedAt,
}
return db.WithContext(ctx).Model(&entities.DocumentMetadata{}).Where("id = ?", metadata.ID).Updates(updates).Error
}
func (r *DocumentMetadataRepository) Delete(ctx context.Context, documentID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).
Where("document_id = ?", documentID).
Delete(&entities.DocumentMetadata{}).Error
}
// DocumentErrorRepository handles document error logging
type DocumentErrorRepository struct {
db *gorm.DB
}
func NewDocumentErrorRepository(db *gorm.DB) *DocumentErrorRepository {
return &DocumentErrorRepository{db: db}
}
func (r *DocumentErrorRepository) Create(ctx context.Context, docError *entities.DocumentError) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(docError).Error
}
func (r *DocumentErrorRepository) ListByDocument(ctx context.Context, documentID uuid.UUID) ([]entities.DocumentError, error) {
db := DBFromContext(ctx, r.db)
var errors []entities.DocumentError
err := db.WithContext(ctx).
Preload("Session").
Where("document_id = ?", documentID).
Order("created_at DESC").
Find(&errors).Error
return errors, err
}
func (r *DocumentErrorRepository) ListBySession(ctx context.Context, sessionID uuid.UUID) ([]entities.DocumentError, error) {
db := DBFromContext(ctx, r.db)
var errors []entities.DocumentError
err := db.WithContext(ctx).
Where("session_id = ?", sessionID).
Order("created_at DESC").
Find(&errors).Error
return errors, err
}
@@ -1,818 +0,0 @@
package repository
import (
"context"
"fmt"
"time"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type LetterOutgoingRepository struct{ db *gorm.DB }
func NewLetterOutgoingRepository(db *gorm.DB) *LetterOutgoingRepository {
return &LetterOutgoingRepository{db: db}
}
func (r *LetterOutgoingRepository) Create(ctx context.Context, e *entities.LetterOutgoing) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterOutgoing
if err := db.WithContext(ctx).
Preload("Priority").
Preload("ReceiverInstitution").
Preload("Creator").
Preload("ApprovalFlow").
Preload("Recipients").
Preload("Attachments").
Preload("FinalAttachments").
Preload("Approvals.Step").
Preload("Approvals.Approver").
Where("id = ? AND deleted_at IS NULL", id).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterOutgoingRepository) GetByReferenceNumber(ctx context.Context, refNumber *string) (*entities.LetterOutgoing, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterOutgoing
if err := db.WithContext(ctx).
Preload("Priority").
Preload("ReceiverInstitution").
Preload("Creator").
Preload("ApprovalFlow").
Preload("Recipients").
Preload("Attachments").
Preload("FinalAttachments").
Preload("Approvals.Step").
Preload("Approvals.Approver").
Where("reference_number = ? AND deleted_at IS NULL", refNumber).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterOutgoingRepository) Update(ctx context.Context, e *entities.LetterOutgoing) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
}
func (r *LetterOutgoingRepository) SoftDelete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
now := time.Now()
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("deleted_at", now).Error
}
func (r *LetterOutgoingRepository) BulkSoftDelete(ctx context.Context, ids []uuid.UUID) error {
if len(ids) == 0 {
return nil
}
db := DBFromContext(ctx, r.db)
now := time.Now()
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id IN ? AND deleted_at IS NULL", ids).Update("deleted_at", now).Error
}
func (r *LetterOutgoingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db)
now := time.Now()
result := db.WithContext(ctx).
Model(&entities.LetterOutgoing{}).
Where("id IN ? AND deleted_at IS NULL", letterIDs).
Updates(map[string]interface{}{
"is_archived": true,
"archived_at": now,
})
return result.RowsAffected, result.Error
}
func (r *LetterOutgoingRepository) Archive(ctx context.Context, letterID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
now := time.Now()
return db.WithContext(ctx).
Model(&entities.LetterOutgoing{}).
Where("id = ? AND deleted_at IS NULL", letterID).
Updates(map[string]interface{}{
"is_archived": true,
"archived_at": now,
}).Error
}
func (r *LetterOutgoingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db)
// Archive only the recipient records for the specific user
// Note: letter_incoming_recipients uses recipient_user_id column
result := db.WithContext(ctx).
Model(&entities.LetterOutgoingRecipient{}).
Where("letter_id IN ? AND user_id = ?", letterIDs, userID).
Update("is_archived", true)
return result.RowsAffected, result.Error
}
func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id)
// Preload all specified relations
for _, relation := range relations {
query = query.Preload(relation)
}
var e entities.LetterOutgoing
if err := query.First(&e).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &e, nil
}
type ListOutgoingLettersFilter struct {
Status *string
Query *string
CreatedBy *uuid.UUID
DepartmentID *uuid.UUID
UserID *uuid.UUID
ReceiverInstitutionID *uuid.UUID
FromDate *time.Time
ToDate *time.Time
PriorityID *uuid.UUID
PriorityIDs []uuid.UUID
SortBy *string
SortOrder *string
IsArchived *bool
IsRead *bool
}
func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("deleted_at IS NULL")
// Apply is_archived filter
if filter.IsArchived != nil {
if *filter.IsArchived {
query = query.Where("letter_outgoing_recipients.is_archived = ?", true)
} else {
query = query.Where("letter_outgoing_recipients.is_archived = ? OR letter_outgoing_recipients.is_archived IS NULL", false)
}
}
if filter.Query != nil {
q := "%" + *filter.Query + "%"
query = query.Where("subject ILIKE ? OR reference_number ILIKE ? OR letter_number ILIKE ?", q, q, q)
}
if filter.CreatedBy != nil {
query = query.Where("created_by = ?", *filter.CreatedBy)
}
// Filter by UserID through recipients
if filter.UserID != nil {
query = query.Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id")
query = query.Where("letter_outgoing_recipients.user_id = ?", *filter.UserID)
fmt.Printf("[DEBUG] filter.UserID: %v\n", filter.UserID)
fmt.Printf("[DEBUG] filter.isRead: %v\n", filter.IsRead)
// Tambahkan filter IsRead
if filter.IsRead != nil {
if *filter.IsRead {
query = query.Where("letter_outgoing_recipients.read_at IS NOT NULL")
} else {
query = query.Where("letter_outgoing_recipients.read_at IS NULL")
}
}
}
if filter.Status != nil {
query = query.Joins("LEFT JOIN letter_outgoing_approvals ON letter_outgoing_approvals.letter_id = letters_outgoing.id")
query = query.Where("letter_outgoing_approvals.approver_id = ?", *filter.UserID)
query = query.Where("letter_outgoing_approvals.status = ?", *filter.Status)
query = query.Distinct()
}
if filter.ReceiverInstitutionID != nil {
query = query.Where("receiver_institution_id = ?", *filter.ReceiverInstitutionID)
}
if filter.PriorityID != nil {
query = query.Where("priority_id = ?", *filter.PriorityID)
}
if len(filter.PriorityIDs) > 0 {
query = query.Where("priority_id IN ?", filter.PriorityIDs)
}
fmt.Printf("Priority %s", filter.PriorityIDs)
if filter.FromDate != nil {
query = query.Where("issue_date >= ?", *filter.FromDate)
}
if filter.ToDate != nil {
query = query.Where("issue_date <= ?", *filter.ToDate)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "created_at DESC" // default
if filter.SortBy != nil {
sortField := *filter.SortBy
sortDirection := "ASC"
if filter.SortOrder != nil && (*filter.SortOrder == "desc" || *filter.SortOrder == "DESC") {
sortDirection = "DESC"
}
switch sortField {
case "letter_number":
orderBy = "letter_number " + sortDirection
case "subject":
orderBy = "subject " + sortDirection
case "issue_date":
orderBy = "issue_date " + sortDirection
case "status":
orderBy = "status " + sortDirection
case "created_at":
orderBy = "created_at " + sortDirection
default:
orderBy = "created_at " + sortDirection
}
}
var list []entities.LetterOutgoing
if err := query.
Preload("Priority").
Preload("ReceiverInstitution").
Preload("Creator").
Preload("Creator.Profile").
Preload("Creator.Departments").
Preload("Recipients").
Preload("Recipients.User").
Preload("Recipients.Department").
Preload("Attachments").
Preload("FinalAttachments").
Preload("Approvals.Step").
Preload("Approvals.Approver").
Order(orderBy).
Limit(limit).
Offset(offset).
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *LetterOutgoingRepository) ListAll(ctx context.Context, filter ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("deleted_at IS NULL")
// Apply search query filter
if filter.Query != nil {
q := "%" + *filter.Query + "%"
query = query.Where("subject ILIKE ? OR reference_number ILIKE ? OR letter_number ILIKE ?", q, q, q)
}
// Filter by creator (if admin wants to see letters from specific creator)
if filter.CreatedBy != nil {
query = query.Where("created_by = ?", *filter.CreatedBy)
}
// Filter by receiver institution
if filter.ReceiverInstitutionID != nil {
query = query.Where("receiver_institution_id = ?", *filter.ReceiverInstitutionID)
}
// Filter by priority
if filter.PriorityID != nil {
query = query.Where("priority_id = ?", *filter.PriorityID)
}
// Filter by multiple priorities
if len(filter.PriorityIDs) > 0 {
query = query.Where("priority_id IN ?", filter.PriorityIDs)
}
// Date range filters
if filter.FromDate != nil {
query = query.Where("issue_date >= ?", *filter.FromDate)
}
if filter.ToDate != nil {
query = query.Where("issue_date <= ?", *filter.ToDate)
}
// Filter by approval status (if admin wants to see letters with specific approval status)
// Note: This is different from user-specific approval status
if filter.Status != nil {
query = query.Joins("LEFT JOIN letter_outgoing_approvals ON letter_outgoing_approvals.letter_id = letters_outgoing.id").
Where("letter_outgoing_approvals.status = ?", *filter.Status).
Distinct()
}
// Get total count
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Prepare sorting
orderBy := "created_at DESC" // default
if filter.SortBy != nil {
sortField := *filter.SortBy
sortDirection := "ASC"
if filter.SortOrder != nil && (*filter.SortOrder == "desc" || *filter.SortOrder == "DESC") {
sortDirection = "DESC"
}
switch sortField {
case "letter_number":
orderBy = "letter_number " + sortDirection
case "subject":
orderBy = "subject " + sortDirection
case "issue_date":
orderBy = "issue_date " + sortDirection
case "status":
orderBy = "status " + sortDirection
case "created_at":
orderBy = "created_at " + sortDirection
default:
orderBy = "created_at " + sortDirection
}
}
// Get paginated data with all relations
var list []entities.LetterOutgoing
if err := query.
Preload("Priority").
Preload("ReceiverInstitution").
Preload("Creator").
Preload("Creator.Profile").
Preload("Creator.Departments").
Preload("Recipients").
Preload("Recipients.User").
Preload("Recipients.Department").
Preload("Attachments").
Preload("FinalAttachments").
Preload("Approvals").
Preload("Approvals.Step").
Preload("Approvals.Approver").
Order(orderBy).
Limit(limit).
Offset(offset).
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *LetterOutgoingRepository) Search(ctx context.Context, filters map[string]interface{}, limit, offset int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("deleted_at IS NULL")
// Apply search filters
if q, ok := filters["query"]; ok && q != "" {
searchTerm := "%" + q.(string) + "%"
query = query.Where("subject ILIKE ? OR reference_number ILIKE ? OR letter_number ILIKE ? OR description ILIKE ? OR receiver_name ILIKE ?", searchTerm, searchTerm, searchTerm, searchTerm, searchTerm)
}
if letterNumber, ok := filters["letter_number"]; ok && letterNumber != "" {
query = query.Where("letter_number ILIKE ?", "%"+letterNumber.(string)+"%")
}
if subject, ok := filters["subject"]; ok && subject != "" {
query = query.Where("subject ILIKE ?", "%"+subject.(string)+"%")
}
if status, ok := filters["status"]; ok && status != "" {
query = query.Where("status = ?", status)
}
if priorityID, ok := filters["priority_id"]; ok {
query = query.Where("priority_id = ?", priorityID)
}
if institutionID, ok := filters["receiver_institution_id"]; ok {
query = query.Where("receiver_institution_id = ?", institutionID)
}
if createdBy, ok := filters["created_by"]; ok {
query = query.Where("created_by = ?", createdBy)
}
if dateFrom, ok := filters["date_from"]; ok {
query = query.Where("issue_date >= ?", dateFrom)
}
if dateTo, ok := filters["date_to"]; ok {
query = query.Where("issue_date <= ?", dateTo)
}
// Apply user context filters if present
if userContext, ok := filters["user_context"]; ok {
if ctx, ok := userContext.(map[string]interface{}); ok {
if userID, ok := ctx["user_id"]; ok {
// User can see: letters created by them OR letters where they are recipients
subQuery := db.Model(&entities.LetterOutgoingRecipient{}).Select("letter_id").Where("user_id = ?", userID)
query = query.Where("created_by = ? OR id IN (?)", userID, subQuery)
}
}
}
// Count total results
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Apply sorting
if sortBy == "" {
sortBy = "created_at"
}
if sortOrder == "" {
sortOrder = "desc"
}
validSortFields := map[string]bool{
"letter_number": true,
"subject": true,
"issue_date": true,
"status": true,
"created_at": true,
"updated_at": true,
}
if !validSortFields[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
orderBy := sortBy + " " + sortOrder
// Execute query with preloads
var letters []entities.LetterOutgoing
if err := query.
Preload("Priority").
Preload("ReceiverInstitution").
Preload("Creator").
Preload("Creator.Profile").
Order(orderBy).
Limit(limit).
Offset(offset).
Find(&letters).Error; err != nil {
return nil, 0, err
}
return letters, total, nil
}
func (r *LetterOutgoingRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.LetterOutgoingStatus) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("status", status).Error
}
type LetterOutgoingAttachmentRepository struct{ db *gorm.DB }
func NewLetterOutgoingAttachmentRepository(db *gorm.DB) *LetterOutgoingAttachmentRepository {
return &LetterOutgoingAttachmentRepository{db: db}
}
func (r *LetterOutgoingAttachmentRepository) Create(ctx context.Context, e *entities.LetterOutgoingAttachment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingAttachment) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterOutgoingAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingAttachment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingAttachment
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingAttachmentRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingAttachment{}).Error
}
// ListByLetterIDs fetches attachments for multiple letters in a single query
func (r *LetterOutgoingAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterOutgoingAttachment), nil
}
db := DBFromContext(ctx, r.db)
var attachments []entities.LetterOutgoingAttachment
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil {
return nil, err
}
// Group attachments by letter ID
result := make(map[uuid.UUID][]entities.LetterOutgoingAttachment)
for _, att := range attachments {
result[att.LetterID] = append(result[att.LetterID], att)
}
return result, nil
}
type LetterOutgoingFinalAttachmentRepository struct{ db *gorm.DB }
func NewLetterOutgoingFinalAttachmentRepository(db *gorm.DB) *LetterOutgoingFinalAttachmentRepository {
return &LetterOutgoingFinalAttachmentRepository{db: db}
}
func (r *LetterOutgoingFinalAttachmentRepository) Create(ctx context.Context, e *entities.LetterOutgoingFinalAttachment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingFinalAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingFinalAttachment) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterOutgoingFinalAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingFinalAttachment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingFinalAttachment
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingFinalAttachmentRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingAttachment{}).Error
}
// ListByLetterIDs fetches attachments for multiple letters in a single query
func (r *LetterOutgoingFinalAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingFinalAttachment, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterOutgoingFinalAttachment), nil
}
db := DBFromContext(ctx, r.db)
var attachments []entities.LetterOutgoingFinalAttachment
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil {
return nil, err
}
// Group attachments by letter ID
result := make(map[uuid.UUID][]entities.LetterOutgoingFinalAttachment)
for _, att := range attachments {
result[att.LetterID] = append(result[att.LetterID], att)
}
return result, nil
}
type LetterOutgoingRecipientRepository struct{ db *gorm.DB }
func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository {
return &LetterOutgoingRecipientRepository{db: db}
}
func (r *LetterOutgoingRecipientRepository) Create(ctx context.Context, e *entities.LetterOutgoingRecipient) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingRecipientRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingRecipient) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterOutgoingRecipientRepository) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
db := DBFromContext(ctx, r.db)
var count int64
sql := `
WITH valid_recipients AS (
SELECT lor.id, lor.letter_id, lor.read_at, lor.created_at,
ROW_NUMBER() OVER (PARTITION BY lor.letter_id ORDER BY lor.created_at DESC) as rn
FROM letter_outgoing_recipients lor
INNER JOIN letters_outgoing l ON l.id = lor.letter_id AND l.deleted_at IS NULL
WHERE lor.user_id = ?
)
SELECT COUNT(*)
FROM valid_recipients
WHERE rn = 1 AND read_at IS NULL
`
if err := db.WithContext(ctx).Raw(sql, userID).Scan(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}
func (r *LetterOutgoingRecipientRepository) MarkAsRead(ctx context.Context, letterID, userID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
now := time.Now()
return db.WithContext(ctx).
Model(&entities.LetterOutgoingRecipient{}).
Where("letter_id = ? AND user_id = ?", letterID, userID).
Update("read_at", now).Error
}
func (r *LetterOutgoingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingRecipient, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingRecipient
if err := db.WithContext(ctx).
Preload("User").
Preload("Department").
Where("letter_id = ?", letterID).
Order("is_primary DESC, created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingRecipientRepository) Update(ctx context.Context, e *entities.LetterOutgoingRecipient) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterOutgoingRecipient{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *LetterOutgoingRecipientRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingRecipient{}).Error
}
func (r *LetterOutgoingRecipientRepository) DeleteByLetter(ctx context.Context, letterID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("letter_id = ?", letterID).Delete(&entities.LetterOutgoingRecipient{}).Error
}
// ListByLetterIDs fetches recipients for multiple letters in a single query
func (r *LetterOutgoingRecipientRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterOutgoingRecipient), nil
}
db := DBFromContext(ctx, r.db)
var recipients []entities.LetterOutgoingRecipient
if err := db.WithContext(ctx).
Preload("User").
Preload("User.Profile").
Preload("Department").
Where("letter_id IN ?", letterIDs).
Order("is_primary DESC, created_at ASC").
Find(&recipients).Error; err != nil {
return nil, err
}
// Group recipients by letter ID
result := make(map[uuid.UUID][]entities.LetterOutgoingRecipient)
for _, rec := range recipients {
result[rec.LetterID] = append(result[rec.LetterID], rec)
}
return result, nil
}
type LetterOutgoingDiscussionRepository struct{ db *gorm.DB }
func NewLetterOutgoingDiscussionRepository(db *gorm.DB) *LetterOutgoingDiscussionRepository {
return &LetterOutgoingDiscussionRepository{db: db}
}
func (r *LetterOutgoingDiscussionRepository) Create(ctx context.Context, e *entities.LetterOutgoingDiscussion) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingDiscussionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterOutgoingDiscussion
if err := db.WithContext(ctx).
Preload("User").
Preload("Attachments").
Where("id = ?", id).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterOutgoingDiscussionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingDiscussion, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingDiscussion
if err := db.WithContext(ctx).
Preload("User").
Preload("Attachments").
Preload("Replies.User").
Where("letter_id = ? AND parent_id IS NULL", letterID).
Order("created_at DESC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterOutgoingRecipientRepository) GetByLetterIDsAndUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID]*entities.LetterOutgoingRecipient), nil
}
db := DBFromContext(ctx, r.db)
var recipients []entities.LetterOutgoingRecipient
if err := db.WithContext(ctx).
Where(`id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY letter_id ORDER BY created_at DESC) as rn
FROM letter_outgoing_recipients
WHERE letter_id IN ? AND user_id = ?
) t WHERE rn = 1
)`, letterIDs, userID).
Find(&recipients).Error; err != nil {
return nil, err
}
result := make(map[uuid.UUID]*entities.LetterOutgoingRecipient)
for i := range recipients {
result[recipients[i].LetterID] = &recipients[i]
}
return result, nil
}
func (r *LetterOutgoingDiscussionRepository) Update(ctx context.Context, e *entities.LetterOutgoingDiscussion) error {
db := DBFromContext(ctx, r.db)
now := time.Now()
e.EditedAt = &now
return db.WithContext(ctx).Model(&entities.LetterOutgoingDiscussion{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *LetterOutgoingDiscussionRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingDiscussion{}).Error
}
type LetterOutgoingDiscussionAttachmentRepository struct{ db *gorm.DB }
func NewLetterOutgoingDiscussionAttachmentRepository(db *gorm.DB) *LetterOutgoingDiscussionAttachmentRepository {
return &LetterOutgoingDiscussionAttachmentRepository{db: db}
}
func (r *LetterOutgoingDiscussionAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingDiscussionAttachment) error {
db := DBFromContext(ctx, r.db)
if len(list) == 0 {
return nil
}
return db.WithContext(ctx).Create(&list).Error
}
type LetterOutgoingActivityLogRepository struct{ db *gorm.DB }
func NewLetterOutgoingActivityLogRepository(db *gorm.DB) *LetterOutgoingActivityLogRepository {
return &LetterOutgoingActivityLogRepository{db: db}
}
func (r *LetterOutgoingActivityLogRepository) Create(ctx context.Context, e *entities.LetterOutgoingActivityLog) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterOutgoingActivityLogRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingActivityLog, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterOutgoingActivityLog
if err := db.WithContext(ctx).
Preload("ActorUser").
Preload("ActorDepartment").
Where("letter_id = ?", letterID).
Order("occurred_at DESC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
-991
View File
@@ -1,991 +0,0 @@
package repository
import (
"context"
"time"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type LetterIncomingRepository struct{ db *gorm.DB }
func NewLetterIncomingRepository(db *gorm.DB) *LetterIncomingRepository {
return &LetterIncomingRepository{db: db}
}
func (r *LetterIncomingRepository) Create(ctx context.Context, e *entities.LetterIncoming) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterIncoming
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterIncomingRepository) GetByReferenceNumber(ctx context.Context, refNumber *string) (*entities.LetterIncoming, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterIncoming
if err := db.WithContext(ctx).
Where("reference_number = ? AND deleted_at IS NULL", refNumber).
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterIncomingRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) {
return r.Get(ctx, id)
}
func (r *LetterIncomingRepository) Update(ctx context.Context, e *entities.LetterIncoming) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
}
func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL", id).Error
}
func (r *LetterIncomingRepository) BulkSoftDelete(ctx context.Context, ids []uuid.UUID) error {
if len(ids) == 0 {
return nil
}
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id IN ? AND deleted_at IS NULL", ids).Error
}
func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db)
now := time.Now()
result := db.WithContext(ctx).
Model(&entities.LetterIncoming{}).
Where("id IN ? AND deleted_at IS NULL", letterIDs).
Updates(map[string]interface{}{
"is_archived": true,
"archived_at": now,
})
return result.RowsAffected, result.Error
}
func (r *LetterIncomingRepository) Archive(ctx context.Context, letterID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
now := time.Now()
return db.WithContext(ctx).
Model(&entities.LetterIncoming{}).
Where("id = ? AND deleted_at IS NULL", letterID).
Updates(map[string]interface{}{
"is_archived": true,
"archived_at": now,
}).Error
}
// BulkArchiveForUser archives letters for a specific user only
func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db)
// Archive only the recipient records for the specific user
// Note: letter_incoming_recipients uses recipient_user_id column
result := db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}).
Where("letter_id IN ? AND recipient_user_id = ?", letterIDs, userID).
Update("is_archived", true)
return result.RowsAffected, result.Error
}
type ListIncomingLettersFilter struct {
Status *string
Query *string
DepartmentID *uuid.UUID
UserID *uuid.UUID
IsRead *bool
PriorityIDs []uuid.UUID
IsDispositioned *bool
IsArchived *bool
}
func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
joinedRecipients := false
needsGroupBy := false
if filter.DepartmentID != nil {
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID)
joinedRecipients = true
needsGroupBy = true
}
if filter.UserID != nil && filter.IsRead != nil {
if !joinedRecipients {
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id")
joinedRecipients = true
needsGroupBy = true
}
query = query.Where("letter_incoming_recipients.recipient_user_id = ?", *filter.UserID)
if *filter.IsRead {
query = query.Where("letter_incoming_recipients.read_at IS NOT NULL")
} else {
query = query.Where("letter_incoming_recipients.read_at IS NULL")
}
}
if filter.DepartmentID != nil && filter.IsDispositioned != nil {
query = query.Joins("LEFT JOIN letter_incoming_dispositions_department lidd ON lidd.letter_incoming_id = letters_incoming.id AND lidd.department_id = ?", *filter.DepartmentID)
if *filter.IsDispositioned {
query = query.Where("lidd.id IS NOT NULL AND lidd.status != 'pending'")
} else {
query = query.Where("lidd.id IS NULL OR lidd.status = 'pending'")
}
}
if len(filter.PriorityIDs) > 0 {
query = query.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs)
}
if filter.IsArchived != nil {
if *filter.IsArchived {
query = query.Where("letter_incoming_recipients.is_archived = ?", true)
} else {
query = query.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false)
}
}
if filter.Status != nil {
query = query.Where("letters_incoming.status = ?", *filter.Status)
}
if filter.Query != nil {
q := "%" + *filter.Query + "%"
query = query.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q)
}
if needsGroupBy {
query = query.Group("letters_incoming.id")
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// For the actual data fetch, we need to select all columns
var list []entities.LetterIncoming
dataQuery := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
if filter.DepartmentID != nil {
dataQuery = dataQuery.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID)
}
if filter.UserID != nil && filter.IsRead != nil {
if filter.DepartmentID == nil {
dataQuery = dataQuery.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id")
}
dataQuery = dataQuery.Where("letter_incoming_recipients.recipient_user_id = ?", *filter.UserID)
if *filter.IsRead {
dataQuery = dataQuery.Where("letter_incoming_recipients.read_at IS NOT NULL")
} else {
dataQuery = dataQuery.Where("letter_incoming_recipients.read_at IS NULL")
}
}
if filter.DepartmentID != nil && filter.IsDispositioned != nil {
dataQuery = dataQuery.Joins("LEFT JOIN letter_incoming_dispositions_department lidd ON lidd.letter_incoming_id = letters_incoming.id AND lidd.department_id = ?", *filter.DepartmentID)
if *filter.IsDispositioned {
dataQuery = dataQuery.Where("lidd.id IS NOT NULL AND lidd.status != 'pending'")
} else {
dataQuery = dataQuery.Where("lidd.id IS NULL OR lidd.status = 'pending'")
}
}
if len(filter.PriorityIDs) > 0 {
dataQuery = dataQuery.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs)
}
// Apply is_archived filter based on recipient's is_archived field
//if filter.IsArchived != nil {
// if *filter.IsArchived {
// dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ?", true)
// } else {
// dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false)
// }
//}
if filter.IsArchived != nil {
if *filter.IsArchived {
dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ?", true)
} else {
dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false)
}
}
if filter.Status != nil {
dataQuery = dataQuery.Where("letters_incoming.status = ?", *filter.Status)
}
if filter.Query != nil {
q := "%" + *filter.Query + "%"
dataQuery = dataQuery.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q)
}
if needsGroupBy {
dataQuery = dataQuery.Group("letters_incoming.id, letters_incoming.letter_number, letters_incoming.reference_number, letters_incoming.subject, letters_incoming.description, letters_incoming.priority_id, letters_incoming.sender_institution_id, letters_incoming.received_date, letters_incoming.due_date, letters_incoming.status, letters_incoming.created_by, letters_incoming.created_at, letters_incoming.updated_at, letters_incoming.deleted_at")
}
if err := dataQuery.Order("letters_incoming.created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *LetterIncomingRepository) ListAll(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
// Apply filters (same as ListAll)
if len(filter.PriorityIDs) > 0 {
query = query.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs)
}
if filter.Status != nil {
query = query.Where("letters_incoming.status = ?", *filter.Status)
}
if filter.Query != nil {
q := "%" + *filter.Query + "%"
query = query.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ? OR letters_incoming.letter_number ILIKE ?", q, q, q)
}
// Get total count
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated data with preloaded relations
var list []entities.LetterIncoming
if err := query.
Limit(limit).
Offset(offset).
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *LetterIncomingRepository) Search(ctx context.Context, filters map[string]interface{}, limit, offset int, sortBy, sortOrder string) ([]entities.LetterIncoming, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
joinedRecipients := false
needsGroupBy := false
// Apply search filters
if q, ok := filters["query"]; ok && q != "" {
searchTerm := "%" + q.(string) + "%"
query = query.Where("subject ILIKE ? OR reference_number ILIKE ? OR letter_number ILIKE ? OR description ILIKE ? OR sender_name ILIKE ?", searchTerm, searchTerm, searchTerm, searchTerm, searchTerm)
}
if letterNumber, ok := filters["letter_number"]; ok && letterNumber != "" {
query = query.Where("letter_number ILIKE ?", "%"+letterNumber.(string)+"%")
}
if subject, ok := filters["subject"]; ok && subject != "" {
query = query.Where("subject ILIKE ?", "%"+subject.(string)+"%")
}
if status, ok := filters["status"]; ok && status != "" {
query = query.Where("status = ?", status)
}
if priorityID, ok := filters["priority_id"]; ok {
query = query.Where("priority_id = ?", priorityID)
}
if institutionID, ok := filters["sender_institution_id"]; ok {
query = query.Where("sender_institution_id = ?", institutionID)
}
if createdBy, ok := filters["created_by"]; ok {
query = query.Where("created_by = ?", createdBy)
}
if dateFrom, ok := filters["date_from"]; ok {
query = query.Where("received_date >= ?", dateFrom)
}
if dateTo, ok := filters["date_to"]; ok {
query = query.Where("received_date <= ?", dateTo)
}
// Apply user context filters if present
if userContext, ok := filters["user_context"]; ok {
if ctx, ok := userContext.(map[string]interface{}); ok {
if userID, ok := ctx["user_id"]; ok {
// User can see letters where they are recipients
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
Where("letter_incoming_recipients.recipient_user_id = ?", userID)
joinedRecipients = true
needsGroupBy = true
}
if departmentID, ok := ctx["department_id"]; ok {
// Also include letters for user's department
if !joinedRecipients {
query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id")
joinedRecipients = true
needsGroupBy = true
}
query = query.Where("letter_incoming_recipients.recipient_department_id = ?", departmentID)
}
}
}
// Count total results
var total int64
if needsGroupBy {
// For grouped queries, count distinct letter IDs
if err := db.WithContext(ctx).Model(&entities.LetterIncoming{}).
Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id").
Where("letters_incoming.deleted_at IS NULL").
Distinct("letters_incoming.id").
Count(&total).Error; err != nil {
return nil, 0, err
}
} else {
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
}
// Apply sorting
if sortBy == "" {
sortBy = "created_at"
}
if sortOrder == "" {
sortOrder = "desc"
}
validSortFields := map[string]bool{
"letter_number": true,
"subject": true,
"received_date": true,
"status": true,
"created_at": true,
"updated_at": true,
}
if !validSortFields[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
orderBy := "letters_incoming." + sortBy + " " + sortOrder
// Apply grouping if necessary
if needsGroupBy {
query = query.Group("letters_incoming.id, letters_incoming.letter_number, letters_incoming.reference_number, " +
"letters_incoming.subject, letters_incoming.description, letters_incoming.priority_id, " +
"letters_incoming.sender_institution_id, letters_incoming.received_date, letters_incoming.due_date, " +
"letters_incoming.status, letters_incoming.created_by, letters_incoming.created_at, " +
"letters_incoming.updated_at, letters_incoming.deleted_at")
}
// Execute query
var letters []entities.LetterIncoming
if err := query.
Order(orderBy).
Limit(limit).
Offset(offset).
Find(&letters).Error; err != nil {
return nil, 0, err
}
return letters, total, nil
}
type LetterIncomingAttachmentRepository struct{ db *gorm.DB }
func NewLetterIncomingAttachmentRepository(db *gorm.DB) *LetterIncomingAttachmentRepository {
return &LetterIncomingAttachmentRepository{db: db}
}
func (r *LetterIncomingAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingAttachment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterIncomingAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingAttachment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingAttachment
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterIncomingAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil
}
db := DBFromContext(ctx, r.db)
var attachments []entities.LetterIncomingAttachment
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil {
return nil, err
}
// Group attachments by letter ID
result := make(map[uuid.UUID][]entities.LetterIncomingAttachment)
for _, att := range attachments {
result[att.LetterID] = append(result[att.LetterID], att)
}
return result, nil
}
type LetterIncomingActivityLogRepository struct{ db *gorm.DB }
func NewLetterIncomingActivityLogRepository(db *gorm.DB) *LetterIncomingActivityLogRepository {
return &LetterIncomingActivityLogRepository{db: db}
}
func (r *LetterIncomingActivityLogRepository) Create(ctx context.Context, e *entities.LetterIncomingActivityLog) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterIncomingActivityLogRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingActivityLog, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingActivityLog
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("occurred_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterIncomingDispositionRepository struct{ db *gorm.DB }
func NewLetterIncomingDispositionRepository(db *gorm.DB) *LetterIncomingDispositionRepository {
return &LetterIncomingDispositionRepository{db: db}
}
func (r *LetterIncomingDispositionRepository) Create(ctx context.Context, e *entities.LetterIncomingDisposition) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterIncomingDispositionRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingDisposition, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterIncomingDisposition
if err := db.WithContext(ctx).
Preload("Department").
Preload("Departments.Department").
Preload("ActionSelections.Action").
Preload("DispositionNotes.User").
First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDisposition
if err := db.WithContext(ctx).
Where("letter_id = ?", letterID).
Preload("Department").
Preload("Departments.Department").
Preload("ActionSelections.Action").
Preload("DispositionNotes.User").
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterIncomingDispositionRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterIncomingDisposition), nil
}
db := DBFromContext(ctx, r.db)
var dispositions []entities.LetterIncomingDisposition
if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).
Preload("Department").
Preload("Departments.Department").
Preload("ActionSelections.Action").
Preload("DispositionNotes.User").
Order("created_at ASC").
Find(&dispositions).Error; err != nil {
return nil, err
}
// Group by letter ID
result := make(map[uuid.UUID][]entities.LetterIncomingDisposition)
for i := range dispositions { // Gunakan index, bukan value
letterID := dispositions[i].LetterID
result[letterID] = append(result[letterID], dispositions[i])
}
return result, nil
}
func (r *LetterIncomingDispositionRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDisposition
if err := db.WithContext(ctx).
Where("letter_id = ?", letterIncomingID).
Preload("Department").
Preload("Departments.Department").
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterIncomingDispositionDepartmentRepository struct{ db *gorm.DB }
func NewLetterIncomingDispositionDepartmentRepository(db *gorm.DB) *LetterIncomingDispositionDepartmentRepository {
return &LetterIncomingDispositionDepartmentRepository{db: db}
}
func (r *LetterIncomingDispositionDepartmentRepository) DB(ctx context.Context) *gorm.DB {
return DBFromContext(ctx, r.db)
}
func (r *LetterIncomingDispositionDepartmentRepository) Create(ctx context.Context, e *entities.LetterIncomingDispositionDepartment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterIncomingDispositionDepartmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingDispositionDepartment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterIncomingDispositionDepartmentRepository) Update(ctx context.Context, e *entities.LetterIncomingDispositionDepartment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Save(e).Error
}
func (r *LetterIncomingDispositionDepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterIncomingDispositionDepartment
if err := db.WithContext(ctx).
Preload("Department").
Preload("LetterIncoming").
Preload("LetterIncomingDisposition").
First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) GetByDispositionAndDepartment(ctx context.Context, letterIncomingID, departmentID uuid.UUID) (*entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterIncomingDispositionDepartment
if err := db.WithContext(ctx).
Where("letter_incoming_id = ? AND department_id = ?", letterIncomingID, departmentID).
Preload("Department").
Preload("LetterIncoming").
Preload("LetterIncomingDisposition").
First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) GetByLetterAndDepartment(ctx context.Context, letterID, departmentID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDispositionDepartment
if err := db.WithContext(ctx).
Where("letter_incoming_id = ? AND department_id = ?", letterID, departmentID).
Preload("Department").
Preload("LetterIncoming").
Preload("LetterIncomingDisposition").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) ListByDepartmentWithPagination(ctx context.Context, departmentID uuid.UUID, status *string, offset, limit int) ([]entities.LetterIncomingDispositionDepartment, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Where("department_id = ?", departmentID)
if status != nil && *status != "" {
query = query.Where("status = ?", *status)
}
var total int64
if err := query.Model(&entities.LetterIncomingDispositionDepartment{}).Count(&total).Error; err != nil {
return nil, 0, err
}
var list []entities.LetterIncomingDispositionDepartment
if err := query.
Preload("Department").
Preload("LetterIncoming").
Preload("LetterIncomingDisposition.Department").
Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDispositionDepartment
if err := db.WithContext(ctx).
Where("letter_incoming_id = ?", letterIncomingID).
Preload("Department").
Preload("LetterIncoming").
Preload("LetterIncomingDisposition.Department").
Order("created_at DESC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.LetterIncomingDispositionDepartmentStatus, notes string, readAt, completedAt *time.Time) error {
db := DBFromContext(ctx, r.db)
updates := map[string]interface{}{
"status": status,
}
if readAt != nil {
updates["read_at"] = readAt
}
if completedAt != nil {
updates["completed_at"] = completedAt
}
if notes != "" {
updates["notes"] = notes
}
return db.WithContext(ctx).Model(&entities.LetterIncomingDispositionDepartment{}).
Where("id = ?", id).
Updates(updates).Error
}
func (r *LetterIncomingDispositionDepartmentRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDispositionDepartment
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDispositionDepartment
if len(dispositionIDs) == 0 {
return list, nil
}
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id IN ?", dispositionIDs).Order("letter_incoming_disposition_id, created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type DispositionNoteRepository struct{ db *gorm.DB }
func NewDispositionNoteRepository(db *gorm.DB) *DispositionNoteRepository {
return &DispositionNoteRepository{db: db}
}
func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.DispositionNote) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *DispositionNoteRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.DispositionNote, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionNote
if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *DispositionNoteRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.DispositionNote, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionNote
if len(dispositionIDs) == 0 {
return list, nil
}
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterDispositionActionSelectionRepository struct{ db *gorm.DB }
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
return &LetterDispositionActionSelectionRepository{db: db}
}
func (r *LetterDispositionActionSelectionRepository) CreateBulk(ctx context.Context, list []entities.LetterDispositionActionSelection) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDispositionActionSelection
if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterDispositionActionSelectionRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDispositionActionSelection
if len(dispositionIDs) == 0 {
return list, nil
}
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterDiscussionRepository struct{ db *gorm.DB }
func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository {
return &LetterDiscussionRepository{db: db}
}
func (r *LetterDiscussionRepository) Create(ctx context.Context, e *entities.LetterDiscussion) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterDiscussionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterDiscussion, error) {
db := DBFromContext(ctx, r.db)
var e entities.LetterDiscussion
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.LetterDiscussion) error {
db := DBFromContext(ctx, r.db)
// ensure edited_at is set when updating
if e.EditedAt == nil {
now := time.Now()
e.EditedAt = &now
}
return db.WithContext(ctx).Model(&entities.LetterDiscussion{}).
Where("id = ?", e.ID).
Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error
}
func (r *LetterDiscussionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDiscussion, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDiscussion
if err := db.WithContext(ctx).
Where("letter_id = ?", letterID).
Preload("User.Profile").
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterDiscussionRepository) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) {
if len(userIDs) == 0 {
return []entities.User{}, nil
}
db := DBFromContext(ctx, r.db)
var users []entities.User
if err := db.WithContext(ctx).
Where("id IN ?", userIDs).
Preload("Profile").
Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
// recipients
type LetterIncomingRecipientRepository struct{ db *gorm.DB }
func NewLetterIncomingRecipientRepository(db *gorm.DB) *LetterIncomingRecipientRepository {
return &LetterIncomingRecipientRepository{db: db}
}
func (r *LetterIncomingRecipientRepository) DB(ctx context.Context) *gorm.DB {
return DBFromContext(ctx, r.db)
}
func (r *LetterIncomingRecipientRepository) CreateBulk(ctx context.Context, recs []entities.LetterIncomingRecipient) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&recs).Error
}
func (r *LetterIncomingRecipientRepository) Create(ctx context.Context, recipient *entities.LetterIncomingRecipient) error {
return r.DB(ctx).Create(recipient).Error
}
func (r *LetterIncomingRecipientRepository) Update(ctx context.Context, recipient *entities.LetterIncomingRecipient) error {
return r.DB(ctx).Save(recipient).Error
}
func (r *LetterIncomingRecipientRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingRecipient, error) {
var recipient entities.LetterIncomingRecipient
if err := r.DB(ctx).Where("id = ?", id).First(&recipient).Error; err != nil {
return nil, err
}
return &recipient, nil
}
func (r *LetterIncomingRecipientRepository) GetByLetterAndDepartment(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (*entities.LetterIncomingRecipient, error) {
var recipient entities.LetterIncomingRecipient
if err := r.DB(ctx).Where("letter_id = ? AND recipient_department_id = ?", letterID, departmentID).First(&recipient).Error; err != nil {
return nil, err
}
return &recipient, nil
}
func (r *LetterIncomingRecipientRepository) GetByLetterAndUser(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) (*entities.LetterIncomingRecipient, error) {
var recipient entities.LetterIncomingRecipient
if err := r.DB(ctx).Where("letter_id = ? AND recipient_user_id = ?", letterID, userID).First(&recipient).Error; err != nil {
return nil, err
}
return &recipient, nil
}
func (r *LetterIncomingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
var recipients []entities.LetterIncomingRecipient
if err := r.DB(ctx).Where("letter_id = ?", letterID).Find(&recipients).Error; err != nil {
return nil, err
}
return recipients, nil
}
func (r *LetterIncomingRecipientRepository) ListByDepartment(ctx context.Context, departmentID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
var recipients []entities.LetterIncomingRecipient
if err := r.DB(ctx).Where("recipient_department_id = ?", departmentID).Find(&recipients).Error; err != nil {
return nil, err
}
return recipients, nil
}
func (r *LetterIncomingRecipientRepository) GetLetterIDsByDepartment(ctx context.Context, departmentID uuid.UUID) ([]uuid.UUID, error) {
db := DBFromContext(ctx, r.db)
var letterIDs []uuid.UUID
if err := db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}).
Where("recipient_department_id = ?", departmentID).
Distinct("letter_id").
Pluck("letter_id", &letterIDs).Error; err != nil {
return nil, err
}
return letterIDs, nil
}
func (r *LetterIncomingRecipientRepository) CountReadByLetter(ctx context.Context, letterID uuid.UUID) (int, error) {
db := DBFromContext(ctx, r.db)
var count int64
if err := db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}).
Where("letter_id = ? AND read_at IS NOT NULL", letterID).
Count(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}
func (r *LetterIncomingRecipientRepository) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
db := DBFromContext(ctx, r.db)
var count int64
sql := `
WITH valid_recipients AS (
SELECT lir.id, lir.letter_id, lir.read_at, lir.created_at,
ROW_NUMBER() OVER (PARTITION BY lir.letter_id ORDER BY lir.created_at DESC) as rn
FROM letter_incoming_recipients lir
INNER JOIN letters_incoming l ON l.id = lir.letter_id AND l.deleted_at IS NULL
WHERE lir.recipient_user_id = ?
)
SELECT COUNT(*)
FROM valid_recipients
WHERE rn = 1 AND read_at IS NULL
`
if err := db.WithContext(ctx).Raw(sql, userID).Scan(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}
func (r *LetterIncomingRecipientRepository) MarkAsRead(ctx context.Context, letterID, userID uuid.UUID) error {
db := DBFromContext(ctx, r.db)
now := time.Now()
return db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}).
Where("letter_id = ? AND recipient_user_id = ?", letterID, userID).
Update("read_at", now).Error
}
func (r *LetterIncomingRecipientRepository) GetByLetterIDsAndUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) {
if len(letterIDs) == 0 {
return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil
}
db := DBFromContext(ctx, r.db)
var recipients []entities.LetterIncomingRecipient
if err := db.WithContext(ctx).
Where(`id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY letter_id ORDER BY created_at DESC) as rn
FROM letter_incoming_recipients
WHERE letter_id IN ? AND recipient_user_id = ?
) t WHERE rn = 1
)`, letterIDs, userID).
Find(&recipients).Error; err != nil {
return nil, err
}
result := make(map[uuid.UUID]*entities.LetterIncomingRecipient)
for i := range recipients {
result[recipients[i].LetterID] = &recipients[i]
}
return result, nil
}
func (r *LetterIncomingRecipientRepository) HasDepartmentAccess(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (bool, error) {
db := DBFromContext(ctx, r.db)
var count int64
if err := db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}).
Where("letter_id = ? AND recipient_department_id = ?", letterID, departmentID).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
-389
View File
@@ -1,389 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type LabelRepository struct{ db *gorm.DB }
func NewLabelRepository(db *gorm.DB) *LabelRepository { return &LabelRepository{db: db} }
func (r *LabelRepository) Create(ctx context.Context, e *entities.Label) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *LabelRepository) Update(ctx context.Context, e *entities.Label) error {
return r.db.WithContext(ctx).Model(&entities.Label{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Label{}, "id = ?", id).Error
}
func (r *LabelRepository) List(ctx context.Context) ([]entities.Label, error) {
var list []entities.Label
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
return list, err
}
func (r *LabelRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Label, error) {
var e entities.Label
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
type PriorityRepository struct{ db *gorm.DB }
func NewPriorityRepository(db *gorm.DB) *PriorityRepository { return &PriorityRepository{db: db} }
func (r *PriorityRepository) Create(ctx context.Context, e *entities.Priority) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *PriorityRepository) Update(ctx context.Context, e *entities.Priority) error {
return r.db.WithContext(ctx).Model(&entities.Priority{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *PriorityRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Priority{}, "id = ?", id).Error
}
func (r *PriorityRepository) List(ctx context.Context) ([]entities.Priority, error) {
var list []entities.Priority
err := r.db.WithContext(ctx).Order("level ASC").Find(&list).Error
return list, err
}
func (r *PriorityRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Priority, error) {
var e entities.Priority
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *PriorityRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) {
if len(ids) == 0 {
return make(map[uuid.UUID]*entities.Priority), nil
}
var priorities []entities.Priority
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&priorities).Error; err != nil {
return nil, err
}
result := make(map[uuid.UUID]*entities.Priority)
for i := range priorities {
result[priorities[i].ID] = &priorities[i]
}
return result, nil
}
type InstitutionRepository struct{ db *gorm.DB }
func NewInstitutionRepository(db *gorm.DB) *InstitutionRepository {
return &InstitutionRepository{db: db}
}
func (r *InstitutionRepository) Create(ctx context.Context, e *entities.Institution) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *InstitutionRepository) Update(ctx context.Context, e *entities.Institution) error {
return r.db.WithContext(ctx).Model(&entities.Institution{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *InstitutionRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Institution{}, "id = ?", id).Error
}
func (r *InstitutionRepository) List(ctx context.Context) ([]entities.Institution, error) {
var list []entities.Institution
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
return list, err
}
func (r *InstitutionRepository) ListWithSearch(ctx context.Context, search *string) ([]entities.Institution, error) {
var list []entities.Institution
q := r.db.WithContext(ctx).Model(&entities.Institution{})
if search != nil && *search != "" {
like := "%" + *search + "%"
q = q.Where("name ILIKE ? OR type ILIKE ? OR address ILIKE ? OR contact_person ILIKE ?", like, like, like, like)
}
err := q.Order("name ASC").Find(&list).Error
return list, err
}
func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) {
var e entities.Institution
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *InstitutionRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) {
if len(ids) == 0 {
return make(map[uuid.UUID]*entities.Institution), nil
}
var institutions []entities.Institution
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&institutions).Error; err != nil {
return nil, err
}
result := make(map[uuid.UUID]*entities.Institution)
for i := range institutions {
result[institutions[i].ID] = &institutions[i]
}
return result, nil
}
type DispositionActionRepository struct{ db *gorm.DB }
func NewDispositionActionRepository(db *gorm.DB) *DispositionActionRepository {
return &DispositionActionRepository{db: db}
}
func (r *DispositionActionRepository) Create(ctx context.Context, e *entities.DispositionAction) error {
return r.db.WithContext(ctx).Create(e).Error
}
func (r *DispositionActionRepository) Update(ctx context.Context, e *entities.DispositionAction) error {
return r.db.WithContext(ctx).Model(&entities.DispositionAction{}).Where("id = ?", e.ID).Updates(e).Error
}
func (r *DispositionActionRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.DispositionAction{}, "id = ?", id).Error
}
func (r *DispositionActionRepository) List(ctx context.Context) ([]entities.DispositionAction, error) {
var list []entities.DispositionAction
err := r.db.WithContext(ctx).Order("sort_order NULLS LAST, label ASC").Find(&list).Error
return list, err
}
func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionAction, error) {
var e entities.DispositionAction
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *DispositionActionRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]entities.DispositionAction, error) {
var actions []entities.DispositionAction
if len(ids) == 0 {
return actions, nil
}
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&actions).Error; err != nil {
return nil, err
}
return actions, nil
}
type DepartmentRepository struct{ db *gorm.DB }
func NewDepartmentRepository(db *gorm.DB) *DepartmentRepository { return &DepartmentRepository{db: db} }
func (r *DepartmentRepository) GetByCode(ctx context.Context, code string) (*entities.Department, error) {
db := DBFromContext(ctx, r.db)
var dep entities.Department
if err := db.WithContext(ctx).Where("code = ?", code).First(&dep).Error; err != nil {
return nil, err
}
return &dep, nil
}
func (r *DepartmentRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
db := DBFromContext(ctx, r.db)
var dep entities.Department
if err := db.WithContext(ctx).First(&dep, "id = ?", id).Error; err != nil {
return nil, err
}
return &dep, nil
}
func (r *DepartmentRepository) List(ctx context.Context, search string, limit, offset int) ([]entities.Department, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.Department{})
// Add search filter if provided
if search != "" {
query = query.Where("name ILIKE ?", "%"+search+"%")
}
// Get total count
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
var list []entities.Department
if err := query.
Order("name ASC").
Limit(limit).
Offset(offset).
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *DepartmentRepository) ListWithParentFilter(ctx context.Context, search string, limit, offset int, parentPath string, excludedPaths []string) ([]entities.Department, int64, error) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Model(&entities.Department{})
// Filter by parent path if provided - include the parent itself and all descendants
if parentPath != "" {
query = query.Where("path = ? OR path <@ ?", parentPath, parentPath)
}
// Exclude specific paths
for _, excludedPath := range excludedPaths {
query = query.Where("NOT (path ~ ?)", excludedPath)
}
// Add search filter if provided
if search != "" {
query = query.Where("name ILIKE ? OR code ILIKE ?", "%"+search+"%", "%"+search+"%")
}
// Get total count
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
var list []entities.Department
if err := query.
Order("name ASC").
Limit(limit).
Offset(offset).
Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (r *DepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
db := DBFromContext(ctx, r.db)
var e entities.Department
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
}
func (r *DepartmentRepository) Create(ctx context.Context, department *entities.Department) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(department).Error
}
func (r *DepartmentRepository) Update(ctx context.Context, department *entities.Department) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Save(department).Error
}
func (r *DepartmentRepository) Delete(ctx context.Context, id uuid.UUID) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Delete(&entities.Department{}, "id = ?", id).Error
}
func (r *DepartmentRepository) GetByPath(ctx context.Context, path string) (*entities.Department, error) {
db := DBFromContext(ctx, r.db)
var department entities.Department
if err := db.WithContext(ctx).Where("path = ?", path).First(&department).Error; err != nil {
return nil, err
}
return &department, nil
}
func (r *DepartmentRepository) GetAll(ctx context.Context) ([]entities.Department, error) {
db := DBFromContext(ctx, r.db)
var departments []entities.Department
if err := db.WithContext(ctx).Order("path ASC").Find(&departments).Error; err != nil {
return nil, err
}
return departments, nil
}
func (r *DepartmentRepository) GetAllWithParentFilter(ctx context.Context, parentPath string, excludedPaths []string) ([]entities.Department, error) {
db := DBFromContext(ctx, r.db)
var departments []entities.Department
query := db.WithContext(ctx)
// Filter by parent path if provided - include the parent itself and all descendants
if parentPath != "" {
query = query.Where("path = ? OR path <@ ?", parentPath, parentPath)
}
// Exclude specific paths
for _, excludedPath := range excludedPaths {
query = query.Where("NOT (path ~ ?)", excludedPath)
}
if err := query.Order("path ASC").Find(&departments).Error; err != nil {
return nil, err
}
return departments, nil
}
func (r *DepartmentRepository) GetByPathPrefix(ctx context.Context, pathPrefix string) ([]entities.Department, error) {
db := DBFromContext(ctx, r.db)
var departments []entities.Department
// Using ltree operators for hierarchical queries
query := db.WithContext(ctx).Order("path ASC")
if pathPrefix != "" {
// Get all descendants of a path
query = query.Where("path <@ ?", pathPrefix)
}
if err := query.Find(&departments).Error; err != nil {
return nil, err
}
return departments, nil
}
func (r *DepartmentRepository) GetChildren(ctx context.Context, parentPath string) ([]entities.Department, error) {
db := DBFromContext(ctx, r.db)
var departments []entities.Department
// Get direct children and all descendants
if err := db.WithContext(ctx).
Where("path <@ ? AND path != ?", parentPath, parentPath).
Order("path ASC").
Find(&departments).Error; err != nil {
return nil, err
}
return departments, nil
}
func (r *DepartmentRepository) GetAllDescendants(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error) {
db := DBFromContext(ctx, r.db)
// First get the parent department to get its path
var parent entities.Department
if err := db.WithContext(ctx).First(&parent, "id = ?", parentID).Error; err != nil {
return nil, err
}
var departments []entities.Department
// Get all descendants using ltree
if err := db.WithContext(ctx).
Where("path <@ ? AND path != ?", parent.Path, parent.Path).
Order("path ASC").
Find(&departments).Error; err != nil {
return nil, err
}
return departments, nil
}
func (r *DepartmentRepository) UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error {
db := DBFromContext(ctx, r.db)
// Use raw SQL for ltree path update
// This will update all children paths by replacing the old prefix with the new one
query := `
UPDATE departments
SET path = ? || subpath(path, nlevel(?))
WHERE path <@ ? AND path != ?
`
return db.WithContext(ctx).Exec(query, newPath, oldPath, oldPath, oldPath).Error
}
-175
View File
@@ -1,175 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type RBACRepository struct {
db *gorm.DB
}
func NewRBACRepository(db *gorm.DB) *RBACRepository { return &RBACRepository{db: db} }
// Permissions
func (r *RBACRepository) CreatePermission(ctx context.Context, p *entities.Permission) error {
return r.db.WithContext(ctx).Create(p).Error
}
func (r *RBACRepository) UpdatePermission(ctx context.Context, p *entities.Permission) error {
return r.db.WithContext(ctx).Model(&entities.Permission{}).Where("id = ?", p.ID).Updates(p).Error
}
func (r *RBACRepository) DeletePermission(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Permission{}, "id = ?", id).Error
}
func (r *RBACRepository) ListPermissions(ctx context.Context) ([]entities.Permission, error) {
var perms []entities.Permission
if err := r.db.WithContext(ctx).Preload("Module").Order("code ASC").Find(&perms).Error; err != nil {
return nil, err
}
return perms, nil
}
func (r *RBACRepository) GetPermissionByCode(ctx context.Context, code string) (*entities.Permission, error) {
var p entities.Permission
if err := r.db.WithContext(ctx).First(&p, "code = ?", code).Error; err != nil {
return nil, err
}
return &p, nil
}
// Roles
func (r *RBACRepository) CreateRole(ctx context.Context, role *entities.Role) error {
return r.db.WithContext(ctx).Create(role).Error
}
func (r *RBACRepository) UpdateRole(ctx context.Context, role *entities.Role) error {
return r.db.WithContext(ctx).Model(&entities.Role{}).Where("id = ?", role.ID).Updates(role).Error
}
func (r *RBACRepository) DeleteRole(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Role{}, "id = ?", id).Error
}
func (r *RBACRepository) ListRoles(ctx context.Context) ([]entities.Role, error) {
var roles []entities.Role
if err := r.db.WithContext(ctx).Order("name ASC").Find(&roles).Error; err != nil {
return nil, err
}
return roles, nil
}
func (r *RBACRepository) GetRoleByCode(ctx context.Context, code string) (*entities.Role, error) {
var role entities.Role
if err := r.db.WithContext(ctx).First(&role, "code = ?", code).Error; err != nil {
return nil, err
}
return &role, nil
}
func (r *RBACRepository) SetRolePermissionsByCodes(ctx context.Context, roleID uuid.UUID, permCodes []string) error {
if err := r.db.WithContext(ctx).Where("role_id = ?", roleID).Delete(&entities.RolePermission{}).Error; err != nil {
return err
}
if len(permCodes) == 0 {
return nil
}
var perms []entities.Permission
if err := r.db.WithContext(ctx).Where("code IN ?", permCodes).Find(&perms).Error; err != nil {
return err
}
pairs := make([]entities.RolePermission, 0, len(perms))
for _, p := range perms {
pairs = append(pairs, entities.RolePermission{RoleID: roleID, PermissionID: p.ID})
}
return r.db.WithContext(ctx).Create(&pairs).Error
}
func (r *RBACRepository) GetPermissionsByRoleID(ctx context.Context, roleID uuid.UUID) ([]entities.Permission, error) {
var perms []entities.Permission
if err := r.db.WithContext(ctx).
Preload("Module").
Table("permissions p").
Select("p.*").
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
Where("rp.role_id = ?", roleID).
Find(&perms).Error; err != nil {
return nil, err
}
return perms, nil
}
// Modules
func (r *RBACRepository) CreateModule(ctx context.Context, m *entities.Module) error {
return r.db.WithContext(ctx).Create(m).Error
}
func (r *RBACRepository) UpdateModule(ctx context.Context, m *entities.Module) error {
return r.db.WithContext(ctx).Model(&entities.Module{}).Where("id = ?", m.ID).Updates(m).Error
}
func (r *RBACRepository) DeleteModule(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Module{}, "id = ?", id).Error
}
func (r *RBACRepository) ListModules(ctx context.Context) ([]entities.Module, error) {
var modules []entities.Module
if err := r.db.WithContext(ctx).Order("name ASC").Find(&modules).Error; err != nil {
return nil, err
}
return modules, nil
}
func (r *RBACRepository) GetModuleByCode(ctx context.Context, code string) (*entities.Module, error) {
var m entities.Module
if err := r.db.WithContext(ctx).First(&m, "code = ?", code).Error; err != nil {
return nil, err
}
return &m, nil
}
func (r *RBACRepository) GetModuleByID(ctx context.Context, id uuid.UUID) (*entities.Module, error) {
var m entities.Module
if err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
return nil, err
}
return &m, nil
}
func (r *RBACRepository) GetPermissionsGroupedByModule(ctx context.Context) ([]entities.Module, error) {
var modules []entities.Module
if err := r.db.WithContext(ctx).
Preload("Permissions", func(db *gorm.DB) *gorm.DB {
return db.Order("action ASC")
}).
Find(&modules).Error; err != nil {
return nil, err
}
return modules, nil
}
func (r *RBACRepository) GetRoleByID(ctx context.Context, id uuid.UUID) (*entities.Role, error) {
var role entities.Role
if err := r.db.WithContext(ctx).First(&role, "id = ?", id).Error; err != nil {
return nil, err
}
return &role, nil
}
func (r *RBACRepository) SetRolePermissionsByIDs(ctx context.Context, roleID uuid.UUID, permissionIDs []uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Delete existing permissions
if err := tx.WithContext(ctx).Where("role_id = ?", roleID).Delete(&entities.RolePermission{}).Error; err != nil {
return err
}
// Add new permissions
if len(permissionIDs) == 0 {
return nil
}
pairs := make([]entities.RolePermission, 0, len(permissionIDs))
for _, permID := range permissionIDs {
pairs = append(pairs, entities.RolePermission{RoleID: roleID, PermissionID: permID})
}
return tx.WithContext(ctx).Create(&pairs).Error
})
}
@@ -1,74 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type RepositoryAttachmentRepositoryImpl struct {
b *gorm.DB
}
func NewRepositoryAttachmentRepositoryImpl(db *gorm.DB) *RepositoryAttachmentRepositoryImpl {
return &RepositoryAttachmentRepositoryImpl{
b: db,
}
}
func (r *RepositoryAttachmentRepositoryImpl) Create(ctx context.Context, user *entities.RepositoryAttachment) error {
return r.b.WithContext(ctx).Create(user).Error
}
func (r *RepositoryAttachmentRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.RepositoryAttachment, error) {
var attachment entities.RepositoryAttachment
err := r.b.WithContext(ctx).
First(&attachment, "id = ?", id).Error
if err != nil {
return nil, err
}
return &attachment, nil
}
func (r *RepositoryAttachmentRepositoryImpl) Update(ctx context.Context, user *entities.RepositoryAttachment) error {
return r.b.WithContext(ctx).Save(user).Error
}
func (r *RepositoryAttachmentRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
return r.b.WithContext(ctx).Delete(&entities.RepositoryAttachment{}, "id = ?", id).Error
}
func (r *RepositoryAttachmentRepositoryImpl) List(ctx context.Context, search *string, limit, offset int) ([]*entities.RepositoryAttachment, int64, error) {
var attachments []*entities.RepositoryAttachment
var total int64
baseQuery := r.b.WithContext(ctx).Model(&entities.RepositoryAttachment{})
if search != nil && *search != "" {
like := "%" + *search + "%"
baseQuery = baseQuery.Where("name ILIKE ? OR email ILIKE ?", like, like)
}
countQuery := baseQuery
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
dataQuery := r.b.WithContext(ctx).Model(&entities.RepositoryAttachment{})
if search != nil && *search != "" {
like := "%" + *search + "%"
dataQuery = dataQuery.Where("name ILIKE ? OR category ILIKE ?", like, like)
}
if err := dataQuery.
Limit(limit).
Offset(offset).
Find(&attachments).Error; err != nil {
return nil, 0, err
}
return attachments, total, nil
}
-25
View File
@@ -1,25 +0,0 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"gorm.io/gorm"
)
type TitleRepository struct {
db *gorm.DB
}
func NewTitleRepository(db *gorm.DB) *TitleRepository {
return &TitleRepository{db: db}
}
func (r *TitleRepository) ListAll(ctx context.Context) ([]entities.Title, error) {
var titles []entities.Title
if err := r.db.WithContext(ctx).Order("name ASC").Find(&titles).Error; err != nil {
return nil, err
}
return titles, nil
}
@@ -1,33 +0,0 @@
package repository
import (
"context"
"github.com/google/uuid"
"gorm.io/gorm"
)
type UserDepartmentRepository struct{ db *gorm.DB }
func NewUserDepartmentRepository(db *gorm.DB) *UserDepartmentRepository {
return &UserDepartmentRepository{db: db}
}
type UserDepartmentRow struct {
UserID uuid.UUID `gorm:"column:user_id"`
DepartmentID uuid.UUID `gorm:"column:department_id"`
}
func (r *UserDepartmentRepository) ListActiveByDepartmentIDs(ctx context.Context, departmentIDs []uuid.UUID) ([]UserDepartmentRow, error) {
db := DBFromContext(ctx, r.db)
rows := make([]UserDepartmentRow, 0)
if len(departmentIDs) == 0 {
return rows, nil
}
err := db.WithContext(ctx).
Table("user_department").
Select("user_id, department_id").
Where("department_id IN ? AND removed_at IS NULL", departmentIDs).
Find(&rows).Error
return rows, err
}
@@ -6,7 +6,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"eslogad-be/internal/entities"
"go-backend-template/internal/entities"
"github.com/google/uuid"
)
+36 -133
View File
@@ -3,7 +3,7 @@ package repository
import (
"context"
"eslogad-be/internal/entities"
"go-backend-template/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
@@ -27,7 +27,6 @@ func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entiti
var user entities.User
err := r.b.WithContext(ctx).
Preload("Profile").
Preload("Departments").
First(&user, "id = ?", id).Error
if err != nil {
return nil, err
@@ -51,7 +50,6 @@ func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*ent
var user entities.User
err := r.b.WithContext(ctx).
Preload("Profile").
Preload("Departments").
Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
@@ -59,18 +57,28 @@ func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*ent
return &user, nil
}
func (r *UserRepositoryImpl) GetByUsername(ctx context.Context, username string) (*entities.User, error) {
var user entities.User
err := r.b.WithContext(ctx).
Preload("Profile").
Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
var users []*entities.User
err := r.b.WithContext(ctx).Preload("Profile").Preload("Departments").Where("role = ?", role).Find(&users).Error
err := r.b.WithContext(ctx).Preload("Profile").Where("role = ?", role).Find(&users).Error
return users, err
}
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context) ([]*entities.User, error) {
var users []*entities.User
err := r.b.WithContext(ctx).
Where(" is_active = ?", organizationID, true).
Where("is_active = ?", true).
Preload("Profile").
Preload("Departments").
Find(&users).Error
return users, err
}
@@ -109,7 +117,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
return nil, 0, err
}
err := query.Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error
err := query.Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error
return users, total, err
}
@@ -125,155 +133,50 @@ func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]inter
return count, err
}
// RBAC helpers
func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
var roles []entities.Role
err := r.b.WithContext(ctx).
Table("roles as r").
Select("r.*").
Joins("JOIN user_role ur ON ur.role_id = r.id AND ur.removed_at IS NULL").
Where("ur.user_id = ?", userID).
Find(&roles).Error
return roles, err
}
func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
var perms []entities.Permission
err := r.b.WithContext(ctx).
Table("permissions as p").
Select("DISTINCT p.*").
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
Joins("JOIN user_role ur ON ur.role_id = rp.role_id AND ur.removed_at IS NULL").
Where("ur.user_id = ?", userID).
Find(&perms).Error
return perms, err
}
func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error) {
var departments []entities.Department
err := r.b.WithContext(ctx).
Table("departments as d").
Select("d.*").
Joins("JOIN user_department ud ON ud.department_id = d.id AND ud.removed_at IS NULL").
Where("ud.user_id = ?", userID).
Find(&departments).Error
return departments, err
}
func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
result := make(map[uuid.UUID][]entities.Role)
if len(userIDs) == 0 {
return result, nil
}
type row struct {
UserID uuid.UUID
RoleID uuid.UUID
Name string
Code string
}
var rows []row
err := r.b.WithContext(ctx).
Table("user_role as ur").
Select("ur.user_id, r.id as role_id, r.name, r.code").
Joins("JOIN roles r ON r.id = ur.role_id").
Where("ur.removed_at IS NULL AND ur.user_id IN ?", userIDs).
Scan(&rows).Error
if err != nil {
return nil, err
}
for _, rw := range rows {
role := entities.Role{ID: rw.RoleID, Name: rw.Name, Code: rw.Code}
result[rw.UserID] = append(result[rw.UserID], role)
}
return result, nil
}
func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
func (r *UserRepositoryImpl) GetAll(ctx context.Context, page, limit int) ([]*entities.User, int64, error) {
var users []*entities.User
var total int64
// Build base query - use Model directly without Table for proper field mapping
baseQuery := r.b.WithContext(ctx).Model(&entities.User{})
if search != nil && *search != "" {
like := "%" + *search + "%"
baseQuery = baseQuery.Where("name ILIKE ? OR email ILIKE ?", like, like)
}
offset := (page - 1) * limit
if isActive != nil {
baseQuery = baseQuery.Where("is_active = ?", *isActive)
}
query := r.b.WithContext(ctx).Model(&entities.User{})
// For counting with role filter, we need to use a subquery or join
countQuery := baseQuery
if roleCode != nil && *roleCode != "" {
countQuery = countQuery.
Joins("JOIN user_role ur ON ur.user_id = users.id AND ur.removed_at IS NULL").
Joins("JOIN roles r ON r.id = ur.role_id").
Where("r.code = ?", *roleCode)
}
// Get total count
if err := countQuery.Count(&total).Error; err != nil {
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Build query for fetching data
dataQuery := r.b.WithContext(ctx).Model(&entities.User{})
err := query.Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error
return users, total, err
}
func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
var users []*entities.User
var total int64
query := r.b.WithContext(ctx).Model(&entities.User{})
if search != nil && *search != "" {
like := "%" + *search + "%"
dataQuery = dataQuery.Where("name ILIKE ? OR email ILIKE ?", like, like)
query = query.Where("name ILIKE ? OR email ILIKE ? OR username ILIKE ?", like, like, like)
}
if isActive != nil {
dataQuery = dataQuery.Where("is_active = ?", *isActive)
query = query.Where("is_active = ?", *isActive)
}
if roleCode != nil && *roleCode != "" {
dataQuery = dataQuery.
Joins("JOIN user_role ur ON ur.user_id = users.id AND ur.removed_at IS NULL").
Joins("JOIN roles r ON r.id = ur.role_id").
Where("r.code = ?", *roleCode)
// Get total count
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Fetch users with preloads
if err := dataQuery.
if err := query.
Limit(limit).
Offset(offset).
Preload("Profile").
Preload("Departments").
Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func (r *UserRepositoryImpl) GetByIDWithDepartments(ctx context.Context, id uuid.UUID) (*entities.User, error) {
var user entities.User
err := r.b.WithContext(ctx).
Preload("Profile").
Preload("Departments").
First(&user, "id = ?", id).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepositoryImpl) UpdateDepartments(ctx context.Context, userID uuid.UUID, departments []entities.Department) error {
// First, clear existing associations
if err := r.b.WithContext(ctx).Model(&entities.User{ID: userID}).Association("Departments").Clear(); err != nil {
return err
}
// Then add new associations
if len(departments) > 0 {
return r.b.WithContext(ctx).Model(&entities.User{ID: userID}).Association("Departments").Append(&departments)
}
return nil
}