This commit is contained in:
Aditya Siregar 2025-08-09 23:44:03 +07:00
parent 61d6eed373
commit de60983e4e
24 changed files with 493 additions and 153 deletions

View File

@ -121,6 +121,10 @@ type repositories struct {
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository dispositionNoteRepo *repository.DispositionNoteRepository
letterDiscussionRepo *repository.LetterDiscussionRepository letterDiscussionRepo *repository.LetterDiscussionRepository
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
} }
func (a *App) initRepositories() *repositories { func (a *App) initRepositories() *repositories {
@ -141,6 +145,10 @@ func (a *App) initRepositories() *repositories {
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db), letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db), dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db), letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
settingRepo: repository.NewAppSettingRepository(a.db),
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
departmentRepo: repository.NewDepartmentRepository(a.db),
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
} }
} }
@ -155,7 +163,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
activity := processor.NewActivityLogProcessor(repos.activityLogRepo) activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
return &processors{ return &processors{
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo), userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo), letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo),
activityLogger: activity, activityLogger: activity,
} }
} }

View File

@ -2,6 +2,12 @@ package contract
import "time" import "time"
const (
SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX"
SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE"
SettingIncomingLetterRecipients = "INCOMING_LETTER_RECIPIENTS"
)
type ErrorResponse struct { type ErrorResponse struct {
Error string `json:"error"` Error string `json:"error"`
Message string `json:"message"` Message string `json:"message"`

View File

@ -39,21 +39,23 @@ type LoginRequest struct {
} }
type LoginResponse struct { type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
User UserResponse `json:"user"` User UserResponse `json:"user"`
Roles []RoleResponse `json:"roles"` Roles []RoleResponse `json:"roles"`
Permissions []string `json:"permissions"` Permissions []string `json:"permissions"`
Positions []PositionResponse `json:"positions"` Departments []DepartmentResponse `json:"departments"`
} }
type UserResponse struct { type UserResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Roles []RoleResponse `json:"roles,omitempty"`
Profile *UserProfileResponse `json:"profile,omitempty"`
} }
type ListUsersRequest struct { type ListUsersRequest struct {
@ -61,6 +63,8 @@ type ListUsersRequest struct {
Limit int `json:"limit" validate:"min=1,max=100"` Limit int `json:"limit" validate:"min=1,max=100"`
Role *string `json:"role,omitempty"` Role *string `json:"role,omitempty"`
IsActive *bool `json:"is_active,omitempty"` IsActive *bool `json:"is_active,omitempty"`
Search *string `json:"search,omitempty"`
RoleCode *string `json:"role_code,omitempty"`
} }
type ListUsersResponse struct { type ListUsersResponse struct {
@ -74,7 +78,7 @@ type RoleResponse struct {
Code string `json:"code"` Code string `json:"code"`
} }
type PositionResponse struct { type DepartmentResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Code string `json:"code"` Code string `json:"code"`
@ -97,6 +101,7 @@ type UserProfileResponse struct {
LastSeenAt *time.Time `json:"last_seen_at,omitempty"` LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Roles []RoleResponse `json:"roles"`
} }
type UpdateUserProfileRequest struct { type UpdateUserProfileRequest struct {

View File

@ -0,0 +1,18 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type Department struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null" json:"name"`
Code string `json:"code,omitempty"`
Path string `gorm:"not null" json:"path"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Department) TableName() string { return "departments" }

View File

@ -0,0 +1,28 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type LetterIncomingRecipientStatus string
const (
RecipientStatusNew LetterIncomingRecipientStatus = "new"
RecipientStatusRead LetterIncomingRecipientStatus = "read"
RecipientStatusCompleted LetterIncomingRecipientStatus = "completed"
)
type LetterIncomingRecipient struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
RecipientUserID *uuid.UUID `json:"recipient_user_id,omitempty"`
RecipientDepartmentID *uuid.UUID `json:"recipient_department_id,omitempty"`
Status LetterIncomingRecipientStatus `gorm:"not null;default:'new'" json:"status"`
ReadAt *time.Time `json:"read_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
func (LetterIncomingRecipient) TableName() string { return "letter_incoming_recipients" }

View File

@ -0,0 +1,14 @@
package entities
import (
"time"
)
type AppSetting struct {
Key string `gorm:"primaryKey;size:100" json:"key"`
Value JSONB `gorm:"type:jsonb;default:'{}'" json:"value"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (AppSetting) TableName() string { return "app_settings" }

View File

@ -40,13 +40,14 @@ func (p *Permissions) Scan(value interface{}) error {
} }
type User struct { type User struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"` Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
PasswordHash string `gorm:"not null;size:255" json:"-"` PasswordHash string `gorm:"not null;size:255" json:"-"`
IsActive bool `gorm:"default:true" json:"is_active"` IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
} }
func (u *User) BeforeCreate(tx *gorm.DB) error { func (u *User) BeforeCreate(tx *gorm.DB) error {

View File

@ -166,10 +166,24 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
} }
} }
var roleParam *string
if role := c.Query("role"); role != "" { if role := c.Query("role"); role != "" {
roleParam = &role
req.Role = &role req.Role = &role
} }
if roleCode := c.Query("role_code"); roleCode != "" {
req.RoleCode = &roleCode
}
if req.RoleCode == nil && roleParam != nil {
req.RoleCode = roleParam
}
if search := c.Query("search"); search != "" {
req.Search = &search
}
if isActiveStr := c.Query("is_active"); isActiveStr != "" { if isActiveStr := c.Query("is_active"); isActiveStr != "" {
if isActive, err := strconv.ParseBool(isActiveStr); err == nil { if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
req.IsActive = &isActive req.IsActive = &isActive

View File

@ -2,6 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt"
"time" "time"
"eslogad-be/internal/appcontext" "eslogad-be/internal/appcontext"
@ -24,16 +25,37 @@ type LetterProcessorImpl struct {
dispositionNoteRepo *repository.DispositionNoteRepository dispositionNoteRepo *repository.DispositionNoteRepository
// discussion repo // discussion repo
discussionRepo *repository.LetterDiscussionRepository discussionRepo *repository.LetterDiscussionRepository
// settings and recipients
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
} }
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository) *LetterProcessorImpl { func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo} return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo}
} }
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
var result *contract.IncomingLetterResponse var result *contract.IncomingLetterResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID userID := appcontext.FromGinContext(txCtx).UserID
prefix := "ESLI"
seq := 0
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterPrefix); err == nil {
if v, ok := s.Value["value"].(string); ok && v != "" {
prefix = v
}
}
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterSequence); err == nil {
if v, ok := s.Value["value"].(float64); ok {
seq = int(v)
}
}
seq = seq + 1
letterNumber := fmt.Sprintf("%s%04d", prefix, seq)
entity := &entities.LetterIncoming{ entity := &entities.LetterIncoming{
ReferenceNumber: req.ReferenceNumber, ReferenceNumber: req.ReferenceNumber,
Subject: req.Subject, Subject: req.Subject,
@ -45,26 +67,63 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
Status: entities.LetterIncomingStatusNew, Status: entities.LetterIncomingStatusNew,
CreatedBy: userID, CreatedBy: userID,
} }
entity.LetterNumber = letterNumber
if err := p.letterRepo.Create(txCtx, entity); err != nil { if err := p.letterRepo.Create(txCtx, entity); err != nil {
return err return err
} }
_ = p.settingRepo.Upsert(txCtx, contract.SettingIncomingLetterSequence, entities.JSONB{"value": seq})
defaultDeptCodes := []string{}
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterRecipients); err == nil {
if arr, ok := s.Value["department_codes"].([]interface{}); ok {
for _, it := range arr {
if str, ok := it.(string); ok {
defaultDeptCodes = append(defaultDeptCodes, str)
}
}
}
}
// resolve department codes to ids using repository
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
for _, code := range defaultDeptCodes {
dep, err := p.departmentRepo.GetByCode(txCtx, code)
if err != nil {
continue
}
depIDs = append(depIDs, dep.ID)
}
// query user memberships for all departments at once
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
// build recipients: one department recipient per department + one user recipient per membership
recipients := make([]entities.LetterIncomingRecipient, 0, len(depIDs)+len(userMemberships))
// department recipients
for _, depID := range depIDs {
id := depID
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientDepartmentID: &id, Status: entities.RecipientStatusNew})
}
// user recipients
for _, row := range userMemberships {
uid := row.UserID
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, Status: entities.RecipientStatusNew})
}
if len(recipients) > 0 {
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
return err
}
}
if p.activity != nil { if p.activity != nil {
action := "letter.created" action := "letter.created"
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil { if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{"letter_number": letterNumber}); err != nil {
return err return err
} }
} }
attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments)) attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments))
for _, a := range req.Attachments { for _, a := range req.Attachments {
attachments = append(attachments, entities.LetterIncomingAttachment{ attachments = append(attachments, entities.LetterIncomingAttachment{LetterID: entity.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID})
LetterID: entity.ID,
FileURL: a.FileURL,
FileName: a.FileName,
FileType: a.FileType,
UploadedBy: &userID,
})
} }
if len(attachments) > 0 { if len(attachments) > 0 {
if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil { if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil {
@ -85,7 +144,6 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
result = transformer.LetterEntityToContract(entity, savedAttachments) result = transformer.LetterEntityToContract(entity, savedAttachments)
return nil return nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -267,7 +325,7 @@ func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uui
return err return err
} }
if p.activity != nil { if p.activity != nil {
action := "discussion.created" action := "reference_numberdiscussion.created"
tgt := "discussion" tgt := "discussion"
ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID} ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID}
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil { if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {

View File

@ -110,8 +110,13 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
if err != nil { if err != nil {
return nil, fmt.Errorf("user not found: %w", err) return nil, fmt.Errorf("user not found: %w", err)
} }
resp := transformer.EntityToContract(user)
return transformer.EntityToContract(user), nil if resp != nil {
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
resp.Roles = transformer.RolesToContract(roles)
}
}
return resp, nil
} }
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) { func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
@ -123,17 +128,35 @@ func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*
return transformer.EntityToContract(user), nil return transformer.EntityToContract(user), nil
} }
func (p *UserProcessorImpl) ListUsers(ctx context.Context, page, limit int) ([]contract.UserResponse, int, error) { func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error) {
page := req.Page
if page <= 0 {
page = 1
}
limit := req.Limit
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit offset := (page - 1) * limit
filters := map[string]interface{}{} users, totalCount, err := p.userRepo.ListWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
users, totalCount, err := p.userRepo.List(ctx, filters, limit, offset)
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("failed to get users: %w", err) return nil, 0, fmt.Errorf("failed to get users: %w", err)
} }
responses := transformer.EntitiesToContracts(users) responses := transformer.EntitiesToContracts(users)
userIDs := make([]uuid.UUID, 0, len(responses))
for i := range responses {
userIDs = append(userIDs, responses[i].ID)
}
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
if err == nil {
for i := range responses {
if roles, ok := rolesMap[responses[i].ID]; ok {
responses[i].Roles = transformer.RolesToContract(roles)
}
}
}
return responses, int(totalCount), nil return responses, int(totalCount), nil
} }
@ -219,12 +242,12 @@ func (p *UserProcessorImpl) GetUserPermissionCodes(ctx context.Context, userID u
return codes, nil return codes, nil
} }
func (p *UserProcessorImpl) GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error) { func (p *UserProcessorImpl) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error) {
positions, err := p.userRepo.GetPositionsByUserID(ctx, userID) departments, err := p.userRepo.GetDepartmentsByUserID(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return transformer.PositionsToContract(positions), nil return transformer.DepartmentsToContract(departments), nil
} }
func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) { func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {

View File

@ -3,6 +3,7 @@ package processor
import ( import (
"context" "context"
"eslogad-be/internal/entities" "eslogad-be/internal/entities"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -21,5 +22,9 @@ type UserRepository interface {
GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error)
GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error)
GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error)
// New optimized helpers
GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error)
ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error)
} }

