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
dispositionNoteRepo *repository.DispositionNoteRepository
letterDiscussionRepo *repository.LetterDiscussionRepository
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
}
func (a *App) initRepositories() *repositories {
@ -141,6 +145,10 @@ func (a *App) initRepositories() *repositories {
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
dispositionNoteRepo: repository.NewDispositionNoteRepository(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)
return &processors{
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,
}
}

View File

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

View File

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

View File

@ -166,10 +166,24 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
}
}
var roleParam *string
if role := c.Query("role"); role != "" {
roleParam = &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 isActive, err := strconv.ParseBool(isActiveStr); err == nil {
req.IsActive = &isActive

View File

@ -2,6 +2,7 @@ package processor
import (
"context"
"fmt"
"time"
"eslogad-be/internal/appcontext"
@ -24,16 +25,37 @@ type LetterProcessorImpl struct {
dispositionNoteRepo *repository.DispositionNoteRepository
// discussion repo
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 {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo}
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, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo}
}
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
var result *contract.IncomingLetterResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
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{
ReferenceNumber: req.ReferenceNumber,
Subject: req.Subject,
@ -45,26 +67,63 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
Status: entities.LetterIncomingStatusNew,
CreatedBy: userID,
}
entity.LetterNumber = letterNumber
if err := p.letterRepo.Create(txCtx, entity); err != nil {
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 {
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
}
}
attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments))
for _, a := range req.Attachments {
attachments = append(attachments, entities.LetterIncomingAttachment{
LetterID: entity.ID,
FileURL: a.FileURL,
FileName: a.FileName,
FileType: a.FileType,
UploadedBy: &userID,
})
attachments = append(attachments, entities.LetterIncomingAttachment{LetterID: entity.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID})
}
if len(attachments) > 0 {
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)
return nil
})
if err != nil {
return nil, err
}
@ -267,7 +325,7 @@ func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uui
return err
}
if p.activity != nil {
action := "discussion.created"
action := "reference_numberdiscussion.created"
tgt := "discussion"
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 {

View File

@ -110,8 +110,13 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
return transformer.EntityToContract(user), nil
resp := transformer.EntityToContract(user)
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) {
@ -123,17 +128,35 @@ func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*
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
filters := map[string]interface{}{}
users, totalCount, err := p.userRepo.List(ctx, filters, limit, offset)
users, totalCount, err := p.userRepo.ListWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get users: %w", err)
}
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
}
@ -219,12 +242,12 @@ func (p *UserProcessorImpl) GetUserPermissionCodes(ctx context.Context, userID u
return codes, nil
}
func (p *UserProcessorImpl) GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error) {
positions, err := p.userRepo.GetPositionsByUserID(ctx, userID)
func (p *UserProcessorImpl) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error) {
departments, err := p.userRepo.GetDepartmentsByUserID(ctx, userID)
if err != nil {
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) {

View File

@ -3,6 +3,7 @@ package processor
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
)
@ -21,5 +22,9 @@ type UserRepository interface {
GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, 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).
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
}
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 {
db *gorm.DB
b *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepositoryImpl {
return &UserRepositoryImpl{
db: db,
b: db,
}
}
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) {
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 {
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) {
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 {
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) {
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
}
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
var users []*entities.User
err := r.db.WithContext(ctx).
err := r.b.WithContext(ctx).
Where(" is_active = ?", organizationID, true).
Preload("Profile").
Find(&users).Error
return users, err
}
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 {
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 {
return r.db.WithContext(ctx).Model(&entities.User{}).
return r.b.WithContext(ctx).Model(&entities.User{}).
Where("id = ?", id).
Update("password_hash", passwordHash).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).
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 total int64
query := r.db.WithContext(ctx).Model(&entities.User{})
query := r.b.WithContext(ctx).Model(&entities.User{})
for key, value := range filters {
query = query.Where(key+" = ?", value)
@ -89,13 +90,13 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
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
}
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.User{})
query := r.b.WithContext(ctx).Model(&entities.User{})
for key, value := range filters {
query = query.Where(key+" = ?", value)
@ -108,7 +109,7 @@ func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]inter
// RBAC helpers
func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
var roles []entities.Role
err := r.db.WithContext(ctx).
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").
@ -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) {
var perms []entities.Permission
err := r.db.WithContext(ctx).
err := r.b.WithContext(ctx).
Table("permissions as p").
Select("DISTINCT p.*").
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
}
func (r *UserRepositoryImpl) GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error) {
var positions []entities.Position
err := r.db.WithContext(ctx).
Table("positions as p").
Select("p.*").
Joins("JOIN user_position up ON up.position_id = p.id AND up.removed_at IS NULL").
Where("up.user_id = ?", userID).
Find(&positions).Error
return positions, 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
}
// 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.HTTPStatLogger(),
middleware.PopulateContext(),
middleware.CORS(),
)
r.addAppRoutes(engine)
@ -76,7 +77,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
users := v1.Group("/users")
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.PUT("/profile", r.userHandler.UpdateProfile)
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")
}
// fetch roles, permissions, positions for response and token
roles, _ := s.userProcessor.GetUserRoles(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)
if err != nil {
@ -72,7 +71,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
User: *userResponse,
Roles: roles,
Permissions: permCodes,
Positions: positions,
Departments: departments,
}, nil
}
@ -116,14 +115,14 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
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{
Token: newToken,
ExpiresAt: expiresAt,
User: *userResponse,
Roles: roles,
Permissions: permCodes,
Positions: positions,
Departments: departments,
}, nil
}

View File

