Files
paycheck/app/app.go

215 lines
5.1 KiB
Go

package app
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
goruntime "runtime"
"strings"
"sync"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/xuri/excelize/v2"
"git.dumerain.org/alban/calcul-astreintes/internal/calc"
"git.dumerain.org/alban/calcul-astreintes/internal/importer"
"git.dumerain.org/alban/calcul-astreintes/internal/models"
"git.dumerain.org/alban/calcul-astreintes/internal/pdf"
"git.dumerain.org/alban/calcul-astreintes/internal/profiles"
)
var ymRe = regexp.MustCompile(`^(\d{4})-(\d{2})$|^(\d{2})-(\d{4})$`)
func normalizeYearMonth(sheet string) (string, bool) {
sheet = strings.TrimSpace(sheet)
m := ymRe.FindStringSubmatch(sheet)
if m == nil {
return "", false
}
// YYYY-MM
if m[1] != "" && m[2] != "" {
return fmt.Sprintf("%s-%s", m[1], m[2]), true
}
// MM-YYYY
if m[3] != "" && m[4] != "" {
return fmt.Sprintf("%s-%s", m[4], m[3]), true
}
return "", false
}
type App struct {
ctx context.Context
repo *profiles.Repo
rules *models.GlobalRules
importMu sync.RWMutex
agents []models.Agent
totals map[string]models.AgentTotals
lastImportYearMonth string
lastImportFileBase string
}
func NewApp() *App {
repo, rules, err := profiles.NewEmbeddedRepo()
if err != nil {
panic(err)
}
return &App{
repo: repo,
rules: rules,
totals: map[string]models.AgentTotals{},
}
}
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) ListProfiles() ([]models.ProfileListItem, error) {
return a.repo.List()
}
func (a *App) Calculate(req models.CalculateRequest) (models.CalculateResponse, error) {
profile, err := a.repo.Get(req.ProfileID)
if err != nil {
return models.CalculateResponse{}, err
}
return calc.Compute(req, profile, *a.rules), nil
}
// ImportMonthlyXLSX : import Excel + stockage en mémoire
// ✅ Mois = nom de feuille (YYYY-MM ou MM-YYYY) normalisé en YYYY-MM
func (a *App) ImportMonthlyXLSX() ([]models.Agent, error) {
if a.ctx == nil {
return nil, fmt.Errorf("contexte Wails non initialisé")
}
path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Importer le fichier Excel des astreintes",
Filters: []runtime.FileFilter{
{DisplayName: "Excel (.xlsx)", Pattern: "*.xlsx"},
},
})
if err != nil || path == "" {
return nil, fmt.Errorf("import annulé")
}
// Détection feuille
f, err := excelize.OpenFile(path)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
sheet, err := importer.FindMonthSheet(f)
if err != nil {
return nil, err
}
yearMonth, ok := normalizeYearMonth(sheet)
if !ok {
return nil, fmt.Errorf("nom de feuille invalide pour le mois: %s (attendu YYYY-MM ou MM-YYYY)", sheet)
}
agents, totals, err := importer.ParseAgentsAndTotals(path)
if err != nil {
return nil, err
}
a.importMu.Lock()
a.agents = agents
a.totals = totals
a.lastImportYearMonth = yearMonth // ✅ toujours en YYYY-MM
a.lastImportFileBase = filepath.Base(path)
a.importMu.Unlock()
return agents, nil
}
func (a *App) GetImportedTotals(matricule string) (models.AgentTotals, error) {
a.importMu.RLock()
defer a.importMu.RUnlock()
t, ok := a.totals[matricule]
if !ok {
return models.AgentTotals{}, fmt.Errorf("matricule introuvable: %s", matricule)
}
return t, nil
}
func (a *App) GetLastImportInfo() (map[string]string, error) {
a.importMu.RLock()
defer a.importMu.RUnlock()
return map[string]string{
"yearMonth": a.lastImportYearMonth, // ✅ normalisé
"fileBase": a.lastImportFileBase,
}, nil
}
func (a *App) ExportPDF(payload models.ExportPDFRequest) (models.ExportPDFResponse, error) {
profile, err := a.repo.Get(payload.Request.ProfileID)
if err != nil {
return models.ExportPDFResponse{}, err
}
res := calc.Compute(payload.Request, profile, *a.rules)
defaultName := defaultPDFNameV051(payload.Meta)
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "Exporter le calcul en PDF",
DefaultFilename: defaultName,
Filters: []runtime.FileFilter{
{DisplayName: "PDF (*.pdf)", Pattern: "*.pdf"},
},
})
if err != nil || savePath == "" {
return models.ExportPDFResponse{}, fmt.Errorf("export annulé")
}
if filepath.Ext(strings.ToLower(savePath)) != ".pdf" {
savePath += ".pdf"
}
data, err := pdf.BuildPDF(payload.Meta, profile.Label, payload.Request, res)
if err != nil {
return models.ExportPDFResponse{}, err
}
if err := os.WriteFile(savePath, data, 0o644); err != nil {
return models.ExportPDFResponse{}, err
}
_ = openWithDefaultApp(savePath)
return models.ExportPDFResponse{Path: savePath}, nil
}
func defaultPDFNameV051(meta models.ExportPDFMeta) string {
ts := time.Now().Format("2006-01-02-15h04")
return fmt.Sprintf(
"%s-%s-%s-%s.pdf",
ts,
pdf.SanitizeFilename(meta.Nom),
pdf.SanitizeFilename(meta.Prenom),
pdf.SanitizeFilename(meta.Matricule),
)
}
func openWithDefaultApp(path string) error {
switch goruntime.GOOS {
case "windows":
return exec.Command("cmd", "/c", "start", "", path).Start()
case "darwin":
return exec.Command("open", path).Start()
default:
return exec.Command("xdg-open", path).Start()
}
}