215 lines
5.0 KiB
Go
215 lines
5.0 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/paycheck/internal/calc"
|
|
"git.dumerain.org/alban/paycheck/internal/importer"
|
|
"git.dumerain.org/alban/paycheck/internal/models"
|
|
"git.dumerain.org/alban/paycheck/internal/pdf"
|
|
"git.dumerain.org/alban/paycheck/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()
|
|
}
|
|
}
|