@ -14,14 +14,16 @@ type UserProcessor interface {
DeleteUser(ctx context.Context, id uuid.UUID) error
GetUserByID(ctx context.Context, id uuid.UUID) (*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)
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, 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)
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
}
userResponses, totalCount, err := s.userProcessor.ListUsers(ctx, page, limit)
userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req)
if err != nil {
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) {
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) {

View File

@ -78,13 +78,13 @@ func RolesToContract(roles []entities.Role) []contract.RoleResponse {
return res
}
func PositionsToContract(positions []entities.Position) []contract.PositionResponse {
func DepartmentsToContract(positions []entities.Department) []contract.DepartmentResponse {
if positions == nil {
return nil
}
res := make([]contract.PositionResponse, 0, len(positions))
res := make([]contract.DepartmentResponse, 0, len(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
}

View File

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

View File

@ -1,18 +1,5 @@
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)
-- =========================
@ -20,80 +7,80 @@ INSERT INTO departments (name, code, path) VALUES
-- - superadmin is a separate root
-- - eslogad.aslog is head; waaslog_* under aslog
-- - paban_* under each waaslog_*; pabandya_* under its paban_*
INSERT INTO positions (name, code, path) VALUES
INSERT INTO departments (name, code, path) VALUES
-- ROOTS
('SUPERADMIN', 'superadmin', 'superadmin'),
('ASLOG', 'aslog', 'eslogad.aslog'),
('SUPERADMIN', 'superadmin', 'superadmin'),
('ASLOG', 'aslog', 'eslogad.aslog'),
-- WAASLOG under ASLOG
('WAASLOG RENBINMINLOG', 'waaslogrenbinminlog', 'eslogad.aslog.waaslog_renbinminlog'),
('WAASLOG FASKON BMN', 'waaslogfaskonbmn', 'eslogad.aslog.waaslog_faskon_bmn'),
('WAASLOG BEKPALKES', 'waaslogbekpalkes', 'eslogad.aslog.waaslog_bekpalkes'),
-- WAASLOG under ASLOG
('WAASLOG RENBINMINLOG', 'waaslogrenbinminlog', 'eslogad.aslog.waaslog_renbinminlog'),
('WAASLOG FASKON BMN', 'waaslogfaskonbmn', 'eslogad.aslog.waaslog_faskon_bmn'),
('WAASLOG BEKPALKES', 'waaslogbekpalkes', 'eslogad.aslog.waaslog_bekpalkes'),
-- Other posts directly under ASLOG
('KADISADAAD', 'kadisadaad', 'eslogad.aslog.kadisadaad'),
('KATUUD', 'katuud', 'eslogad.aslog.katuud'),
('SPRI', 'spri', 'eslogad.aslog.spri'),
-- Other posts directly under ASLOG
('KADISADAAD', 'kadisadaad', 'eslogad.aslog.kadisadaad'),
('KATUUD', 'katuud', 'eslogad.aslog.katuud'),
('SPRI', 'spri', 'eslogad.aslog.spri'),
-- PABAN under WAASLOG RENBINMINLOG
('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 under WAASLOG RENBINMINLOG
('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 under WAASLOG FASKON BMN
('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 under WAASLOG FASKON BMN
('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 under WAASLOG BEKPALKES
('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 VII/KES', 'paban-vii-kes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes'),
-- PABAN under WAASLOG BEKPALKES
('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 VII/KES', 'paban-vii-kes', 'eslogad.aslog.waaslog_bekpalkes.paban_VII_kes'),
-- PABANDYA under PABAN I/REN
('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 3 / ANEVDATA', 'pabandya-3-anevdata', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_3_anevdata'),
-- PABANDYA under PABAN I/REN
('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 3 / ANEVDATA', 'pabandya-3-anevdata', 'eslogad.aslog.waaslog_renbinminlog.paban_I_ren.pabandya_3_anevdata'),
-- PABANDYA under PABAN II/BINMINLOG
('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 3 / PUSMAT', 'pabandya-3-pusmat', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_3_pusmat'),
-- PABANDYA under PABAN II/BINMINLOG
('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 3 / PUSMAT', 'pabandya-3-pusmat', 'eslogad.aslog.waaslog_renbinminlog.paban_II_binminlog.pabandya_3_pusmat'),
-- PABANDYA under PABAN IV/BMN
('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 3 / FASMATZI', 'pabandya-3-fasmatzi', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_fasmatzi'),
-- PABANDYA under PABAN IV/BMN
('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 3 / FASMATZI', 'pabandya-3-fasmatzi', 'eslogad.aslog.waaslog_faskon_bmn.paban_IV_bmn.pabandya_3_fasmatzi'),
-- 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 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 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 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 under PABAN III/FASKON
('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 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 under PABAN III/FASKON
('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 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 under PABAN VII/KES
('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')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
path = EXCLUDED.path,
updated_at = CURRENT_TIMESTAMP;
-- PABANDYA under PABAN VII/KES
('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')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
path = EXCLUDED.path,
updated_at = CURRENT_TIMESTAMP;
-- =========================
-- SUPERADMIN role (minimal)
-- =========================
INSERT INTO roles (name, code, description) VALUES
('SUPERADMIN', 'superadmin', 'Full system access and management'),
('ADMIN', 'admin', 'Manage users, letters, and settings within their department'),
('HEAD', 'head', 'Approve outgoing letters and manage dispositions in their department'),
('STAFF', 'staff', 'Create letters, process assigned dispositions')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
description = EXCLUDED.description,
updated_at = CURRENT_TIMESTAMP;
('SUPERADMIN', 'superadmin', 'Full system access and management'),
('ADMIN', 'admin', 'Manage users, letters, and settings within their department'),
('HEAD', 'head', 'Approve outgoing letters and manage dispositions in their department'),
('STAFF', 'staff', 'Create letters, process assigned dispositions')
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name,
description = EXCLUDED.description,
updated_at = CURRENT_TIMESTAMP;
-- =========================
-- 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;