commit cad0b2768a8be5f7f03d303c1baa718a1d73775a Author: Alban Dumerain Date: Mon Jan 19 14:25:43 2026 +0100 Import initail de calcul-astreintes v0.9.4 pour passage en paycheck 1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fb753b --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# --- Build / Wails --- +bin/ +build/ +dist/ +*.exe +*.app +*.dmg +!build/appicon.png +!build/windows/ +!build/windows/icon.ico + +# Wails generated +# frontend/src/wailsjs/ + +# --- Node --- +frontend/node_modules/ +frontend/dist/* +!frontend/dist/.keep +frontend/.vite/ + +# --- Archives / exports lourds --- +*.zip +*.tar +*.tar.gz +*.7z + +# --- PDF / exports --- +*.pdf + +# --- Diffs / patches --- +*.diff +*.patch + +# --- OS / IDE --- +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e1e00d --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2026 Alban + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..e68c7ed --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# Calcul Astreintes + +Outil de calcul de rémunération des astreintes et interventions, basé sur un import de fichier Excel mensuel et un **remplissage automatique** des données par agent. + +--- + +## Fonctionnalités + +- Import d’un fichier Excel mensuel (format `YYYY-MM`) +- Lecture automatique des heures d’astreinte et d’intervention +- Sélection d’un agent par **nom / prénom / matricule** +- **Remplissage automatique** des champs de calcul à partir du fichier Excel +- Saisie manuelle possible (sans import Excel) +- Calcul **manuel** via un bouton « Calculer » (comportement volontairement sécurisé) +- Export PDF du calcul (A4) (nom de fichier : `calcul-astreintes-YYYY-MM-NOM-Prenom.pdf`) +- Application graphique multiplateforme basée sur **Wails** (Go + Web) + +Champs remplis automatiquement : +- Heures d’intervention de jour (code 456) +- Heures d’intervention de nuit (code 459) +- Heures d’intervention dimanche / jours fériés (code 458) +- Total des heures d’astreinte du mois (code 471) + +--- + +## Principe de fonctionnement + +1. L’utilisateur importe le fichier Excel mensuel des astreintes +2. Le logiciel analyse automatiquement la feuille correspondant au mois (`YYYY-MM`) +3. L’utilisateur sélectionne un agent dans la liste (nom, prénom, matricule) +4. Les champs de calcul sont **remplis automatiquement** à partir de la ligne « Total Agent » +5. L’utilisateur vérifie / complète si nécessaire (ex. nombre de dimanches / jours fériés) +6. Le calcul est lancé manuellement via le bouton « Calculer » + +--- + +## Format du fichier Excel attendu + +- Une feuille par mois, nommée `YYYY-MM` (exemple : `2026-01`) +- Les agents sont identifiés par : + - Un **matricule unique** + - Le nom et le prénom dans une seule cellule (format : `NOM Prénom`) +- Les valeurs utilisées sont celles de la ligne **Total Agent**, située juste au-dessus des lignes détaillées + +Colonnes exploitées sur la ligne « Total Agent » : +- Total des heures d’astreinte du mois +- Heures d’intervention de jour +- Heures d’intervention de nuit +- Heures d’intervention dimanche / jour férié + +Les lignes détaillées (types d’intervention) ne sont pas utilisées. + +--- + +## Installation – Debian 13 (Linux) + +### Dépendances système + +```bash +sudo apt update +sudo apt install -y \ + build-essential pkg-config \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev +``` + +### Go (≥ 1.21) + +```bash +sudo apt install -y golang +``` + +### Node.js (Node 20 LTS – recommandé) + +Ajout du dépôt NodeSource : + +```bash +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt install -y nodejs +``` + +### Wails + +```bash +go install github.com/wailsapp/wails/v2/cmd/wails@latest +``` + +--- + +## Lancement en mode développement (Linux) + +```bash +wails dev -tags webkit2_41 +``` + +--- + +## Compilation Linux + +```bash +wails build -tags webkit2_41 +``` + +Le binaire généré se trouve dans le dossier `build/`. + +--- + +## Compilation Windows (préparation / roadmap) + +La compilation Windows est prévue prochainement. Le projet est déjà compatible Wails. + +### Prérequis Windows + +- Windows 10 ou 11 (64 bits) +- Go ≥ 1.21 +- Node.js 20 LTS +- Outils de compilation Microsoft : + - **Visual Studio Build Tools** + - Composant « Développement Desktop en C++ » + +### Étapes prévues pour compiler sous Windows + +1. Installer Go et Node.js +2. Installer les Visual Studio Build Tools (C++ requis) +3. Installer Wails : + +```powershell +go install github.com/wailsapp/wails/v2/cmd/wails@latest +``` + +4. Depuis un terminal PowerShell dans le projet : + +```powershell +wails build +``` + +Un binaire Windows (`.exe`) sera alors généré. + +> Remarque : aucune dépendance GTK/WebKit n’est nécessaire sous Windows. + +--- + +## Licence + +Ce projet est distribué sous licence **GNU GPL v3**. + +--- + +## Statut du projet + +- Version actuelle : **v0.2** +- Projet en cours de développement + +### Évolutions prévues + +- Amélioration de l’ergonomie de l’interface +- Gestion de plusieurs profils utilisateurs +- Export des résultats (PDF / CSV) +- Calcul du net à partir du brut +- Compilation et distribution Windows + diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..cf8b176 --- /dev/null +++ b/app/app.go @@ -0,0 +1,214 @@ +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() + } +} diff --git a/assets/icon/export/paybackcheck-icon-alt.png b/assets/icon/export/paybackcheck-icon-alt.png new file mode 100644 index 0000000..fbc6dd0 Binary files /dev/null and b/assets/icon/export/paybackcheck-icon-alt.png differ diff --git a/assets/icon/export/paybackcheck-icon-piggy.ico b/assets/icon/export/paybackcheck-icon-piggy.ico new file mode 100644 index 0000000..d7a625e Binary files /dev/null and b/assets/icon/export/paybackcheck-icon-piggy.ico differ diff --git a/assets/icon/export/paybackcheck-icon-primary.png b/assets/icon/export/paybackcheck-icon-primary.png new file mode 100644 index 0000000..fb08b24 Binary files /dev/null and b/assets/icon/export/paybackcheck-icon-primary.png differ diff --git a/assets/icon/export/paybackcheck-logo-eurocheck.ico b/assets/icon/export/paybackcheck-logo-eurocheck.ico new file mode 100644 index 0000000..31147d4 Binary files /dev/null and b/assets/icon/export/paybackcheck-logo-eurocheck.ico differ diff --git a/assets/icon/export/paybackcheck-logo-eurocheck.png b/assets/icon/export/paybackcheck-logo-eurocheck.png new file mode 100644 index 0000000..686de54 Binary files /dev/null and b/assets/icon/export/paybackcheck-logo-eurocheck.png differ diff --git a/assets/icon/src/paybackcheck-logo-eurocheck (1).svg b/assets/icon/src/paybackcheck-logo-eurocheck (1).svg new file mode 100644 index 0000000..e65a515 --- /dev/null +++ b/assets/icon/src/paybackcheck-logo-eurocheck (1).svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/src/paybackcheck-logo-eurocheck.svg b/assets/icon/src/paybackcheck-logo-eurocheck.svg new file mode 100644 index 0000000..28143cb --- /dev/null +++ b/assets/icon/src/paybackcheck-logo-eurocheck.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icon/src/paybackcheck-logo-eurocheck2.svg b/assets/icon/src/paybackcheck-logo-eurocheck2.svg new file mode 100644 index 0000000..c20e347 --- /dev/null +++ b/assets/icon/src/paybackcheck-logo-eurocheck2.svg @@ -0,0 +1,3 @@ + + + diff --git a/cmd/checkprofiles/main.go b/cmd/checkprofiles/main.go new file mode 100644 index 0000000..8141a40 --- /dev/null +++ b/cmd/checkprofiles/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "log" + "sort" + + "git.dumerain.org/alban/calcul-astreintes/internal/profiles" +) + +func main() { + repo, rules, err := profiles.NewEmbeddedRepo() + if err != nil { + log.Fatalf("Erreur chargement profils: %v", err) + } + items, _ := repo.List() + fmt.Printf("OK: %d profil(s) charge(s) - forfait=%.2f\n", len(items), rules.ForfaitDimFerie) + + keys := make([]string, 0, len(items)) + for _, it := range items { + keys = append(keys, it.ID) + } + sort.Strings(keys) + for _, id := range keys { + p, _ := repo.Get(id) + fmt.Printf("- %s: label=%q t456=%.2f t458=%.2f t459=%.2f t471=%.2f\n", id, p.Label, p.T456, p.T458, p.T459, p.T471) + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d6f3dc6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Calcul astreintes + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e5b0396 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,987 @@ +{ + "name": "calcul-astreintes-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "calcul-astreintes-frontend", + "version": "0.0.1", + "devDependencies": { + "vite": "^5.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f75d574 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "calcul-astreintes-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --port 4173" + }, + "devDependencies": { + "vite": "^5.4.0" + } +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..ceead85 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +58af01706385cb79d0824bdff23f5bc8 \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..bb0fd23 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,531 @@ +import './style.css'; + +import { + ListProfiles, + Calculate, + ImportMonthlyXLSX, + GetImportedTotals, + GetLastImportInfo, + ExportPDF +} from './wailsjs/go/app/App'; + +const app = document.querySelector('#app'); + +/* ---------- helpers ---------- */ + +function euro(n) { + if (typeof n !== 'number' || Number.isNaN(n)) return ''; + return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(n); +} + +function numberOrZero(s) { + const cleaned = String(s ?? '').trim().replace(/\s/g, '').replace(',', '.'); + const n = Number(cleaned); + return Number.isFinite(n) ? n : 0; +} + +function intOrZero(s) { + const n = parseInt(String(s ?? '').trim(), 10); + return Number.isFinite(n) ? n : 0; +} + +function parseAgentDisplay(display) { + // "NOM Prenom (123456)" => { nom:"NOM", prenom:"Prenom" } + const s = String(display || '').trim(); + const withoutMat = s.replace(/\(\s*\d+\s*\)\s*$/, '').trim(); + const parts = withoutMat.split(/\s+/).filter(Boolean); + if (parts.length === 0) return { nom: '', prenom: '' }; + if (parts.length === 1) return { nom: parts[0], prenom: '' }; + return { nom: parts[0], prenom: parts.slice(1).join(' ') }; +} + +function sectionDetails(sectionKey, title, status, isOpen, innerHTML) { + return ` +
+ + + ${title} + — ${status} + + + +
+ ${innerHTML} +
+
+ `; +} + +function autoSelectProfileForMatricule(state, matricule) { + if (!matricule) { + state.profileId = ''; + return; + } + const match = state.profiles.find(p => String(p.matricule || '') === String(matricule)); + if (match) { + state.profileId = match.id; + return; + } + // fallback + const hasTest = state.profiles.some(p => p.id === 'test'); + state.profileId = hasTest ? 'test' : ''; +} + +/* ---------- auto-load + auto-calc (v0.8) ---------- */ + +async function recalcNow(state) { + // Recalcule uniquement si profil OK + if (!state.profileId) { + state.rows = []; + state.total = 0; + state.error = "Sélectionne un profil (ou choisis un agent pour auto-match)."; + return; + } + state.error = ''; + + const res = await Calculate({ + profileId: state.profileId, + q456: numberOrZero(state.q456), + q458: numberOrZero(state.q458), + q459: numberOrZero(state.q459), + q471: numberOrZero(state.q471), + nbDimFerie: intOrZero(state.nbDimFerie), + }); + + state.rows = res.lines || []; + state.total = res.total || 0; + state.sections.details = true; +} + +async function loadSelectedAgentIntoForm(state) { + if (!state.selectedMatricule) return; + + const t = await GetImportedTotals(state.selectedMatricule); + + state.q456 = String(t.q456 ?? 0); + state.q458 = String(t.q458 ?? 0); + state.q459 = String(t.q459 ?? 0); + state.q471 = String(t.q471 ?? 0); + state.nbDimFerie = String(t.nbDimFerie ?? 0); + + const agent = state.importedAgents.find(a => a.matricule === state.selectedMatricule); + if (agent) { + const { nom, prenom } = parseAgentDisplay(agent.display); + state.nom = nom; + state.prenom = prenom; + state.matricule = state.selectedMatricule; + } else { + state.matricule = state.selectedMatricule; + } +} + +async function onAgentChanged(state) { + // 1) Auto profil (match matricule → sinon test) + autoSelectProfileForMatricule(state, state.selectedMatricule); + + // 2) Auto load valeurs (si import présent) + if (state.selectedMatricule) { + await loadSelectedAgentIntoForm(state); + } else { + // reset si aucun agent + state.profileId = ''; + state.q456 = state.q458 = state.q459 = state.q471 = '0'; + state.nbDimFerie = '0'; + state.rows = []; + state.total = 0; + state.error = ''; + } + + // 3) Auto calc + if (state.selectedMatricule && state.profileId) { + await recalcNow(state); + } else { + state.rows = []; + state.total = 0; + } +} + +async function onProfileChanged(state) { + // Recalcul “à chaud” si on a déjà des valeurs + if (state.selectedMatricule && state.profileId) { + await recalcNow(state); + } else { + state.rows = []; + state.total = 0; + } +} + +/* ---------- UI state ---------- */ + +function computeStatuses(state) { + let importStatus = 'non importé'; + if (state.importedAgents.length > 0) { + if (state.selectedMatricule) { + const agent = state.importedAgents.find(a => a.matricule === state.selectedMatricule); + importStatus = agent ? `agent : ${agent.display}` : `agent : ${state.selectedMatricule}`; + } else { + importStatus = `${state.importedAgents.length} agents chargés`; + } + } + + const currentProfile = state.profiles.find(p => p.id === state.profileId); + const dim = intOrZero(state.nbDimFerie); + const profileStatus = currentProfile + ? `${currentProfile.label} • Dim/JF : ${dim}` + : 'non sélectionné'; + + const values = [ + numberOrZero(state.q456), + numberOrZero(state.q458), + numberOrZero(state.q459), + numberOrZero(state.q471) + ]; + const filled = values.filter(v => v !== 0).length; + const inputsStatus = filled === 0 ? 'vides' : `${filled} valeur${filled > 1 ? 's' : ''}`; + + const detailsStatus = + state.rows.length === 0 + ? 'non calculé' + : `${state.rows.length} ligne${state.rows.length > 1 ? 's' : ''} • total : ${euro(state.total)}`; + + return { importStatus, profileStatus, inputsStatus, detailsStatus }; +} + +/* ---------- render ---------- */ + +function render(state) { + const st = computeStatuses(state); + + app.innerHTML = ` +
+ +
+
+
+