View File

@ -178,3 +178,31 @@ func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.Let
Where("id = ?", e.ID). Where("id = ?", e.ID).
Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error
} }
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
}
// recipients
type LetterIncomingRecipientRepository struct{ db *gorm.DB }
func NewLetterIncomingRecipientRepository(db *gorm.DB) *LetterIncomingRecipientRepository {
return &LetterIncomingRecipientRepository{db: db}
}
func (r *LetterIncomingRecipientRepository) CreateBulk(ctx context.Context, recs []entities.LetterIncomingRecipient) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&recs).Error
}

View File

@ -112,3 +112,16 @@ func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*e
} }
return &e, nil return &e, 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
}

View File

@ -0,0 +1,34 @@
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"`
}
// ListActiveByDepartmentIDs returns active user-department memberships for given department IDs.
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
}

View File

@ -10,22 +10,22 @@ import (
) )
type UserRepositoryImpl struct { type UserRepositoryImpl struct {
db *gorm.DB b *gorm.DB
} }
func NewUserRepository(db *gorm.DB) *UserRepositoryImpl { func NewUserRepository(db *gorm.DB) *UserRepositoryImpl {
return &UserRepositoryImpl{ return &UserRepositoryImpl{
db: db, b: db,
} }
} }
func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) error { func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) error {
return r.db.WithContext(ctx).Create(user).Error return r.b.WithContext(ctx).Create(user).Error
} }
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) { func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
var user entities.User var user entities.User
err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error err := r.b.WithContext(ctx).Preload("Profile").First(&user, "id = ?", id).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -34,7 +34,7 @@ func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entiti
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) { func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
var user entities.User var user entities.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error err := r.b.WithContext(ctx).Preload("Profile").Where("email = ?", email).First(&user).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -43,34 +43,35 @@ func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*ent
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) { func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
var users []*entities.User var users []*entities.User
err := r.db.WithContext(ctx).Where("role = ?", role).Find(&users).Error err := r.b.WithContext(ctx).Preload("Profile").Where("role = ?", role).Find(&users).Error
return users, err return users, err
} }
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) { func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
var users []*entities.User var users []*entities.User
err := r.db.WithContext(ctx). err := r.b.WithContext(ctx).
Where(" is_active = ?", organizationID, true). Where(" is_active = ?", organizationID, true).
Preload("Profile").
Find(&users).Error Find(&users).Error
return users, err return users, err
} }
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error { func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error {
return r.db.WithContext(ctx).Save(user).Error return r.b.WithContext(ctx).Save(user).Error
} }
func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error return r.b.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error
} }
func (r *UserRepositoryImpl) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error { func (r *UserRepositoryImpl) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
return r.db.WithContext(ctx).Model(&entities.User{}). return r.b.WithContext(ctx).Model(&entities.User{}).
Where("id = ?", id). Where("id = ?", id).
Update("password_hash", passwordHash).Error Update("password_hash", passwordHash).Error
} }
func (r *UserRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error { func (r *UserRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
return r.db.WithContext(ctx).Model(&entities.User{}). return r.b.WithContext(ctx).Model(&entities.User{}).
Where("id = ?", id). Where("id = ?", id).
Update("is_active", isActive).Error Update("is_active", isActive).Error
} }
@ -79,7 +80,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
var users []*entities.User var users []*entities.User
var total int64 var total int64
query := r.db.WithContext(ctx).Model(&entities.User{}) query := r.b.WithContext(ctx).Model(&entities.User{})
for key, value := range filters { for key, value := range filters {
query = query.Where(key+" = ?", value) query = query.Where(key+" = ?", value)
@ -89,13 +90,13 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
return nil, 0, err return nil, 0, err
} }
err := query.Limit(limit).Offset(offset).Find(&users).Error err := query.Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error
return users, total, err return users, total, err
} }
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
var count int64 var count int64
query := r.db.WithContext(ctx).Model(&entities.User{}) query := r.b.WithContext(ctx).Model(&entities.User{})
for key, value := range filters { for key, value := range filters {
query = query.Where(key+" = ?", value) query = query.Where(key+" = ?", value)
@ -108,7 +109,7 @@ func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]inter
// RBAC helpers // RBAC helpers
func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) { func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
var roles []entities.Role var roles []entities.Role
err := r.db.WithContext(ctx). err := r.b.WithContext(ctx).
Table("roles as r"). Table("roles as r").
Select("r.*"). Select("r.*").
Joins("JOIN user_role ur ON ur.role_id = r.id AND ur.removed_at IS NULL"). Joins("JOIN user_role ur ON ur.role_id = r.id AND ur.removed_at IS NULL").
@ -119,7 +120,7 @@ func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.U
func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) { func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
var perms []entities.Permission var perms []entities.Permission
err := r.db.WithContext(ctx). err := r.b.WithContext(ctx).
Table("permissions as p"). Table("permissions as p").
Select("DISTINCT p.*"). Select("DISTINCT p.*").
Joins("JOIN role_permissions rp ON rp.permission_id = p.id"). Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
@ -129,13 +130,72 @@ func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID
return perms, err return perms, err
} }
func (r *UserRepositoryImpl) GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error) { func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error) {
var positions []entities.Position var departments []entities.Department
err := r.db.WithContext(ctx). err := r.b.WithContext(ctx).
Table("positions as p"). Table("departments as d").
Select("p.*"). Select("d.*").
Joins("JOIN user_position up ON up.position_id = p.id AND up.removed_at IS NULL"). Joins("JOIN user_department ud ON ud.department_id = d.id AND ud.removed_at IS NULL").
Where("up.user_id = ?", userID). Where("ud.user_id = ?", userID).
Find(&positions).Error Find(&departments).Error
return positions, err return departments, err
}
// GetRolesByUserIDs returns roles per user for a batch of user IDs
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
}
// fetch pairs user_id, role
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
}
// ListWithFilters supports name search and filtering by role code
func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
var users []*entities.User
var total int64
q := r.b.WithContext(ctx).Table("users").Model(&entities.User{})
if search != nil && *search != "" {
like := "%" + *search + "%"
q = q.Where("users.name ILIKE ?", like)
}
if isActive != nil {
q = q.Where("users.is_active = ?", *isActive)
}
if roleCode != nil && *roleCode != "" {
q = q.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)
}
if err := q.Distinct("users.id").Count(&total).Error; err != nil {
return nil, 0, err
}
if err := q.Select("users.*").Distinct("users.id").Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
} }

