Init
This commit is contained in:
parent
61d6eed373
commit
de60983e4e
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
18
internal/entities/department.go
Normal file
18
internal/entities/department.go
Normal 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" }
|
||||||
28
internal/entities/letter_incoming_recipient.go
Normal file
28
internal/entities/letter_incoming_recipient.go
Normal 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" }
|
||||||
14
internal/entities/setting.go
Normal file
14
internal/entities/setting.go
Normal 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" }
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
34
internal/repository/user_department_repository.go
Normal file
34
internal/repository/user_department_repository.go
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
4
migrations/000011_settings.down.sql
Normal file
4
migrations/000011_settings.down.sql
Normal 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;
|
||||||
21
migrations/000011_settings.up.sql
Normal file
21
migrations/000011_settings.up.sql
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user