Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
161 lines
5.2 KiB
Go
161 lines
5.2 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// WorkingHourRepo implements GORM repository for WorkingHour.
|
|
type WorkingHourRepo struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewWorkingHourRepo(db *gorm.DB) *WorkingHourRepo {
|
|
return &WorkingHourRepo{db: db}
|
|
}
|
|
|
|
// FindByInbox returns all 7 working hours for an inbox, ordered by day_of_week.
|
|
func (r *WorkingHourRepo) FindByInbox(ctx context.Context, inboxID uint) ([]model.WorkingHour, error) {
|
|
var hours []model.WorkingHour
|
|
err := r.db.WithContext(ctx).Where("inbox_id = ?", inboxID).Order("day_of_week ASC").Find(&hours).Error
|
|
return hours, err
|
|
}
|
|
|
|
// FindByInboxAndDay returns the working hour for a specific inbox and day_of_week.
|
|
func (r *WorkingHourRepo) FindByInboxAndDay(ctx context.Context, inboxID uint, dayOfWeek int) (*model.WorkingHour, error) {
|
|
var wh model.WorkingHour
|
|
err := r.db.WithContext(ctx).Where("inbox_id = ? AND day_of_week = ?", inboxID, dayOfWeek).First(&wh).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &wh, nil
|
|
}
|
|
|
|
// CreateDefaultWorkingHours creates 7 default working hour records for a new inbox.
|
|
// Reference: Chatwoot out_of_offisable.rb — after_create :create_default_working_hours
|
|
// - Sunday(0): closed_all_day
|
|
// - Mon-Fri(1-5): 9:00-17:00
|
|
// - Saturday(6): closed_all_day
|
|
func (r *WorkingHourRepo) CreateDefaultWorkingHours(ctx context.Context, inboxID, accountID uint) error {
|
|
nine := 9
|
|
zero := 0
|
|
seventeen := 17
|
|
|
|
defaults := []model.WorkingHour{
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 0, ClosedAllDay: true, OpenAllDay: false},
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 1, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 2, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 3, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 4, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 5, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
|
|
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 6, ClosedAllDay: true, OpenAllDay: false},
|
|
}
|
|
for i := range defaults {
|
|
if err := r.db.WithContext(ctx).Create(&defaults[i]).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateWorkingHours bulk-updates the weekly schedule for an inbox.
|
|
// Reference: Chatwoot out_of_offisable.rb — update_working_hours(params)
|
|
// - For each param entry, find the existing working_hour by day_of_week and update its fields.
|
|
// - Runs in a transaction.
|
|
func (r *WorkingHourRepo) UpdateWorkingHours(ctx context.Context, inboxID uint, params []WorkingHourUpdateParam) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
for _, p := range params {
|
|
var wh model.WorkingHour
|
|
if err := tx.Where("inbox_id = ? AND day_of_week = ?", inboxID, p.DayOfWeek).First(&wh).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
wh.ClosedAllDay = p.ClosedAllDay
|
|
wh.OpenAllDay = p.OpenAllDay
|
|
wh.OpenHour = p.OpenHour
|
|
wh.OpenMinutes = p.OpenMinutes
|
|
wh.CloseHour = p.CloseHour
|
|
wh.CloseMinutes = p.CloseMinutes
|
|
wh.EnsureOpenAllDayHours()
|
|
|
|
if err := tx.Save(&wh).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WorkingHourUpdateParam represents a single day's update in a bulk update request.
|
|
// Reference: Chatwoot OFFISABLE_ATTRS
|
|
type WorkingHourUpdateParam struct {
|
|
DayOfWeek int `json:"day_of_week"`
|
|
ClosedAllDay bool `json:"closed_all_day"`
|
|
OpenAllDay bool `json:"open_all_day"`
|
|
OpenHour *int `json:"open_hour"`
|
|
OpenMinutes *int `json:"open_minutes"`
|
|
CloseHour *int `json:"close_hour"`
|
|
CloseMinutes *int `json:"close_minutes"`
|
|
}
|
|
|
|
func (p *WorkingHourUpdateParam) UnmarshalJSON(data []byte) error {
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
p.DayOfWeek = intFromAny(raw["day_of_week"])
|
|
p.ClosedAllDay = boolFromAny(raw["closed_all_day"])
|
|
p.OpenAllDay = boolFromAny(raw["open_all_day"])
|
|
p.OpenHour = intPtrFromAny(raw["open_hour"])
|
|
p.OpenMinutes = intPtrFromAny(raw["open_minutes"])
|
|
p.CloseHour = intPtrFromAny(raw["close_hour"])
|
|
p.CloseMinutes = intPtrFromAny(raw["close_minutes"])
|
|
return nil
|
|
}
|
|
|
|
func intFromAny(value interface{}) int {
|
|
if ptr := intPtrFromAny(value); ptr != nil {
|
|
return *ptr
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func intPtrFromAny(value interface{}) *int {
|
|
switch typed := value.(type) {
|
|
case nil:
|
|
return nil
|
|
case float64:
|
|
v := int(typed)
|
|
return &v
|
|
case int:
|
|
return &typed
|
|
case string:
|
|
if typed == "" || typed == "null" {
|
|
return nil
|
|
}
|
|
parsed, err := strconv.Atoi(typed)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &parsed
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func boolFromAny(value interface{}) bool {
|
|
switch typed := value.(type) {
|
|
case bool:
|
|
return typed
|
|
case string:
|
|
parsed, _ := strconv.ParseBool(typed)
|
|
return parsed
|
|
default:
|
|
return false
|
|
}
|
|
}
|