Paychek - Outil de calcul de rémunération d'astreintes

+
+ +
+ +
+
+
+ +
+ + ${sectionDetails( + 'import', + 'Import Excel', + st.importStatus, + state.sections.import, + ` + + +
+
+ + +
+
+ +
+ + +
+ + ${state.importError ? `
${state.importError}
` : ''} + ` + )} + + ${sectionDetails( + 'profile', + 'Profil', + st.profileStatus, + state.sections.profile, + ` +
+
+ + +
+
+ + +
+
+ ` + )} + + ${sectionDetails( + 'inputs', + 'Interventions (heures)', + st.inputsStatus, + state.sections.inputs, + ` +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + + ${state.error ? `
${state.error}
` : ''} + ` + )} + + ${sectionDetails( + 'details', + 'Détail', + st.detailsStatus, + state.sections.details, + ` + + + + + + + + + + + + ${state.rows.map(r => ` + + + + + + + + `).join('')} + + + + + + + +
CodeLibelléBaseQuantitéTotal
${r.code}${r.label}${Number(r.rate).toFixed(2)}${Number(r.quantity).toFixed(2)} ${r.unit}${euro(r.amount)}
TOTAL BRUT${euro(state.total)}
+ ` + )} + +
+ +
+ +
+ +
+ `; + + /* ---------- section state ---------- */ + + document.querySelectorAll('details.section[data-section]').forEach(d => { + d.ontoggle = () => { + const key = d.getAttribute('data-section'); + state.sections[key] = d.open; + }; + }); + +const toggleBtn = document.getElementById('toggleAll'); +if (toggleBtn) { + toggleBtn.onclick = () => { + const sections = document.querySelectorAll('details'); + const allOpen = Array.from(sections).every(d => d.open); + + sections.forEach(d => { + d.open = !allOpen; + }); + + toggleBtn.textContent = allOpen + ? 'Tout afficher' + : 'Tout masquer'; + const toggleAllBtn = document.getElementById('toggleAll'); + if (toggleAllBtn) { + const sections = document.querySelectorAll('details'); + const allOpen = + sections.length > 0 && + Array.from(sections).every(d => d.open); + + toggleAllBtn.textContent = allOpen + ? 'Tout masquer' + : 'Tout afficher'; + } +}; +} + + /* ---------- profile ---------- */ + + const profileSel = document.getElementById('profile'); + profileSel.innerHTML = ` + + ${state.profiles.map(p => + `` + ).join('')} + `; + + profileSel.onchange = async (e) => { + state.profileId = e.target.value; + try { + await onProfileChanged(state); // ✅ auto-calc “à chaud” + render(state); + } catch (err) { + state.error = String(err); + render(state); + } + }; + + /* ---------- import ---------- */ + +document.getElementById('importXlsx').onclick = async () => { + try { + state.importedAgents = await ImportMonthlyXLSX() || []; + state.importError = ''; + + // 🔧 Récupère le mois normalisé détecté par le backend (utilisé ensuite par l'export PDF) + try { + const info = await GetLastImportInfo(); + if (info && info.yearMonth) { + state.yearMonth = info.yearMonth; + } + } catch (_) { + // Ne pas bloquer l'import si l'info n'est pas dispo + } + + // Si un agent est déjà sélectionné, on re-joue la logique (profil + load + calc) + if (state.selectedMatricule) { + await onAgentChanged(state); + } + render(state); + } catch (e) { + state.importError = String(e); + render(state); + } +}; + + document.getElementById('agentSelect').onchange = async (e) => { + state.selectedMatricule = e.target.value; + try { + await onAgentChanged(state); // ✅ auto profil + auto load + auto calc + render(state); + } catch (err) { + state.error = String(err); + render(state); + } + }; + + document.getElementById('loadAgent').onclick = async () => { + if (!state.selectedMatricule) return; + try { + await loadSelectedAgentIntoForm(state); + if (state.profileId) { + await recalcNow(state); // ✅ rechargement => recalcul + } + render(state); + } catch (e) { + state.error = String(e); + render(state); + } + }; + + document.getElementById('clearAgent').onclick = () => { + state.selectedMatricule = ''; + state.profileId = ''; + state.q456 = state.q458 = state.q459 = state.q471 = '0'; + state.nbDimFerie = '0'; + state.rows = []; + state.total = 0; + state.error = ''; + render(state); + }; + + /* ---------- manual calculation button ---------- */ + + document.getElementById('calc').onclick = async () => { + try { + await recalcNow(state); + render(state); + } catch (e) { + state.error = String(e); + render(state); + } + }; + + /* ---------- export ---------- */ + + document.getElementById('exportPdf').onclick = async () => { + try { + if (!state.profileId) { + alert("Sélectionne un profil (ou choisis un agent pour auto-match)."); + return; + } + await ExportPDF({ + meta: { + yearMonth: state.yearMonth, + nom: state.nom, + prenom: state.prenom, + matricule: state.matricule + }, + request: { + profileId: state.profileId, + q456: numberOrZero(state.q456), + q458: numberOrZero(state.q458), + q459: numberOrZero(state.q459), + q471: numberOrZero(state.q471), + nbDimFerie: intOrZero(state.nbDimFerie), + } + }); + } catch (e) { + alert(String(e)); + } + }; +} + +/* ---------- boot ---------- */ + +async function boot() { + const state = { + profiles: [], + profileId: '', + + q456: '0', q458: '0', q459: '0', q471: '0', + nbDimFerie: '0', + + rows: [], total: 0, error: '', + + importedAgents: [], selectedMatricule: '', importError: '', + + yearMonth: '', nom: '', prenom: '', matricule: '', + + sections: { import: true, profile: false, inputs: false, details: false } + }; + + state.profiles = await ListProfiles(); + render(state); +} + +boot(); diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..7b0a6fd --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,213 @@ +/* CSS volontairement simple : lisible, pas de framework. */ +:root { + font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + line-height: 1.35; + color: #111; +} + +body { + margin: 0; + background: #f5f5f5; +} + +.container { + max-width: 980px; + margin: 24px auto; + padding: 16px; +} + +.card { + background: #fff; + border-radius: 14px; + padding: 16px; + box-shadow: 0 6px 18px rgba(0,0,0,.06); + margin-bottom: 14px; +} + +/* ========================= + HEADER +========================= */ + +.header-card { + padding: 14px 16px; +} + +.header-top { + display: flex; + justify-content: space-between; + align-items: center; /* ✅ alignement vertical corrigé */ + gap: 12px; +} + +.header-top h2 { + margin: 0; + flex: 1; /* le titre prend l’espace */ +} + +.header-actions { + display: flex; + gap: 10px; + align-items: center; + flex-shrink: 0; /* le bouton ne se compresse pas */ +} + +/* ========================= */ + +.row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +@media (max-width: 820px) { + .row { + grid-template-columns: 1fr; + } +} + +label { + display: block; + font-size: .92rem; + margin-bottom: 6px; + color: #333; +} + +input, +select { + width: 100%; + padding: 10px 12px; + border: 1px solid #d8d8d8; + border-radius: 10px; + font-size: 1rem; +} + +button { + padding: 10px 14px; + border: 0; + border-radius: 10px; + font-size: 1rem; + cursor: pointer; + background: #eee; +} + +button.primary { + background: #111; + color: #fff; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + text-align: left; + padding: 10px 8px; + border-bottom: 1px solid #eee; +} + +.muted { + color: #666; +} + +.right { + text-align: right; +} + +.total { + font-weight: 700; + font-size: 1.1rem; +} + +.error { + color: #b00020; + margin-top: 10px; +} + +.success { + color: #0a7a2f; + margin-top: 10px; +} + +/* --------------------------- + Sections repliables +---------------------------- */ + +.section { + border: 1px solid #eee; + border-radius: 12px; + margin-bottom: 12px; + overflow: hidden; +} + +/* Supprime le marker natif */ +.section > summary { + list-style: none; +} + +.section > summary::-webkit-details-marker { + display: none; +} + +.section > summary { + cursor: pointer; + padding: 12px 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + user-select: none; + background: #fafafa; + border-bottom: 1px solid #eee; +} + +.section[open] > summary { + background: #f7f7f7; +} + +.section-title { + font-weight: 650; +} + +.section-body { + padding: 14px 12px; +} + +/* Chevron simple (sans dépendance) */ +.chevron::before { + content: "▸"; + display: inline-block; + transform: rotate(0deg); + transition: transform 120ms ease; + color: #444; + font-size: 1.1rem; +} + +.section[open] .chevron::before { + transform: rotate(90deg); +} + +/* Légère amélioration tactile */ +.section > summary:hover { + background: #f3f3f3; +} + +.section-status { + font-weight: 500; + color: #666; + margin-left: 8px; + white-space: nowrap; +} + +/* --------------------------- + Barre d’export PDF +---------------------------- */ + +.export-bar { + position: sticky; + bottom: 16px; + display: flex; + justify-content: flex-end; + margin-top: 18px; +} diff --git a/frontend/src/wailsjs/go/app/App.d.ts b/frontend/src/wailsjs/go/app/App.d.ts new file mode 100755 index 0000000..6b3f37b --- /dev/null +++ b/frontend/src/wailsjs/go/app/App.d.ts @@ -0,0 +1,15 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {models} from '../models'; + +export function Calculate(arg1:models.CalculateRequest):Promise; + +export function ExportPDF(arg1:models.ExportPDFRequest):Promise; + +export function GetImportedTotals(arg1:string):Promise; + +export function GetLastImportInfo():Promise>; + +export function ImportMonthlyXLSX():Promise>; + +export function ListProfiles():Promise>; diff --git a/frontend/src/wailsjs/go/app/App.js b/frontend/src/wailsjs/go/app/App.js new file mode 100755 index 0000000..19c2ff8 --- /dev/null +++ b/frontend/src/wailsjs/go/app/App.js @@ -0,0 +1,27 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Calculate(arg1) { + return window['go']['app']['App']['Calculate'](arg1); +} + +export function ExportPDF(arg1) { + return window['go']['app']['App']['ExportPDF'](arg1); +} + +export function GetImportedTotals(arg1) { + return window['go']['app']['App']['GetImportedTotals'](arg1); +} + +export function GetLastImportInfo() { + return window['go']['app']['App']['GetLastImportInfo'](); +} + +export function ImportMonthlyXLSX() { + return window['go']['app']['App']['ImportMonthlyXLSX'](); +} + +export function ListProfiles() { + return window['go']['app']['App']['ListProfiles'](); +} diff --git a/frontend/src/wailsjs/go/models.ts b/frontend/src/wailsjs/go/models.ts new file mode 100755 index 0000000..ab44775 --- /dev/null +++ b/frontend/src/wailsjs/go/models.ts @@ -0,0 +1,200 @@ +export namespace models { + + export class Agent { + matricule: string; + nom: string; + prenom: string; + display: string; + + static createFrom(source: any = {}) { + return new Agent(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.matricule = source["matricule"]; + this.nom = source["nom"]; + this.prenom = source["prenom"]; + this.display = source["display"]; + } + } + export class AgentTotals { + matricule: string; + q471: number; + q456: number; + q459: number; + q458: number; + nbDimFerie: number; + + static createFrom(source: any = {}) { + return new AgentTotals(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.matricule = source["matricule"]; + this.q471 = source["q471"]; + this.q456 = source["q456"]; + this.q459 = source["q459"]; + this.q458 = source["q458"]; + this.nbDimFerie = source["nbDimFerie"]; + } + } + export class CalculateRequest { + profileId: string; + q456: number; + q458: number; + q459: number; + q471: number; + nbDimFerie: number; + + static createFrom(source: any = {}) { + return new CalculateRequest(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.profileId = source["profileId"]; + this.q456 = source["q456"]; + this.q458 = source["q458"]; + this.q459 = source["q459"]; + this.q471 = source["q471"]; + this.nbDimFerie = source["nbDimFerie"]; + } + } + export class Line { + code: string; + label: string; + rate: number; + quantity: number; + amount: number; + unit: string; + + static createFrom(source: any = {}) { + return new Line(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.code = source["code"]; + this.label = source["label"]; + this.rate = source["rate"]; + this.quantity = source["quantity"]; + this.amount = source["amount"]; + this.unit = source["unit"]; + } + } + export class CalculateResponse { + lines: Line[]; + total: number; + + static createFrom(source: any = {}) { + return new CalculateResponse(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.lines = this.convertValues(source["lines"], Line); + this.total = source["total"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class ExportPDFMeta { + yearMonth: string; + nom: string; + prenom: string; + matricule: string; + + static createFrom(source: any = {}) { + return new ExportPDFMeta(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.yearMonth = source["yearMonth"]; + this.nom = source["nom"]; + this.prenom = source["prenom"]; + this.matricule = source["matricule"]; + } + } + export class ExportPDFRequest { + meta: ExportPDFMeta; + request: CalculateRequest; + + static createFrom(source: any = {}) { + return new ExportPDFRequest(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.meta = this.convertValues(source["meta"], ExportPDFMeta); + this.request = this.convertValues(source["request"], CalculateRequest); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class ExportPDFResponse { + path: string; + + static createFrom(source: any = {}) { + return new ExportPDFResponse(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.path = source["path"]; + } + } + + export class ProfileListItem { + id: string; + label: string; + matricule?: string; + + static createFrom(source: any = {}) { + return new ProfileListItem(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.label = source["label"]; + this.matricule = source["matricule"]; + } + } + +} + diff --git a/frontend/src/wailsjs/runtime/package.json b/frontend/src/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/src/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/src/wailsjs/runtime/runtime.d.ts b/frontend/src/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..4445dac --- /dev/null +++ b/frontend/src/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/frontend/src/wailsjs/runtime/runtime.js b/frontend/src/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..7cb89d7 --- /dev/null +++ b/frontend/src/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..42b2599 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; + +/** + * Configuration minimale Vite : + * - base './' pour que les assets fonctionnent en mode "embarqué" + * - outDir = 'dist' (Wails embarque frontend/dist dans le binaire) + */ +export default defineConfig({ + base: './', + build: { + outDir: 'dist', + emptyOutDir: true + } +}); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..add5c32 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module git.dumerain.org/alban/calcul-astreintes + +go 1.24.0 + +toolchain go1.24.4 + +require ( + github.com/phpdave11/gofpdf v1.4.2 + github.com/wailsapp/wails/v2 v2.11.0 + github.com/xuri/excelize/v2 v2.10.0 + golang.org/x/text v0.33.0 +) + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tiendc/go-deepcopy v1.7.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6b72db2 --- /dev/null +++ b/go.sum @@ -0,0 +1,107 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/calc/compute.go b/internal/calc/compute.go new file mode 100644 index 0000000..a3665fe --- /dev/null +++ b/internal/calc/compute.go @@ -0,0 +1,74 @@ +package calc + +import ( + "math" + + "git.dumerain.org/alban/calcul-astreintes/internal/models" +) + +/* +Le moteur de calcul est volontairement isolé du reste. +Ça facilite : +- les tests unitaires +- l'évolution des règles +- le support de nouveaux codes +*/ + +func round2(v float64) float64 { + return math.Round(v*100) / 100 +} + +func Compute(req models.CalculateRequest, profile models.Profile, rules models.GlobalRules) models.CalculateResponse { + // Chaque ligne = taux * quantité, arrondi au centime. + // Libellés volontairement "fiche de paie" (courts) pour que UI + PDF collent. + lines := []models.Line{ + { + Code: "456", + Label: "Interv.astreinte Jour", + Rate: profile.T456, + Quantity: req.Q456, + Unit: "h", + Amount: round2(profile.T456 * req.Q456), + }, + { + Code: "458", + Label: "Interv.astreinte Dim & JF", + Rate: profile.T458, + Quantity: req.Q458, + Unit: "h", + Amount: round2(profile.T458 * req.Q458), + }, + { + Code: "459", + Label: "Interv.astreinte Nuit", + Rate: profile.T459, + Quantity: req.Q459, + Unit: "h", + Amount: round2(profile.T459 * req.Q459), + }, + { + Code: "471", + Label: "Astreinte", + Rate: profile.T471, + Quantity: req.Q471, + Unit: "h", + Amount: round2(profile.T471 * req.Q471), + }, + { + Code: "480", + Label: "Ind. Forfait. Dim & JF", + Rate: rules.ForfaitDimFerie, + Quantity: float64(req.NbDimFerie), + Unit: "j", + Amount: round2(rules.ForfaitDimFerie * float64(req.NbDimFerie)), + }, + } + + // Total = somme des montants déjà arrondis (comportement typique paie). + total := 0.0 + for _, l := range lines { + total += l.Amount + } + + return models.CalculateResponse{Lines: lines, Total: round2(total)} +} diff --git a/internal/importer/xlsx.go b/internal/importer/xlsx.go new file mode 100644 index 0000000..e00e187 --- /dev/null +++ b/internal/importer/xlsx.go @@ -0,0 +1,205 @@ +package importer + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/xuri/excelize/v2" + + "git.dumerain.org/alban/calcul-astreintes/internal/models" +) + +// Feuille de mois acceptée : +// - YYYY-MM (ex: 2025-01) +// - MM-YYYY (ex: 01-2025) +var sheetRe = regexp.MustCompile(`^(\d{4})-(\d{2})$|^(\d{2})-(\d{4})$`) + +func normalizeYearMonth(sheet string) (string, bool) { + sheet = strings.TrimSpace(sheet) + m := sheetRe.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 +} + +// parseNumberFR convertit "1,92" / "1.92" / "" -> float64 +func parseNumberFR(s string) float64 { + s = strings.TrimSpace(s) + if s == "" { + return 0 + } + s = strings.ReplaceAll(s, "\u00a0", "") + s = strings.ReplaceAll(s, " ", "") + s = strings.ReplaceAll(s, ",", ".") + f, _ := strconv.ParseFloat(s, 64) + return f +} + +func isMatriculeCell(s string) bool { + s = strings.TrimSpace(s) + if s == "" { + return false + } + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} + +// FindMonthSheet: prend la première feuille qui match YYYY-MM ou MM-YYYY, sinon la première feuille. +func FindMonthSheet(f *excelize.File) (string, error) { + sheets := f.GetSheetList() + for _, sh := range sheets { + if _, ok := normalizeYearMonth(sh); ok { + return strings.TrimSpace(sh), nil + } + } + if len(sheets) == 0 { + return "", fmt.Errorf("aucune feuille trouvée dans le fichier") + } + return sheets[0], nil +} + +// ParseAgentsAndTotals lit la feuille détectée et renvoie agents + totaux. +// (Le mois est géré côté app/app.go via normalizeYearMonth) +func ParseAgentsAndTotals(path string) ([]models.Agent, map[string]models.AgentTotals, error) { + f, err := excelize.OpenFile(path) + if err != nil { + return nil, nil, err + } + defer func() { _ = f.Close() }() + + sheet, err := FindMonthSheet(f) + if err != nil { + return nil, nil, err + } + + rows, err := f.GetRows(sheet) + if err != nil { + return nil, nil, err + } + + agents := []models.Agent{} + totals := map[string]models.AgentTotals{} + + // Dedup P_DIM-JF par agent (par date) + dimDates := map[string]map[string]struct{}{} + currentMatricule := "" + + cell := func(col string, excelRow int) string { + v, _ := f.GetCellValue(sheet, fmt.Sprintf("%s%d", col, excelRow)) + return v + } + + for i := 0; i < len(rows); i++ { + row := rows[i] + + colA := "" + if len(row) >= 1 { + colA = row[0] + } + + if isMatriculeCell(colA) { + currentMatricule = strings.TrimSpace(colA) + + if _, ok := totals[currentMatricule]; !ok { + fullName := "" + if len(row) >= 2 { + fullName = strings.TrimSpace(row[1]) // Col B = "NOM Prenom" + } + + nom, prenom := "", "" + if fullName != "" { + parts := strings.Fields(fullName) + if len(parts) >= 1 { + nom = parts[0] + } + if len(parts) >= 2 { + prenom = strings.Join(parts[1:], " ") + } + } + + displayName := strings.TrimSpace(strings.Join([]string{nom, prenom}, " ")) + if displayName == "" { + displayName = currentMatricule + } + display := fmt.Sprintf("%s (%s)", displayName, currentMatricule) + + if i == 0 { + continue + } + + totalExcelRow := i // ligne au-dessus en Excel (cf logique actuelle) + + q471 := parseNumberFR(cell("G", totalExcelRow)) + q456 := parseNumberFR(cell("L", totalExcelRow)) + q459 := parseNumberFR(cell("M", totalExcelRow)) + q458 := parseNumberFR(cell("N", totalExcelRow)) + + agents = append(agents, models.Agent{ + Matricule: currentMatricule, + Nom: nom, + Prenom: prenom, + Display: display, + }) + + totals[currentMatricule] = models.AgentTotals{ + Matricule: currentMatricule, + Q471: q471, + Q456: q456, + Q459: q459, + Q458: q458, + NbDimFerie: 0, + } + } + } + + if currentMatricule != "" { + typeLigne := "" + if len(row) >= 3 { + typeLigne = strings.TrimSpace(row[2]) // Col C + } + if typeLigne == "P_DIM-JF" { + dateStr := "" + if len(row) >= 4 { + dateStr = strings.TrimSpace(row[3]) // Col D + } + if _, ok := dimDates[currentMatricule]; !ok { + dimDates[currentMatricule] = map[string]struct{}{} + } + if dateStr != "" { + dimDates[currentMatricule][dateStr] = struct{}{} + } else { + dimDates[currentMatricule][fmt.Sprintf("row-%d", i+1)] = struct{}{} + } + } + } + } + + for mat, t := range totals { + if s, ok := dimDates[mat]; ok { + t.NbDimFerie = len(s) + totals[mat] = t + } + } + + sort.Slice(agents, func(i, j int) bool { + return strings.ToLower(agents[i].Display) < strings.ToLower(agents[j].Display) + }) + + return agents, totals, nil +} diff --git a/internal/models/import.go b/internal/models/import.go new file mode 100644 index 0000000..a86f146 --- /dev/null +++ b/internal/models/import.go @@ -0,0 +1,21 @@ +package models + +// Agent = une personne trouvée dans le fichier (affichage + clé matricule) +type Agent struct { + Matricule string `json:"matricule"` + Nom string `json:"nom"` + Prenom string `json:"prenom"` + Display string `json:"display"` // "NOM Prenom (matricule)" +} + +// AgentTotals = les totaux "Total Agent" (ligne du dessus) +type AgentTotals struct { + Matricule string `json:"matricule"` + Q471 float64 `json:"q471"` // Total heures astreinte (col G) + Q456 float64 `json:"q456"` // Intervention jour (col L) + Q459 float64 `json:"q459"` // Intervention nuit (col M) + Q458 float64 `json:"q458"` // Intervention dimanche/ferié (col N) + + // ✅ Nouveau: nombre de dimanches / jours fériés détectés via les lignes P_DIM-JF + NbDimFerie int `json:"nbDimFerie"` +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..f2b29ee --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,57 @@ +package models + +// GlobalRules = constantes communes à tous les utilisateurs. +type GlobalRules struct { + // ForfaitDimFerie = forfait par dimanche / jour férié (€/jour) + ForfaitDimFerie float64 `json:"forfait_dim_ferie"` +} + +// Profile = taux dépendants de la personne (grade, etc.) +type Profile struct { + ID string `json:"id"` + Label string `json:"label"` + Matricule string `json:"matricule,omitempty"` // ✅ pour auto-match avec l'agent importé + + // Taux horaires (€/h) + T456 float64 `json:"t456"` // Interventions jour hors dimanche/férié + T458 float64 `json:"t458"` // Interventions dimanche/jour férié + T459 float64 `json:"t459"` // Interventions nuit hors dimanche/férié + T471 float64 `json:"t471"` // Astreinte: heures totales du mois +} + +// ProfileListItem = ce qu'on expose à l'UI (id + label + matricule pour auto-match) +type ProfileListItem struct { + ID string `json:"id"` + Label string `json:"label"` + Matricule string `json:"matricule,omitempty"` +} + +// CalculateRequest = saisies mensuelles par l'utilisateur. +type CalculateRequest struct { + ProfileID string `json:"profileId"` + + // Quantités en heures + Q456 float64 `json:"q456"` // heures interventions jour hors dim/férié + Q458 float64 `json:"q458"` // heures interventions dimanche/férié + Q459 float64 `json:"q459"` // heures interventions nuit hors dim/férié + Q471 float64 `json:"q471"` // heures totales astreinte du mois + + // ✅ Nombre de dimanches / jours fériés "au forfait" (auto-rempli à l'import, modifiable) + NbDimFerie int `json:"nbDimFerie"` +} + +// Line = une ligne de la grille (456/458/459/471/480). +type Line struct { + Code string `json:"code"` + Label string `json:"label"` + Rate float64 `json:"rate"` + Quantity float64 `json:"quantity"` + Amount float64 `json:"amount"` + Unit string `json:"unit"` // "h" ou "j" +} + +// CalculateResponse = résultat du calcul (lignes + total) +type CalculateResponse struct { + Lines []Line `json:"lines"` + Total float64 `json:"total"` +} diff --git a/internal/models/pdf.go b/internal/models/pdf.go new file mode 100644 index 0000000..ed04b1f --- /dev/null +++ b/internal/models/pdf.go @@ -0,0 +1,24 @@ +package models + +// ExportPDFMeta = métadonnées utilisées pour nommer le fichier et afficher l'entête du PDF. +// +// YearMonth doit être au format YYYY-MM. +type ExportPDFMeta struct { + YearMonth string `json:"yearMonth"` + Nom string `json:"nom"` + Prenom string `json:"prenom"` + Matricule string `json:"matricule"` +} + +// ExportPDFRequest = données nécessaires à la génération du PDF. +// +// Note: on envoie les inputs (CalculateRequest) et on recalcule côté backend +// pour garantir que le PDF correspond exactement aux règles/taux du profil. +type ExportPDFRequest struct { + Meta ExportPDFMeta `json:"meta"` + Request CalculateRequest `json:"request"` +} + +type ExportPDFResponse struct { + Path string `json:"path"` +} diff --git a/internal/pdf/assets/paychek-logo.png b/internal/pdf/assets/paychek-logo.png new file mode 100644 index 0000000..fb08b24 Binary files /dev/null and b/internal/pdf/assets/paychek-logo.png differ diff --git a/internal/pdf/fonts/DejaVuSans-Bold.ttf b/internal/pdf/fonts/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..2cef1aa Binary files /dev/null and b/internal/pdf/fonts/DejaVuSans-Bold.ttf differ diff --git a/internal/pdf/fonts/DejaVuSans.ttf b/internal/pdf/fonts/DejaVuSans.ttf new file mode 100644 index 0000000..5789a29 Binary files /dev/null and b/internal/pdf/fonts/DejaVuSans.ttf differ diff --git a/internal/pdf/pdf.go b/internal/pdf/pdf.go new file mode 100644 index 0000000..4b9f1c5 --- /dev/null +++ b/internal/pdf/pdf.go @@ -0,0 +1,237 @@ +package pdf + +import ( + "bytes" + "embed" + "fmt" + "strings" + "time" + + "github.com/phpdave11/gofpdf" + + "git.dumerain.org/alban/calcul-astreintes/internal/models" +) + +//go:embed fonts/DejaVuSans.ttf fonts/DejaVuSans-Bold.ttf assets/paychek-logo.png +var assetFS embed.FS + +func BuildPDF( + meta models.ExportPDFMeta, + profileLabel string, + req models.CalculateRequest, + res models.CalculateResponse, +) ([]byte, error) { + + regularFont, err := assetFS.ReadFile("fonts/DejaVuSans.ttf") + if err != nil { + return nil, err + } + boldFont, err := assetFS.ReadFile("fonts/DejaVuSans-Bold.ttf") + if err != nil { + return nil, err + } + logoBytes, logoErr := assetFS.ReadFile("assets/paychek-logo.png") + hasLogo := logoErr == nil && len(logoBytes) > 0 + + pdf := gofpdf.New("P", "mm", "A4", "") + if hasLogo { + pdf.RegisterImageOptionsReader( + "paychek-logo", + gofpdf.ImageOptions{ImageType: "PNG"}, + bytes.NewReader(logoBytes), + ) + } + // ✅ IMPORTANT : on désactive l'auto page-break pour éviter une 2e page + pdf.SetMargins(12, 14, 12) + pdf.SetAutoPageBreak(false, 0) + pdf.AddPage() + + pdf.AddUTF8FontFromBytes("DejaVu", "", regularFont) + pdf.AddUTF8FontFromBytes("DejaVu", "B", boldFont) + + // A4 en mm + pageW := 210.0 + contentW := 170.0 + left := (pageW - contentW) / 2.0 + + created := time.Now().Format("02/01/2006 15:04") + + fullName := strings.TrimSpace(strings.Join([]string{ + meta.Nom, + meta.Prenom, + }, " ")) + if fullName == "" { + fullName = "(nom non renseigné)" + } + +// ---------------- TITRE ---------------- +titleY := pdf.GetY() +logoSize := 10.0 // mm +gap := 2.5 // espace texte ↔ logo + +pdf.SetFont("DejaVu", "B", 18) + +// Largeur du texte +title := "Paychek" +textW := pdf.GetStringWidth(title) + +// Largeur totale du bloc (texte + logo) +blockW := textW +if hasLogo { + blockW += gap + logoSize +} + +// Position X pour centrer le bloc +startX := left + (contentW-blockW)/2 + +// Texte +pdf.SetXY(startX, titleY) +pdf.CellFormat(textW, 10, title, "", 0, "L", false, 0, "") + +// Logo à droite du texte +if hasLogo { + pdf.ImageOptions( + "paychek-logo", + startX+textW+gap, + titleY, + logoSize, + logoSize, + false, + gofpdf.ImageOptions{ImageType: "PNG"}, + 0, + "", + ) +} + +pdf.Ln(10) + +pdf.SetFont("DejaVu", "", 11) +pdf.SetX(left) +pdf.CellFormat(contentW, 6, "Give me my fuc** money", "", 1, "C", false, 0, "") + + // ✅ Séparation franche sous le titre + pdf.Ln(6) + + // ✅ CENTRAGE VERTICAL : on place le "bloc principal" à une hauteur stable + // (A4, contenu fixe : 2 colonnes + tableau 5 lignes + légende) + // -> évite un rendu collé en haut. + startY := 48.0 + pdf.SetY(startY) + + // ---------------- IDENTITÉ / VALEURS ---------------- + pdf.SetFont("DejaVu", "B", 12) + pdf.SetX(left) + pdf.CellFormat(contentW/2-5, 6, "Identité", "", 0, "L", false, 0, "") + pdf.CellFormat(contentW/2-5, 6, "Valeurs importées", "", 1, "L", false, 0, "") + + pdf.SetFont("DejaVu", "", 11) + + y := pdf.GetY() + pdf.SetXY(left, y) + pdf.CellFormat(contentW/2-5, 6, "Mois : "+meta.YearMonth, "", 1, "L", false, 0, "") + pdf.SetX(left) + pdf.CellFormat(contentW/2-5, 6, "Agent : "+fullName, "", 1, "L", false, 0, "") + if strings.TrimSpace(meta.Matricule) != "" { + pdf.SetX(left) + pdf.CellFormat(contentW/2-5, 6, "Matricule : "+meta.Matricule, "", 1, "L", false, 0, "") + } else { + pdf.SetX(left) + pdf.CellFormat(contentW/2-5, 6, "Matricule : (non renseigné)", "", 1, "L", false, 0, "") + } + pdf.SetX(left) + pdf.CellFormat(contentW/2-5, 6, "Profil : "+profileLabel, "", 1, "L", false, 0, "") + + rightX := left + contentW/2 + 5 + pdf.SetXY(rightX, y) + pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("456 : %.2f h", req.Q456), "", 1, "L", false, 0, "") + pdf.SetX(rightX) + pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("458 : %.2f h", req.Q458), "", 1, "L", false, 0, "") + pdf.SetX(rightX) + pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("459 : %.2f h", req.Q459), "", 1, "L", false, 0, "") + pdf.SetX(rightX) + pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("471 : %.2f h", req.Q471), "", 1, "L", false, 0, "") + pdf.SetX(rightX) + pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("Dim/JF : %d j", req.NbDimFerie), "", 1, "L", false, 0, "") + + pdf.Ln(8) + + // ---------------- TABLEAU ---------------- + pdf.SetFont("DejaVu", "B", 12) + pdf.SetX(left) + pdf.CellFormat(contentW, 7, "Résultat", "", 1, "L", false, 0, "") + + // Largeurs adaptées (total = 170) + colW := []float64{24, 52, 38, 22, 34} + headerColor := []int{197, 227, 242} + + pdf.SetFont("DejaVu", "B", 9) + pdf.SetFillColor(headerColor[0], headerColor[1], headerColor[2]) + pdf.SetX(left) + headers := []string{"Code paie", "Libellé", "Nombre", "Base", "À payer"} + for i, h := range headers { + align := "L" + if i >= 2 { + align = "R" + } + pdf.CellFormat(colW[i], 7, h, "1", 0, align, true, 0, "") + } + pdf.Ln(-1) + + pdf.SetFont("DejaVu", "", 9) + for _, l := range res.Lines { + pdf.SetX(left) + pdf.CellFormat(colW[0], 7, l.Code, "1", 0, "L", false, 0, "") + pdf.CellFormat(colW[1], 7, l.Label, "1", 0, "L", false, 0, "") + pdf.CellFormat(colW[2], 7, fmt.Sprintf("%.2f %s", l.Quantity, l.Unit), "1", 0, "R", false, 0, "") + pdf.CellFormat(colW[3], 7, fmt.Sprintf("%.2f", l.Rate), "1", 0, "R", false, 0, "") + pdf.CellFormat(colW[4], 7, fmt.Sprintf("%.2f", l.Amount), "1", 0, "R", false, 0, "") + pdf.Ln(-1) + } + + // TOTAL BRUT (fond jaune clair) + pdf.SetFont("DejaVu", "B", 10) + pdf.SetFillColor(255, 249, 196) + pdf.SetX(left) + pdf.CellFormat(colW[0]+colW[1]+colW[2]+colW[3], 7, "TOTAL BRUT", "1", 0, "L", true, 0, "") + pdf.CellFormat(colW[4], 7, fmt.Sprintf("%.2f", res.Total), "1", 1, "R", true, 0, "") + + pdf.Ln(6) + + // ---------------- LÉGENDE ---------------- + pdf.SetFont("DejaVu", "B", 9) + pdf.SetX(left) + pdf.CellFormat(contentW, 5, "Correspondance code paie / fichier Excel", "", 1, "L", false, 0, "") + + pdf.SetFont("DejaVu", "", 8) + legend := "" + + "• 456 → colonne L (Interv.astreinte Jour)\n" + + "• 458 → colonne N (Interv.astreinte Dim & JF)\n" + + "• 459 → colonne M (Interv.astreinte Nuit)\n" + + "• 471 → colonne G (Astreinte = total d'heures du mois)\n" + + "• 480 → déduit des lignes \"P_DIM-JF\" (colonne C) → compteur Dim/JF\n" + pdf.SetX(left) + pdf.MultiCell(contentW, 4.2, legend, "", "L", false) + + // ---------------- FOOTER (toujours sur la page 1) ---------------- + // ✅ Position absolue proche du bas (A4=297mm) + marges + pdf.SetFont("DejaVu", "", 8) + pdf.SetTextColor(120, 120, 120) + pdf.SetXY(12, 284) + pdf.CellFormat( + 186, + 6, + fmt.Sprintf("Paychek - Made with \u2665 in Go by Flooze Corp \u00B7 A Niarmud Nablax Company - Exporté le %s", created), + "", + 0, + "C", + false, + 0, + "", + ) + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/internal/pdf/sanitize.go b/internal/pdf/sanitize.go new file mode 100644 index 0000000..5518b0d --- /dev/null +++ b/internal/pdf/sanitize.go @@ -0,0 +1,50 @@ +package pdf + +import ( + "regexp" + "strings" + "unicode" + + "golang.org/x/text/unicode/norm" +) + +// SanitizeFilename transforme une chaîne Unicode en nom de fichier sûr : +// - supprime les accents (Clément → Clement) +// - remplace les espaces par des tirets +// - supprime les caractères interdits (Windows / Unix) +// - évite les noms vides +func SanitizeFilename(input string) string { + if input == "" { + return "inconnu" + } + + // 1) Normalisation Unicode (NFD) pour séparer lettres + accents + t := norm.NFD.String(input) + + // 2) Suppression des marques diacritiques (accents) + sb := strings.Builder{} + for _, r := range t { + if unicode.Is(unicode.Mn, r) { + continue + } + sb.WriteRune(r) + } + out := sb.String() + + // 3) Remplacements simples + out = strings.ReplaceAll(out, " ", "-") + out = strings.ReplaceAll(out, "_", "-") + + // 4) Suppression des caractères interdits dans les noms de fichiers + // Windows + Unix + re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`) + out = re.ReplaceAllString(out, "") + + // 5) Nettoyage final + out = strings.Trim(out, "-.") + if out == "" { + return "inconnu" + } + + return out +} diff --git a/internal/profiles/loader.go b/internal/profiles/loader.go new file mode 100644 index 0000000..d1347fa --- /dev/null +++ b/internal/profiles/loader.go @@ -0,0 +1,98 @@ +package profiles + +import ( + "embed" + "encoding/json" + "fmt" + "sort" + "sync" + + "git.dumerain.org/alban/calcul-astreintes/internal/models" +) + +//go:embed profiles.json +var embeddedFS embed.FS + +type profileJSON struct { + Label string `json:"label"` + Matricule string `json:"matricule,omitempty"` // ✅ pour auto-match avec l'agent importé + Rates map[string]float64 `json:"rates"` + ForfaitDimFerie float64 `json:"forfait_dim_ferie"` +} + +type Repo struct { + mu sync.RWMutex + profiles map[string]models.Profile + rules models.GlobalRules +} + +// NewEmbeddedRepo charge les profils depuis profiles.json (embarqué). +func NewEmbeddedRepo() (*Repo, *models.GlobalRules, error) { + b, err := embeddedFS.ReadFile("profiles.json") + if err != nil { + return nil, nil, err + } + + var raw map[string]profileJSON + if err := json.Unmarshal(b, &raw); err != nil { + return nil, nil, fmt.Errorf("profiles.json invalide: %w", err) + } + + repo := &Repo{profiles: map[string]models.Profile{}} + + for id, p := range raw { + repo.profiles[id] = models.Profile{ + ID: id, + Label: p.Label, + Matricule: p.Matricule, + T456: p.Rates["456"], + T458: p.Rates["458"], + T459: p.Rates["459"], + T471: p.Rates["471"], + } + if repo.rules.ForfaitDimFerie == 0 { + repo.rules.ForfaitDimFerie = p.ForfaitDimFerie + } + } + + // Valeur par défaut de sécurité si le JSON n'a aucun profil + if repo.rules.ForfaitDimFerie == 0 { + repo.rules.ForfaitDimFerie = 120 + } + + return repo, &repo.rules, nil +} + +// List renvoie les profils triés par label (dropdown stable). +func (r *Repo) List() ([]models.ProfileListItem, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + items := make([]models.ProfileListItem, 0, len(r.profiles)) + for _, p := range r.profiles { + items = append(items, models.ProfileListItem{ + ID: p.ID, + Label: p.Label, + Matricule: p.Matricule, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].Label < items[j].Label }) + return items, nil +} + +func (r *Repo) Get(id string) (models.Profile, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + p, ok := r.profiles[id] + if !ok { + return models.Profile{}, fmt.Errorf("profil introuvable: %s", id) + } + return p, nil +} + +func (r *Repo) Rules() models.GlobalRules { + r.mu.RLock() + defer r.mu.RUnlock() + return r.rules +} diff --git a/internal/profiles/profiles.json b/internal/profiles/profiles.json new file mode 100644 index 0000000..1290f53 --- /dev/null +++ b/internal/profiles/profiles.json @@ -0,0 +1,46 @@ +{ + "test": { + "label": "TEST (fallback)", + "matricule": "", + "rates": { + "456": 1, + "458": 1, + "459": 1, + "471": 1 + }, + "forfait_dim_ferie": 120 + }, + "alban": { + "label": "DUMERAIN Alban (165004)", + "matricule": "165004", + "rates": { + "456": 18.03, + "458": 30.05, + "459": 36.06, + "471": 3.58 + }, + "forfait_dim_ferie": 120 + }, + "lydie": { + "label": "ROUQUIER Lydie (266340)", + "matricule": "266340", + "rates": { + "456": 15.24, + "458": 25.4, + "459": 30.48, + "471": 3.03 + }, + "forfait_dim_ferie": 120 + }, + "clement": { + "label": "DUFETELLE Clement (178549)", + "matricule": "178549", + "rates": { + "456": 19.05, + "458": 31.75, + "459": 38.1, + "471": 3.78 + }, + "forfait_dim_ferie": 120 + } +} diff --git a/internal/store/profiles.json b/internal/store/profiles.json new file mode 100644 index 0000000..756d05d --- /dev/null +++ b/internal/store/profiles.json @@ -0,0 +1,15 @@ +{ + "rules": { + "forfait_dim_ferie": 120.0 + }, + "profiles": [ + { + "id": "alban", + "label": "Alban", + "t456": 18.03, + "t458": 30.05, + "t459": 36.06, + "t471": 3.58 + } + ] +} diff --git a/internal/store/repo.go b/internal/store/repo.go new file mode 100644 index 0000000..669104f --- /dev/null +++ b/internal/store/repo.go @@ -0,0 +1,69 @@ +package store + +import ( + "embed" + "encoding/json" + "fmt" + "sort" + "sync" + + "git.dumerain.org/alban/calcul-astreintes/internal/models" +) + +/* +On embarque internal/store/profiles.json dans le binaire. +Ça simplifie énormément le "standalone": pas de fichier externe obligatoire. +*/ +//go:embed profiles.json +var embeddedFS embed.FS + +type ProfileRepo struct { + mu sync.RWMutex + profiles map[string]models.Profile +} + +type embeddedData struct { + Rules models.GlobalRules `json:"rules"` + Profiles []models.Profile `json:"profiles"` +} + +func NewEmbeddedRepo() (*ProfileRepo, *models.GlobalRules, error) { + b, err := embeddedFS.ReadFile("profiles.json") + if err != nil { + return nil, nil, err + } + var data embeddedData + if err := json.Unmarshal(b, &data); err != nil { + return nil, nil, err + } + + repo := &ProfileRepo{profiles: map[string]models.Profile{}} + for _, p := range data.Profiles { + repo.profiles[p.ID] = p + } + return repo, &data.Rules, nil +} + +// List : profils triés par label (pour un dropdown stable). +func (r *ProfileRepo) List() ([]models.ProfileListItem, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + items := make([]models.ProfileListItem, 0, len(r.profiles)) + for _, p := range r.profiles { + items = append(items, models.ProfileListItem{ID: p.ID, Label: p.Label}) + } + sort.Slice(items, func(i, j int) bool { return items[i].Label < items[j].Label }) + return items, nil +} + +func (r *ProfileRepo) Get(id string) (models.Profile, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + p, ok := r.profiles[id] + if !ok { + return models.Profile{}, fmt.Errorf("profil introuvable: %s", id) + } + return p, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7d20f5c --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "embed" + "log" + + "git.dumerain.org/alban/calcul-astreintes/app" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +/* +Wails embarque des fichiers statiques (le build du frontend) dans le binaire Go. +⚠️ Go exige que le pattern //go:embed matche au moins 1 fichier. +C'est pourquoi on garde un fichier placeholder: frontend/dist/.keep +*/ +//go:embed frontend/dist/* +var assets embed.FS + +func main() { + // Instance de l'application Wails (backend Go) + application := app.NewApp() + + // Lancement de la fenêtre desktop + serveur d'assets + err := wails.Run(&options.App{ + Title: "Paychek - Give me my fuc** money", + Width: 980, + Height: 720, + MinWidth: 880, + MinHeight: 640, + + AssetServer: &assetserver.Options{ + Assets: assets, + }, + + BackgroundColour: &options.RGBA{R: 245, G: 245, B: 245, A: 1}, + + OnStartup: application.Startup, + + // Méthodes exportées au frontend via "wailsjs" + Bind: []interface{}{ + application, + }, + }) + if err != nil { + log.Fatal(err) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..ac8b37e --- /dev/null +++ b/wails.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "calcul-astreintes", + "title": "Paychek - Give me my money", + "outputfilename": "calcul-astreintes", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "frontend:dir": "frontend", + "wailsjsdir": "frontend/src" +}