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() } }