View File

@ -55,6 +55,7 @@ func (r *Router) Init() *gin.Engine {
middleware.Recover(), middleware.Recover(),
middleware.HTTPStatLogger(), middleware.HTTPStatLogger(),
middleware.PopulateContext(), middleware.PopulateContext(),
middleware.CORS(),
) )
r.addAppRoutes(engine) r.addAppRoutes(engine)
@ -76,7 +77,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
users := v1.Group("/users") users := v1.Group("/users")
users.Use(r.authMiddleware.RequireAuth()) users.Use(r.authMiddleware.RequireAuth())
{ {
users.GET("", r.authMiddleware.RequirePermissions("user.view"), r.userHandler.ListUsers) users.GET("", r.authMiddleware.RequirePermissions("user.read"), r.userHandler.ListUsers)
users.GET("/profile", r.userHandler.GetProfile) users.GET("/profile", r.userHandler.GetProfile)
users.PUT("/profile", r.userHandler.UpdateProfile) users.PUT("/profile", r.userHandler.UpdateProfile)
users.PUT(":id/password", r.userHandler.ChangePassword) users.PUT(":id/password", r.userHandler.ChangePassword)

View File

@ -56,10 +56,9 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
return nil, fmt.Errorf("invalid credentials") return nil, fmt.Errorf("invalid credentials")
} }
// fetch roles, permissions, positions for response and token
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID) roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID) permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
positions, _ := s.userProcessor.GetUserPositions(ctx, userResponse.ID) departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes) token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
if err != nil { if err != nil {
@ -72,7 +71,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
User: *userResponse, User: *userResponse,
Roles: roles, Roles: roles,
Permissions: permCodes, Permissions: permCodes,
Positions: positions, Departments: departments,
}, nil }, nil
} }
@ -116,14 +115,14 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
return nil, fmt.Errorf("failed to generate token: %w", err) return nil, fmt.Errorf("failed to generate token: %w", err)
} }
positions, _ := s.userProcessor.GetUserPositions(ctx, userResponse.ID) departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
return &contract.LoginResponse{ return &contract.LoginResponse{
Token: newToken, Token: newToken,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
User: *userResponse, User: *userResponse,
Roles: roles, Roles: roles,
Permissions: permCodes, Permissions: permCodes,
Positions: positions, Departments: departments,
}, nil }, nil
} }

