Init
This commit is contained in:
parent
61d6eed373
commit
de60983e4e
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -44,7 +44,7 @@ type LoginResponse struct {
|
||||
User UserResponse `json:"user"`
|
||||
Roles []RoleResponse `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Positions []PositionResponse `json:"positions"`
|
||||
Departments []DepartmentResponse `json:"departments"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
@ -54,6 +54,8 @@ type UserResponse struct {
|
||||
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 {
|
||||
|
||||
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" }
|
||||
@ -47,6 +47,7 @@ type User struct {
|
||||
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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,7 +7,7 @@ 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'),
|
||||
@ -77,7 +64,7 @@ INSERT INTO positions (name, code, path) VALUES
|
||||
-- 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
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
path = EXCLUDED.path,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
@ -90,7 +77,7 @@ INSERT INTO roles (name, code, description) VALUES
|
||||
('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
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
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