275 lines
9.5 KiB
Go
275 lines
9.5 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"time"
|
|
|
|
"botpontos/internal/adapters/postgres"
|
|
"botpontos/internal/app"
|
|
"botpontos/internal/domain"
|
|
)
|
|
|
|
// Formatos JSON da API. Os tipos TypeScript equivalentes ficam em web/lib/types.ts.
|
|
|
|
type userJSON struct {
|
|
ID int64 `json:"id"`
|
|
TelegramChatID int64 `json:"telegram_chat_id"`
|
|
DisplayName string `json:"display_name"`
|
|
Status string `json:"status"`
|
|
IsBotAdmin bool `json:"is_bot_admin"`
|
|
NotificationsEnabled bool `json:"notifications_enabled"`
|
|
PausedReason *string `json:"paused_reason"`
|
|
Timezone string `json:"timezone"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
func toUser(u domain.User) userJSON {
|
|
var reason *string
|
|
if u.PausedReason != nil {
|
|
v := string(*u.PausedReason)
|
|
reason = &v
|
|
}
|
|
return userJSON{
|
|
ID: u.ID, TelegramChatID: u.TelegramChatID, DisplayName: u.DisplayName, Status: string(u.Status),
|
|
IsBotAdmin: u.IsBotAdmin, NotificationsEnabled: u.NotificationsEnabled, PausedReason: reason,
|
|
Timezone: u.Timezone, CreatedAt: u.CreatedAt,
|
|
}
|
|
}
|
|
|
|
type userSummaryJSON struct {
|
|
userJSON
|
|
HasCredential bool `json:"has_credential"`
|
|
CredentialValidated bool `json:"credential_validated"`
|
|
ConsecutiveFailures int `json:"consecutive_failures"`
|
|
CustomWindow bool `json:"custom_window"`
|
|
}
|
|
|
|
func toUserSummary(s postgres.UserSummary) userSummaryJSON {
|
|
return userSummaryJSON{
|
|
userJSON: toUser(s.User), HasCredential: s.HasCredential, CredentialValidated: s.CredentialValidated,
|
|
ConsecutiveFailures: s.ConsecutiveFailures, CustomWindow: s.CustomWindow,
|
|
}
|
|
}
|
|
|
|
type windowJSON struct {
|
|
QueryWindowStart string `json:"query_window_start"`
|
|
QueryWindowEnd string `json:"query_window_end"`
|
|
DayCheckDeadline string `json:"day_check_deadline"`
|
|
NoPointsRecheckUntil string `json:"no_points_recheck_until"`
|
|
}
|
|
|
|
func toWindow(w domain.QueryWindow) windowJSON {
|
|
return windowJSON{
|
|
QueryWindowStart: w.Start.String(), QueryWindowEnd: w.End.String(),
|
|
DayCheckDeadline: w.DayCheckDeadline.String(), NoPointsRecheckUntil: w.NoPointsRecheckUntil.String(),
|
|
}
|
|
}
|
|
|
|
// parse converte para o domínio, com erros por campo.
|
|
func (w windowJSON) parse(prefix string) (domain.QueryWindow, error) {
|
|
var out domain.QueryWindow
|
|
var errs domain.ValidationErrors
|
|
fields := []struct {
|
|
name string
|
|
value string
|
|
target *domain.ClockTime
|
|
}{
|
|
{"query_window_start", w.QueryWindowStart, &out.Start},
|
|
{"query_window_end", w.QueryWindowEnd, &out.End},
|
|
{"day_check_deadline", w.DayCheckDeadline, &out.DayCheckDeadline},
|
|
{"no_points_recheck_until", w.NoPointsRecheckUntil, &out.NoPointsRecheckUntil},
|
|
}
|
|
for _, f := range fields {
|
|
c, err := domain.ParseClock(f.value)
|
|
if err != nil {
|
|
errs.Add(prefix+f.name, "horário inválido (use HH:MM)")
|
|
continue
|
|
}
|
|
*f.target = c
|
|
}
|
|
return out, errs.OrNil()
|
|
}
|
|
|
|
type scheduleJSON struct {
|
|
LunchMinutes int `json:"lunch_minutes"`
|
|
DailyWorkMinutes int `json:"daily_work_minutes"`
|
|
Entrada2LeadMinutes int `json:"entrada2_lead_minutes"`
|
|
Saida2LeadMinutes int `json:"saida2_lead_minutes"`
|
|
Window *windowJSON `json:"window"`
|
|
}
|
|
|
|
func toSchedule(s domain.WorkSchedule) scheduleJSON {
|
|
out := scheduleJSON{
|
|
LunchMinutes: s.LunchMinutes, DailyWorkMinutes: s.DailyWorkMinutes,
|
|
Entrada2LeadMinutes: s.Entrada2LeadMinutes, Saida2LeadMinutes: s.Saida2LeadMinutes,
|
|
}
|
|
if s.Window != nil {
|
|
w := toWindow(*s.Window)
|
|
out.Window = &w
|
|
}
|
|
return out
|
|
}
|
|
|
|
type credentialJSON struct {
|
|
APIEmail string `json:"api_email"`
|
|
Validated bool `json:"validated"`
|
|
LastValidatedAt *time.Time `json:"last_validated_at"`
|
|
LastError *string `json:"last_error"`
|
|
ConsecutiveFailures int `json:"consecutive_failures"`
|
|
LastFailureAt *time.Time `json:"last_failure_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type dayJSON struct {
|
|
Day string `json:"day"`
|
|
Status string `json:"status"`
|
|
ZeroPointChecks int `json:"zero_point_checks"`
|
|
LastPointCount int `json:"last_point_count"`
|
|
NextCheckAt *time.Time `json:"next_check_at"`
|
|
ResumedBy *string `json:"resumed_by"`
|
|
}
|
|
|
|
func toDay(d domain.UserDay) dayJSON {
|
|
var resumed *string
|
|
if d.ResumedBy != nil {
|
|
v := string(*d.ResumedBy)
|
|
resumed = &v
|
|
}
|
|
return dayJSON{
|
|
Day: d.Day, Status: string(d.Status), ZeroPointChecks: d.ZeroPointChecks,
|
|
LastPointCount: d.LastPointCount, NextCheckAt: d.NextCheckAt, ResumedBy: resumed,
|
|
}
|
|
}
|
|
|
|
type userDetailJSON struct {
|
|
User userJSON `json:"user"`
|
|
Schedule scheduleJSON `json:"schedule"`
|
|
Credential *credentialJSON `json:"credential"`
|
|
Today dayJSON `json:"today"`
|
|
EffectiveWindow windowJSON `json:"effective_window"`
|
|
CustomWindow bool `json:"custom_window"`
|
|
}
|
|
|
|
func toUserDetail(d app.UserDetail) userDetailJSON {
|
|
out := userDetailJSON{
|
|
User: toUser(d.User), Schedule: toSchedule(d.Schedule), Today: toDay(d.Today),
|
|
EffectiveWindow: toWindow(d.Window), CustomWindow: d.CustomWindow,
|
|
}
|
|
if c := d.Credential; c != nil {
|
|
out.Credential = &credentialJSON{
|
|
APIEmail: c.APIEmail, Validated: c.IsValidated(), LastValidatedAt: c.LastValidatedAt,
|
|
LastError: c.LastError, ConsecutiveFailures: c.ConsecutiveFailures, LastFailureAt: c.LastFailureAt,
|
|
UpdatedAt: c.UpdatedAt,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type todayJSON struct {
|
|
Day string `json:"day"`
|
|
Timezone string `json:"timezone"`
|
|
Points []time.Time `json:"points"`
|
|
Invalid bool `json:"invalid"`
|
|
Overtime bool `json:"overtime"`
|
|
Entrada2At *time.Time `json:"entrada2_at"`
|
|
Saida2At *time.Time `json:"saida2_at"`
|
|
Saida2Error string `json:"saida2_error,omitempty"`
|
|
DayState dayJSON `json:"day_state"`
|
|
Window windowJSON `json:"window"`
|
|
CustomWindow bool `json:"custom_window"`
|
|
}
|
|
|
|
func toToday(v app.TodayView) todayJSON {
|
|
points := v.Points
|
|
if points == nil {
|
|
points = []time.Time{}
|
|
}
|
|
return todayJSON{
|
|
Day: v.Day, Timezone: v.User.Timezone, Points: points, Invalid: v.Invalid, Overtime: v.Overtime,
|
|
Entrada2At: v.Entrada2At, Saida2At: v.Saida2At, Saida2Error: v.Saida2Error,
|
|
DayState: toDay(v.DayState), Window: toWindow(v.Window), CustomWindow: v.CustomWindow,
|
|
}
|
|
}
|
|
|
|
type notificationJSON struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"user_id"`
|
|
UserName string `json:"user_name,omitempty"`
|
|
ReferenceDate string `json:"reference_date"`
|
|
Type string `json:"type"`
|
|
ScheduledAt time.Time `json:"scheduled_at"`
|
|
PredictedAt time.Time `json:"predicted_at"`
|
|
Status string `json:"status"`
|
|
Attempts int `json:"attempts"`
|
|
SentAt *time.Time `json:"sent_at"`
|
|
ErrorMessage *string `json:"error_message"`
|
|
}
|
|
|
|
func toNotification(n domain.Notification, userName string) notificationJSON {
|
|
return notificationJSON{
|
|
ID: n.ID, UserID: n.UserID, UserName: userName, ReferenceDate: n.ReferenceDate, Type: string(n.Type),
|
|
ScheduledAt: n.ScheduledAt, PredictedAt: n.PredictedAt, Status: string(n.Status), Attempts: n.Attempts,
|
|
SentAt: n.SentAt, ErrorMessage: n.ErrorMessage,
|
|
}
|
|
}
|
|
|
|
type auditJSON struct {
|
|
ID int64 `json:"id"`
|
|
OccurredAt time.Time `json:"occurred_at"`
|
|
Source string `json:"source"`
|
|
ActorUserID *int64 `json:"actor_user_id"`
|
|
ActorUserName *string `json:"actor_user_name"`
|
|
ActorAccount *int64 `json:"actor_account"`
|
|
ActorUsername *string `json:"actor_username"`
|
|
TargetUserID *int64 `json:"target_user_id"`
|
|
TargetUserName *string `json:"target_user_name"`
|
|
EventType string `json:"event_type"`
|
|
Success bool `json:"success"`
|
|
Details map[string]any `json:"details"`
|
|
}
|
|
|
|
func toAudit(l domain.AuditLog) auditJSON {
|
|
return auditJSON{
|
|
ID: l.ID, OccurredAt: l.OccurredAt, Source: string(l.Source), ActorUserID: l.ActorUserID,
|
|
ActorUserName: l.ActorUserName, ActorAccount: l.ActorAccount, ActorUsername: l.ActorUsername,
|
|
TargetUserID: l.TargetUserID, TargetUserName: l.TargetUserName, EventType: l.EventType,
|
|
Success: l.Success, Details: l.Details,
|
|
}
|
|
}
|
|
|
|
type holidayJSON struct {
|
|
Day string `json:"day"`
|
|
Name string `json:"name"`
|
|
Source string `json:"source"`
|
|
}
|
|
|
|
type accountJSON struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
UserID *int64 `json:"user_id"`
|
|
Disabled bool `json:"disabled"`
|
|
LockedUntil *time.Time `json:"locked_until"`
|
|
LastLoginAt *time.Time `json:"last_login_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
func toAccount(a domain.WebAccount) accountJSON {
|
|
return accountJSON{
|
|
ID: a.ID, Username: a.Username, Role: string(a.Role), UserID: a.UserID, Disabled: a.IsDisabled(),
|
|
LockedUntil: a.LockedUntil, LastLoginAt: a.LastLoginAt, CreatedAt: a.CreatedAt,
|
|
}
|
|
}
|
|
|
|
type dashboardJSON struct {
|
|
UsersByStatus map[string]int `json:"users_by_status"`
|
|
NotificationsEnabled int `json:"notifications_enabled"`
|
|
PausedByAPIFailures int `json:"paused_by_api_failures"`
|
|
TodayByStatus map[string]int `json:"today_by_status"`
|
|
FailedLast7Days int `json:"failed_last_7_days"`
|
|
ExpiredLast7Days int `json:"expired_last_7_days"`
|
|
CustomWindows int `json:"custom_windows"`
|
|
OutageSince *string `json:"outage_since"`
|
|
HolidayFallbackDay *string `json:"holiday_fallback_day"`
|
|
Today string `json:"today"`
|
|
}
|