View File

@ -14,14 +14,16 @@ type UserProcessor interface {
DeleteUser(ctx context.Context, id uuid.UUID) error DeleteUser(ctx context.Context, id uuid.UUID) error
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
ListUsers(ctx context.Context, page, limit int) ([]contract.UserResponse, int, error)
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error) GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error)
GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error) GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error)
GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error)
GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error)
UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
// New optimized listing
ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error)
} }

View File

@ -56,7 +56,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers
limit = 10 limit = 10
} }
userResponses, totalCount, err := s.userProcessor.ListUsers(ctx, page, limit) userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -72,7 +72,14 @@ func (s *UserServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID,
} }
func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) { func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
return s.userProcessor.GetUserProfile(ctx, userID) prof, err := s.userProcessor.GetUserProfile(ctx, userID)
if err != nil {
return nil, err
}
if roles, err := s.userProcessor.GetUserRoles(ctx, userID); err == nil {
prof.Roles = roles
}
return prof, nil
} }
func (s *UserServiceImpl) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) { func (s *UserServiceImpl) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) {

View File

@ -78,13 +78,13 @@ func RolesToContract(roles []entities.Role) []contract.RoleResponse {
return res return res
} }
func PositionsToContract(positions []entities.Position) []contract.PositionResponse { func DepartmentsToContract(positions []entities.Department) []contract.DepartmentResponse {
if positions == nil { if positions == nil {
return nil return nil
} }
res := make([]contract.PositionResponse, 0, len(positions)) res := make([]contract.DepartmentResponse, 0, len(positions))
for _, p := range positions { for _, p := range positions {
res = append(res, contract.PositionResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path}) res = append(res, contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path})
} }
return res return res
} }

View File

@ -37,14 +37,18 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
if user == nil { if user == nil {
return nil return nil
} }
return &contract.UserResponse{ resp := &contract.UserResponse{
ID: user.ID, ID: user.ID,
Name: user.Name, Name: user.Profile.FullName,
Email: user.Email, Email: user.Email,
IsActive: user.IsActive, IsActive: user.IsActive,
CreatedAt: user.CreatedAt, CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt, UpdatedAt: user.UpdatedAt,
} }
if user.Profile != nil {
resp.Profile = ProfileEntityToContract(user.Profile)
}
return resp
} }
func EntitiesToContracts(users []*entities.User) []contract.UserResponse { func EntitiesToContracts(users []*entities.User) []contract.UserResponse {

View File

@ -1,18 +1,5 @@
BEGIN; BEGIN;
-- =========================
-- Departments (as requested)
-- =========================
-- Root org namespace is "eslogad" in ltree path
INSERT INTO departments (name, code, path) VALUES
('RENBINMINLOG', 'renbinminlog', 'eslogad.renbinminlog'),
('FASKON BMN', 'faskon_bmn', 'eslogad.faskon_bmn'),
('BEKPALKES', 'bekpalkes', 'eslogad.bekpalkes')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
path = EXCLUDED.path,
updated_at = CURRENT_TIMESTAMP;
-- ========================= -- =========================
-- Positions (hierarchy) -- Positions (hierarchy)
-- ========================= -- =========================
@ -20,80 +7,80 @@ INSERT INTO departments (name, code, path) VALUES
-- - superadmin is a separate root -- - superadmin is a separate root
-- - eslogad.aslog is head; waaslog_* under aslog -- - eslogad.aslog is head; waaslog_* under aslog
-- - paban_* under each waaslog_*; pabandya_* under its paban_* -- - paban_* under each waaslog_*; pabandya_* under its paban_*
INSERT INTO positions (name, code, path) VALUES INSERT INTO departments (name, code, path) VALUES
-- ROOTS -- ROOTS
('SUPERADMIN', 'superadmin', 'superadmin'), ('SUPERADMIN', 'superadmin', 'superadmin'),
('ASLOG', 'aslog', 'eslogad.aslog'), ('ASLOG', 'aslog', 'eslogad.aslog'),
-- WAASLOG under ASLOG -- WAASLOG under ASLOG
('WAASLOG RENBINMINLOG', 'waaslogrenbinminlog', 'eslogad.aslog.waaslog_renbinminlog'), ('WAASLOG RENBINMINLOG', 'waaslogrenbinminlog', 'eslogad.aslog.waaslog_renbinminlog'),
('WAASLOG FASKON BMN', 'waaslogfaskonbmn', 'eslogad.aslog.waaslog_faskon_bmn'), ('WAASLOG FASKON BMN', 'waaslogfaskonbmn', 'eslogad.aslog.waaslog_faskon_bmn'),
('WAASLOG BEKPALKES', 'waaslogbekpalkes', 'eslogad.aslog.waaslog_bekpalkes'), ('WAASLOG BEKPALKES', 'waaslogbekpalkes', 'eslogad.aslog.waaslog_bekpalkes'),
-- Other posts directly under ASLOG -- Other posts directly under ASLOG
('KADISADAAD', 'kadisadaad', 'eslogad.aslog.kadisadaad'), ('KADISADAAD', 'kadisadaad', 'eslogad.aslog.kadisadaad'),
('KATUUD', 'katuud', 'eslogad.aslog.katuud'), ('KATUUD', 'katuud', 'eslogad.aslog.katuud'),
('SPRI', 'spri', 'eslogad.aslog.spri'), ('SPRI', 'spri', 'eslogad.aslog.spri'),
-- PABAN under WAASLOG RENBINMINLOG -- PABAN under WAASLOG RENBINMINLOG
('PABAN I/REN', 'paban-I-ren', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren'), ('PABAN I/REN', 'paban-I-ren', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren'),
('PABAN II/BINMINLOG', 'paban-II-binminlog', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog'), ('PABAN II/BINMINLOG', 'paban-II-binminlog', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog'),
-- PABAN under WAASLOG FASKON BMN -- PABAN under WAASLOG FASKON BMN
('PABAN III/FASKON', 'paban-III-faskon', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon'), ('PABAN III/FASKON', 'paban-III-faskon', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon'),
('PABAN IV/BMN', 'paban-iv-bmn', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn'), ('PABAN IV/BMN', 'paban-iv-bmn', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn'),
-- PABAN under WAASLOG BEKPALKES -- PABAN under WAASLOG BEKPALKES
('PABAN V/BEK', 'paban-v-bek', 'eslogad.aslog.waaslog_bekpalkes.paban_V_bek'), ('PABAN V/BEK', 'paban-v-bek', 'eslogad.aslog.waaslog_bekpalkes.paban_V_bek'),
('PABAN VI/ALPAL', 'paban-vi-alpal', 'eslogad.aslog.waaslog_bekpalkes.paban_VI_alpal'), ('PABAN VI/ALPAL', 'paban-vi-alpal', 'eslogad.aslog.waaslog_bekpalkes.paban_VI_alpal'),
('PABAN VII/KES', 'paban-vii-kes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes'), ('PABAN VII/KES', 'paban-vii-kes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes'),
-- PABANDYA under PABAN I/REN -- PABANDYA under PABAN I/REN
('PABANDYA 1 / RENPROGGAR', 'pabandya-1-renproggar', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_1_renproggar'), ('PABANDYA 1 / RENPROGGAR', 'pabandya-1-renproggar', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_1_renproggar'),
('PABANDYA 2 / DALWASGAR', 'pabandya-2-dalwasgar', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_2_dalwasgar'), ('PABANDYA 2 / DALWASGAR', 'pabandya-2-dalwasgar', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_2_dalwasgar'),
('PABANDYA 3 / ANEVDATA', 'pabandya-3-anevdata', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_3_anevdata'), ('PABANDYA 3 / ANEVDATA', 'pabandya-3-anevdata', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_3_anevdata'),
-- PABANDYA under PABAN II/BINMINLOG -- PABANDYA under PABAN II/BINMINLOG
('PABANDYA 1 / MINLOG', 'pabandya-1-minlog', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_1_minlog'), ('PABANDYA 1 / MINLOG', 'pabandya-1-minlog', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_1_minlog'),
('PABANDYA 2 / HIBAHKOD', 'pabandya-2-hibahkod', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_2_hibahkod'), ('PABANDYA 2 / HIBAHKOD', 'pabandya-2-hibahkod', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_2_hibahkod'),
('PABANDYA 3 / PUSMAT', 'pabandya-3-pusmat', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_3_pusmat'), ('PABANDYA 3 / PUSMAT', 'pabandya-3-pusmat', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_3_pusmat'),
-- PABANDYA under PABAN IV/BMN -- PABANDYA under PABAN IV/BMN
('PABANDYA 1 / TANAH', 'pabandya-1-tanah', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_1_tanah'), ('PABANDYA 1 / TANAH', 'pabandya-1-tanah', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_1_tanah'),
('PABANDYA 2 / PANGKALAN KONSTRUKSI','pabandya-2-pangkalankonstruksi','eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_2_pangkalan_konstruksi'), ('PABANDYA 2 / PANGKALAN KONSTRUKSI','pabandya-2-pangkalankonstruksi','eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_2_pangkalan_konstruksi'),
('PABANDYA 3 / FASMATZI', 'pabandya-3-fasmatzi', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_fasmatzi'), ('PABANDYA 3 / FASMATZI', 'pabandya-3-fasmatzi', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_fasmatzi'),
-- PABANDYA under PABAN IV/BMN (AKUN group) -- PABANDYA under PABAN IV/BMN (AKUN group)
('PABANDYA 1 / AKUN BB', 'pabandya-1-akunbb', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_1_akun_bb'), ('PABANDYA 1 / AKUN BB', 'pabandya-1-akunbb', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_1_akun_bb'),
('PABANDYA 2 / AKUN BTB', 'pabandya-2-akunbtb', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_2_akun_btb'), ('PABANDYA 2 / AKUN BTB', 'pabandya-2-akunbtb', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_2_akun_btb'),
('PABANDYA 3 / SISFO BMN DAN UAKPB-KP','pabandya-3-sisfo-bmn-uakpbkp','eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_sisfo_bmn_uakpb_kp'), ('PABANDYA 3 / SISFO BMN DAN UAKPB-KP','pabandya-3-sisfo-bmn-uakpbkp','eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_sisfo_bmn_uakpb_kp'),
-- PABANDYA under PABAN III/FASKON -- PABANDYA under PABAN III/FASKON
('PABANDYA 1 / JATOPTIKMU', 'pabandya-1-jatoptikmu', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_1_jatoptikmu'), ('PABANDYA 1 / JATOPTIKMU', 'pabandya-1-jatoptikmu', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_1_jatoptikmu'),
('PABANDYA 2 / RANTEKMEK', 'pabandya-2-rantekmek', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_2_rantekmek'), ('PABANDYA 2 / RANTEKMEK', 'pabandya-2-rantekmek', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_2_rantekmek'),
('PABANDYA 3 / ALHUBTOPPALSUS', 'pabandya-3-alhubtoppalsus', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_3_alhubtoppalsus'), ('PABANDYA 3 / ALHUBTOPPALSUS', 'pabandya-3-alhubtoppalsus', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_3_alhubtoppalsus'),
('PABANDYA 4 / PESUD', 'pabandya-4-pesud', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_4_pesud'), ('PABANDYA 4 / PESUD', 'pabandya-4-pesud', 'eslogad.aslog.waaslog_faskon_bmn.paban_III_faskon.pabandya_4_pesud'),
-- PABANDYA under PABAN VII/KES -- PABANDYA under PABAN VII/KES
('PABANDYA 1 / BEKKES', 'pabandya-1-bekkes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes.pabandya_1_bekkes'), ('PABANDYA 1 / BEKKES', 'pabandya-1-bekkes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes.pabandya_1_bekkes'),
('PABANDYA 2 / ALKES', 'pabandya-2-alkes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes.pabandya_2_alkes') ('PABANDYA 2 / ALKES', 'pabandya-2-alkes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes.pabandya_2_alkes')
ON CONFLICT (code) DO UPDATE ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
path = EXCLUDED.path, path = EXCLUDED.path,
updated_at = CURRENT_TIMESTAMP; updated_at = CURRENT_TIMESTAMP;
-- ========================= -- =========================
-- SUPERADMIN role (minimal) -- SUPERADMIN role (minimal)
-- ========================= -- =========================
INSERT INTO roles (name, code, description) VALUES INSERT INTO roles (name, code, description) VALUES
('SUPERADMIN', 'superadmin', 'Full system access and management'), ('SUPERADMIN', 'superadmin', 'Full system access and management'),
('ADMIN', 'admin', 'Manage users, letters, and settings within their department'), ('ADMIN', 'admin', 'Manage users, letters, and settings within their department'),
('HEAD', 'head', 'Approve outgoing letters and manage dispositions in their department'), ('HEAD', 'head', 'Approve outgoing letters and manage dispositions in their department'),
('STAFF', 'staff', 'Create letters, process assigned dispositions') ('STAFF', 'staff', 'Create letters, process assigned dispositions')
ON CONFLICT (code) DO UPDATE ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
description = EXCLUDED.description, description = EXCLUDED.description,
updated_at = CURRENT_TIMESTAMP; updated_at = CURRENT_TIMESTAMP;
-- ========================= -- =========================
-- Users (seed 1 superadmin) -- Users (seed 1 superadmin)

View File

@ -0,0 +1,4 @@
BEGIN;
DROP TRIGGER IF EXISTS trg_app_settings_updated_at ON app_settings;
DROP TABLE IF EXISTS app_settings;
COMMIT;

View File

@ -0,0 +1,21 @@
BEGIN;
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_app_settings_updated_at
BEFORE UPDATE ON app_settings
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
INSERT INTO app_settings(key, value)
VALUES
('INCOMING_LETTER_PREFIX', '{"value": "ESLI"}'::jsonb),
('INCOMING_LETTER_SEQUENCE', '{"value": 0}'::jsonb),
('INCOMING_LETTER_RECIPIENTS', '{"department_codes": ["aslog"]}'::jsonb)
ON CONFLICT (key) DO NOTHING;
COMMIT;